Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14037] ファイルの概要

このドキュメントは、Go言語の標準ライブラリ text/template パッケージにおけるコミット 421b75c0dbab3460dbfdb023d67ce0807c4d5327 の詳細な技術解説を提供します。このコミットは、Node インターフェースに非公開メソッドを追加することで、パッケージの内部実装を外部からの不適切な利用から保護することを目的としています。

コミット

commit 421b75c0dbab3460dbfdb023d67ce0807c4d5327
Author: Rob Pike <r@golang.org>
Date:   Sun Oct 7 07:15:11 2012 +1100

    text/template: add an unexported method to Node
    Protects the package a little against undesirable clients.
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6624054

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/421b75c0dbab3460dbfdb023d67ce0807c4d5327

元コミット内容

text/template: add an unexported method to Node Protects the package a little against undesirable clients.

このコミットは、text/template パッケージの Node インターフェースに非公開メソッドを追加し、望ましくないクライアントからのパッケージ保護を強化することを目的としています。

変更の背景

Go言語では、インターフェースは型が特定のメソッドセットを実装していることを保証する強力なメカニズムです。しかし、公開されたインターフェースは、そのインターフェースを実装する任意の型によって満たされる可能性があります。これは通常望ましい動作ですが、特定のパッケージの内部構造を外部から変更したり、不正な方法で利用したりすることを防ぎたい場合には問題となることがあります。

text/template パッケージは、Goのテンプレートエンジンの中核をなすものであり、そのパースツリー(構文木)を表現する Node インターフェースは非常に重要です。このインターフェースが外部の型によって自由に実装されると、パッケージの内部ロジックが意図しない形で操作されたり、互換性のない変更が将来導入されたりするリスクがありました。

このコミットの背景には、このような内部構造の保護と、パッケージの安定性および保守性の向上という目的があります。非公開メソッドを追加することで、Node インターフェースを実装できる型を、その非公開メソッドを定義できる同じパッケージ内の型に限定することができます。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たしていると見なされます(暗黙的な実装)。

type MyInterface interface {
    MethodA()
    MethodB()
}

type MyStruct struct{}

func (s MyStruct) MethodA() {}
func (s MyStruct) MethodB() {}

// MyStruct は MyInterface を満たす

公開(Exported)と非公開(Unexported)

Go言語では、識別子(変数、関数、型、メソッドなど)の名前の最初の文字が大文字である場合、その識別子は「公開(exported)」され、パッケージの外部からアクセス可能です。最初の文字が小文字である場合、その識別子は「非公開(unexported)」であり、その識別子が定義されているパッケージ内からのみアクセス可能です。

このルールは、インターフェースのメソッド名にも適用されます。

インターフェースの「シール」

Go言語には、JavaやC#のような明示的な「sealed」キーワードはありませんが、非公開メソッドを利用することで、インターフェースの実装を特定のパッケージ内に限定するイディオムが存在します。これを「インターフェースのシール」と呼ぶことがあります。

技術的詳細

このコミットの核心は、Node インターフェースに unexported() という非公開メソッドを追加することです。

変更前:

type Node interface {
	Type() NodeType
	String() string
	// CopyXxx methods that return *XxxNode.
	Copy() Node
	Position() Pos // byte position of start of node in full original input string
}

変更後:

type Node interface {
	Type() NodeType
	String() string
	// CopyXxx methods that return *XxxNode.
	Copy() Node
	Position() Pos // byte position of start of node in full original input string
	// Make sure only functions in this package can create Nodes.
	unexported()
}

そして、この非公開メソッド unexported() の実装を、Node インターフェースを実装するすべての型が埋め込んでいる Pos 型に追加します。

// unexported keeps Node implementations local to the package.
// All implementations embed Pos, so this takes care of it.
func (Pos) unexported() {
}

なぜこれで保護されるのか?

  1. 非公開メソッドの制約: unexported() メソッドは小文字で始まるため、text/template/parse パッケージの外部からはアクセスできません。
  2. インターフェースの実装条件: Goのインターフェースは、そのインターフェースが定義するすべてのメソッドを型が実装している場合に満たされます。
  3. 外部からの実装の阻止: 外部のパッケージで Node インターフェースを実装しようとする型は、unexported() メソッドを実装する必要があります。しかし、このメソッドは非公開であるため、外部パッケージからはそのシグネチャを知ることも、呼び出すことも、実装することもできません。
  4. パッケージ内での実装: text/template/parse パッケージ内では、Pos 型が unexported() メソッドを実装しており、Node インターフェースを実装する他のすべての型が Pos を埋め込んでいるため、自動的に unexported() メソッドも実装することになります。これにより、パッケージ内の型は引き続き Node インターフェースを満たすことができます。

このメカニズムにより、Node インターフェースを実装できるのは text/template/parse パッケージ内の型のみに限定され、外部からの意図しない、あるいは不正な Node の実装を防ぐことができます。これは、パッケージの内部構造をカプセル化し、APIの安定性を高めるための一般的なGoのイディオムです。

コアとなるコードの変更箇所

変更は src/pkg/text/template/parse/node.go ファイルに集中しています。

--- a/src/pkg/text/template/parse/node.go
+++ b/src/pkg/text/template/parse/node.go
@@ -13,7 +13,9 @@ import (
 	"strings"
 )
 
-// A node is an element in the parse tree. The interface is trivial.
+// A Node is an element in the parse tree. The interface is trivial.
+// The interface contains an unexported method so that only
+// types local to this package can satisfy it.
 type Node interface {
 	Type() NodeType
 	String() string
@@ -22,6 +24,8 @@ type Node interface {
 	// CopyXxx methods that return *XxxNode.
 	Copy() Node
 	Position() Pos // byte position of start of node in full original input string
+	// Make sure only functions in this package can create Nodes.
+	unexported()
 }
 
 // NodeType identifies the type of a parse tree node.
@@ -35,6 +39,11 @@ func (p Pos) Position() Pos {
 	return p
 }
 
+// unexported keeps Node implementations local to the package.
+// All implementations embed Pos, so this takes care of it.
+func (Pos) unexported() {
+}
+
 // Type returns itself and provides an easy default implementation
 // for embedding in a Node. Embedded in all non-trivial Nodes.
 func (t NodeType) Type() NodeType {

コアとなるコードの解説

  1. Node インターフェースの変更: Node インターフェースの定義に unexported() メソッドが追加されました。このメソッドはコメントで「このパッケージ内の関数のみがNodeを作成できるようにする」と説明されています。これにより、Node インターフェースを実装するすべての型は、この非公開メソッドも実装する必要が生じます。

  2. Pos 型への unexported() メソッドの実装: Pos 型に unexported() メソッドが追加されました。

    func (Pos) unexported() {
    }
    

    このメソッドはレシーバが Pos 型であり、中身は空です。重要なのは、text/template/parse パッケージ内のすべての Node 実装型(例: ListNode, CommandNode など)が Pos 型を埋め込んでいることです。Goの埋め込みのルールにより、埋め込まれた型のメソッドは、埋め込み元の型でも利用可能になります。したがって、Node インターフェースを実装するすべての型は、Pos を埋め込むことで自動的に unexported() メソッドも実装することになり、Node インターフェースの要件を満たします。

この変更により、Node インターフェースの実装は text/template/parse パッケージ内に限定され、外部パッケージが勝手に Node インターフェースを実装してパッケージの内部構造を操作することを防ぎます。これは、ライブラリの堅牢性と将来の互換性を確保するための重要な設計パターンです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のインターフェースの「シール」に関する一般的な議論(Goコミュニティ内)
  • https://golang.org/cl/6624054 (Go Code Review)