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

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

このコミットは、Go言語のドキュメンテーションツールである go/doc パッケージにおいて、埋め込み型(embedded types)のメソッドを収集するための初期段階の変更を導入します。具体的には、匿名フィールドとして埋め込まれた型のメソッドを go/doc が認識し、ドキュメントに含めるための基盤を構築しています。現時点では外部から見える変更はありませんが、将来的にこれらのメソッドがドキュメントに表示されるようにするための重要なステップです。

コミット

go/doc: steps towards collecting methods of embedded types

No visible external changes yet. The current approach is
a stop-gap approach: For methods of anonymous fields to be
seen, the anonymous field's types must be exported.

Missing: computing the actual MethodDocs and displaying them.

(Depending on the operation mode of godoc, the input to go/doc
is a pre-filtered AST with all non-exported nodes removed. Non-
exported anonymous fields are not even seen by go/doc in this
case, and it is impossible to collect associated (even exported)
methods. A correct fix will require some more significant re-
engineering; AST filtering will have to happen later, possibly
inside go/doc.)

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5500055

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

https://github.com/golang/go/commit/97853b46a08e984048e65f1d9c359bb48b8f22e4

元コミット内容

go/doc: steps towards collecting methods of embedded types

No visible external changes yet. The current approach is
a stop-gap approach: For methods of anonymous fields to be
seen, the anonymous field's types must be exported.

Missing: computing the actual MethodDocs and displaying them.

(Depending on the operation mode of godoc, the input to go/doc
is a pre-filtered AST with all non-exported nodes removed. Non-
exported anonymous fields are not even seen by go/doc in this
case, and it is impossible to collect associated (even exported)
methods. A correct fix will require some more significant re-
engineering; AST filtering will have to happen later, possibly
inside go/doc.)

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5500055

変更の背景

この変更の背景には、Go言語のドキュメンテーションツール godoc が、埋め込み型(特に匿名フィールドとして埋め込まれた型)のメソッドを適切に収集・表示できないという課題がありました。

従来の godoc の動作モードによっては、go/doc パッケージへの入力として、エクスポートされていない(非公開の)ノードがすべて削除された抽象構文木(AST)が渡されることがありました。この「事前フィルタリング」のプロセスにより、非公開の匿名フィールドは go/doc から完全に隠されてしまい、結果として、たとえその匿名フィールドの型が公開されており、公開されたメソッドを持っていたとしても、go/doc はそれらのメソッドを認識し、ドキュメントに含めることができませんでした。

このコミットは、この問題を根本的に解決するための「一時的な(stop-gap)」アプローチとして、まず go/doc が埋め込み型を認識し、その情報を内部的に保持できるようにするための基盤を導入しています。最終的な解決策としては、ASTのフィルタリングを go/doc の内部で行うなど、より大規模な再設計が必要であると認識されていますが、このコミットはその第一歩となります。

前提知識の解説

Go言語の埋め込み型(Embedded Types)と匿名フィールド(Anonymous Fields)

Go言語には、構造体(struct)内に他の型を「埋め込む」機能があります。これは、構造体のフィールドとして型名のみを指定し、フィールド名を省略することで実現されます。これを「匿名フィールド」と呼びます。匿名フィールドとして型を埋め込むと、その埋め込まれた型のメソッドが、外側の構造体のメソッドであるかのように直接呼び出せるようになります。これは、Goにおける「コンポジションによるコードの再利用」の主要なメカニズムの一つです。

例:

type Base struct {
    Name string
}

func (b Base) Greet() string {
    return "Hello, " + b.Name
}

type User struct {
    Base // Base型を匿名フィールドとして埋め込み
    Email string
}

func main() {
    u := User{Base: Base{Name: "Alice"}, Email: "alice@example.com"}
    println(u.Greet()) // User型からBase型のGreetメソッドを直接呼び出し
}

go/doc パッケージ

go/doc パッケージは、Goのソースコードからドキュメンテーションを生成するための標準ライブラリです。godoc コマンドやGoの公式ドキュメントサイト(pkg.go.devなど)のバックエンドとして利用されています。このパッケージは、Goのソースコードを解析してASTを構築し、そのASTからパッケージ、型、関数、変数などのドキュメンテーションコメントやシグネチャを抽出し、構造化されたデータとして提供します。

抽象構文木(Abstract Syntax Tree: AST)

ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやインタプリタ、コード分析ツールなどがソースコードを処理する際に、字句解析(トークン化)と構文解析(パース)を経て生成されます。Go言語では、go/ast パッケージがASTの表現と操作を提供します。go/doc はこのASTを読み込み、ドキュメンテーション情報を抽出します。

エクスポートされた(Exported)識別子と非エクスポートされた(Unexported)識別子

Go言語では、識別子(変数名、関数名、型名、フィールド名など)の最初の文字が大文字である場合、その識別子はパッケージ外からアクセス可能(エクスポートされている)になります。小文字で始まる場合は、パッケージ内でのみアクセス可能(エクスポートされていない、非公開)です。godoc は通常、エクスポートされた識別子のみをドキュメント化の対象とします。

技術的詳細

このコミットは、src/pkg/go/doc/doc.go ファイルに対して変更を加えています。主な目的は、go/doc が埋め込み型、特に匿名フィールドとして埋め込まれた型の情報を内部的に追跡できるようにすることです。

変更の核心は、typeDoc および docReader という内部構造体の定義の拡張と、それらに関連するヘルパー関数の修正です。

  1. typeDoc 構造体の拡張:

    • typeDoc は、特定の型に関連するドキュメンテーション情報を保持する構造体です。
    • 新たに embedded []*typeDoc フィールドが追加されました。これは、その型が埋め込んでいる他の型の typeDoc リストを保持するためのものです。これにより、go/doc は型の階層構造と埋め込み関係を内部的に表現できるようになります。
  2. docReader 構造体の拡張:

    • docReader は、単一のパッケージのドキュメンテーションを収集する際に使用される構造体です。
    • 新たに embedded map[string]*typeDoc フィールドが追加されました。これは、パッケージ内で見つかった埋め込み型(エクスポートされているかどうかにかかわらず)を名前でルックアップできるようにするためのマップです。
  3. lookupEmbeddedDoc 関数の追加:

    • docReaderlookupEmbeddedDoc という新しいヘルパー関数が追加されました。この関数は、与えられた型名に対応する typeDocdocReader.embedded マップから検索し、存在しない場合は新しい typeDoc を作成してマップに追加します。これにより、埋め込み型が初めて検出されたときにその情報を登録できるようになります。
  4. baseTypeName 関数の変更:

    • baseTypeName 関数は、ASTノードから基本となる型名を抽出するために使用されます。
    • この関数に allTypes という新しいブール引数が追加されました。この引数が true の場合、型がエクスポートされているかどうかにかかわらず、その名前を返します。これは、非エクスポートの匿名フィールドの型名も取得できるようにするために重要です。
  5. addDecl 関数の変更:

    • addDecl 関数は、パッケージ内の宣言(型宣言、変数宣言など)を処理し、docReader に追加する役割を担います。
    • 型宣言を処理する部分で、構造体やインターフェースのフィールドを走査し、匿名フィールド(len(field.Names) == 0 で識別)を検出するロジックが追加されました。
    • 匿名フィールドが見つかった場合、baseTypeNameallTypes=true で呼び出してその型名を取得し、lookupEmbeddedDoc を使用して対応する typeDoc を取得します。
    • 取得した typeDoc は、親の型の typeDoc.embedded リストに追加されます。

この変更により、go/doc は、ソースコードを解析する際に、構造体やインターフェースがどのような型を匿名フィールドとして埋め込んでいるかを内部的に記録できるようになります。コミットメッセージにもあるように、現時点ではこれらの埋め込み型のメソッドが実際にドキュメントとして表示されるわけではありませんが、そのためのデータ収集の基盤が整えられました。

「stop-gap approach」という表現は、この変更が完全な解決策ではないことを示唆しています。特に、godoc の動作モードによっては、go/doc に渡されるASTが既に非エクスポートノードをフィルタリングしているため、非エクスポートの匿名フィールド自体が go/doc から見えないという根本的な問題が残っています。この問題に対処するには、ASTフィルタリングのタイミングや方法を再検討する必要がある、とコミットメッセージは述べています。

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

src/pkg/go/doc/doc.go ファイルにおける主要な変更箇所は以下の通りです。

typeDoc 構造体の変更

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -18,10 +18,11 @@ type typeDoc struct {
 	// len(decl.Specs) == 1, and the element type is *ast.TypeSpec
 	// if the type declaration hasn't been seen yet, decl is nil
 	decl *ast.GenDecl
-	// values, factory functions, and methods associated with the type
+	// declarations associated with the type
 	values    []*ast.GenDecl // consts and vars
 	factories map[string]*ast.FuncDecl
 	methods   map[string]*ast.FuncDecl
+	embedded  []*typeDoc // list of embedded types
 }

docReader 構造体の変更

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -32,17 +33,19 @@ type typeDoc struct {
 // printing the corresponding AST node).\n //
 type docReader struct {
-	doc     *ast.CommentGroup // package documentation, if any
-	pkgName string
-	values  []*ast.GenDecl // consts and vars
-	types   map[string]*typeDoc
-	funcs   map[string]*ast.FuncDecl
-	bugs    []*ast.CommentGroup
+	doc      *ast.CommentGroup // package documentation, if any
+	pkgName  string
+	values   []*ast.GenDecl // consts and vars
+	types    map[string]*typeDoc
+	embedded map[string]*typeDoc // embedded types, possibly not exported
+	funcs    map[string]*ast.FuncDecl
+	bugs     []*ast.CommentGroup
 }
 
 func (doc *docReader) init(pkgName string) {
 	doc.pkgName = pkgName
 	doc.types = make(map[string]*typeDoc)
+	doc.embedded = make(map[string]*typeDoc)
 	doc.funcs = make(map[string]*ast.FuncDecl)
 }

lookupEmbeddedDoc 関数の追加

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -87,21 +84,35 @@ func (doc *docReader) lookupTypeDoc(name string) *typeDoc {
 	\t\treturn tdoc
 	\t}\n \t// type wasn\'t found - add one without declaration
-\ttdoc := &typeDoc{nil, nil, make(map[string]*ast.FuncDecl), make(map[string]*ast.FuncDecl)}\n+\ttdoc := &typeDoc{nil, nil, make(map[string]*ast.FuncDecl), make(map[string]*ast.FuncDecl), nil}\n \tdoc.types[name] = tdoc\n \treturn tdoc\n }\n \n-func baseTypeName(typ ast.Expr) string {\n+func (doc *docReader) lookupEmbeddedDoc(name string) *typeDoc {\n+\tif name == "" {\n+\t\treturn nil\n+\t}\n+\tif tdoc, found := doc.embedded[name]; found {\n+\t\treturn tdoc\n+\t}\n+\t// type wasn\'t found - add one without declaration\n+\t// note: embedded types only have methods associated with them\n+\ttdoc := &typeDoc{nil, nil, make(map[string]*ast.FuncDecl), make(map[string]*ast.FuncDecl), nil}\n+\tdoc.embedded[name] = tdoc\n+\treturn tdoc\n+}\n+\n+func baseTypeName(typ ast.Expr, allTypes bool) string {\n \tswitch t := typ.(type) {\n \tcase *ast.Ident:\n \t\t// if the type is not exported, the effect to\n \t\t// a client is as if there were no type name
-\t\tif t.IsExported() {\n+\t\tif t.IsExported() || allTypes {\n \t\t\treturn t.Name\n \t\t}\n \tcase *ast.StarExpr:\
-\t\treturn baseTypeName(t.X)\n+\t\treturn baseTypeName(t.X, allTypes)\n \t}\n \treturn ""\n }\

baseTypeName 関数の変更(引数追加)

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -99,10 +115,10 @@ func (doc *docReader) lookupEmbeddedDoc(name string) *typeDoc {
 	return tdoc
 }
 
-func baseTypeName(typ ast.Expr) string {
+func baseTypeName(typ ast.Expr, allTypes bool) string {
 	switch t := typ.(type) {
 	case *ast.Ident:
 		// if the type is not exported, the effect to
 		// a client is as if there were no type name
-		if t.IsExported() {
+		if t.IsExported() || allTypes {
 			return t.Name
 		}
 	case *ast.StarExpr:
-		return baseTypeName(t.X)
+		return baseTypeName(t.X, allTypes)
 	}
 	return ""
 }

addValue, addFunc 関数の baseTypeName 呼び出しの変更

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -120,7 +131,7 @@ func (doc *docReader) addValue(decl *ast.GenDecl) {
 		\t\tswitch {\n \t\t\tcase v.Type != nil:\n \t\t\t\t// a type is present; determine its name
-\t\t\t\tname = baseTypeName(v.Type)\n+\t\t\t\tname = baseTypeName(v.Type, false)\n \t\t\tcase decl.Tok == token.CONST:\n \t\t\t\t// no type is present but we have a constant declaration;\n \t\t\t\t// use the previous type name (w/o more type information
@@ -178,7 +189,7 @@ func (doc *docReader) addFunc(fun *ast.FuncDecl) {
 	// determine if it should be associated with a type
 	if fun.Recv != nil {
 		// method
-\t\ttyp := doc.lookupTypeDoc(baseTypeName(fun.Recv.List[0].Type))\n+\t\ttyp := doc.lookupTypeDoc(baseTypeName(fun.Recv.List[0].Type, false))\n \t\tif typ != nil {\n \t\t\t// exported receiver type\n \t\t\tsetFunc(typ.methods, fun)\
@@ -199,7 +210,7 @@ func (doc *docReader) addFunc(fun *ast.FuncDecl) {
 	\t\t\t// exactly one (named or anonymous) result associated\n \t\t\t// with the first type in result signature (there may\n \t\t\t// be more than one result)
-\t\t\ttname := baseTypeName(res.Type)\n+\t\t\ttname := baseTypeName(res.Type, false)\n \t\t\ttyp := doc.lookupTypeDoc(tname)\n \t\t\tif typ != nil {\n \t\t\t\t// named and exported result type

addDecl 関数の変更(匿名フィールドの処理追加)

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -235,8 +246,30 @@ func (doc *docReader) addDecl(decl ast.Decl) {
 	\t\t\t\t\t// makeTypeDocs below). Simpler data structures, but\n \t\t\t\t\t// would lose GenDecl documentation if the TypeSpec\n \t\t\t\t\t// has documentation as well.
-\t\t\t\t\tdoc.addType(&ast.GenDecl{d.Doc, d.Pos(), token.TYPE, token.NoPos, []ast.Spec{spec}, token.NoPos})\n+\t\t\t\t\ttdoc := doc.addType(&ast.GenDecl{d.Doc, d.Pos(), token.TYPE, token.NoPos, []ast.Spec{spec}, token.NoPos})\n \t\t\t\t\t// A new GenDecl node is created, no need to nil out d.Doc.\n+\t\t\t\t\tif tdoc == nil {\n+\t\t\t\t\t\tcontinue // some error happened; ignore\n+\t\t\t\t\t}\n+\t\t\t\t\tvar fields *ast.FieldList\n+\t\t\t\t\tswitch typ := spec.(*ast.TypeSpec).Type.(type) {\n+\t\t\t\t\tcase *ast.StructType:\n+\t\t\t\t\t\tfields = typ.Fields\n+\t\t\t\t\tcase *ast.InterfaceType:\n+\t\t\t\t\t\tfields = typ.Methods\n+\t\t\t\t\t}\n+\t\t\t\t\tif fields == nil {\n+\t\t\t\t\t\tfor _, field := range fields.List {\n+\t\t\t\t\t\t\tif len(field.Names) == 0 {\n+\t\t\t\t\t\t\t\t// anonymous field\n+\t\t\t\t\t\t\t\tname := baseTypeName(field.Type, true)\n+\t\t\t\t\t\t\t\tedoc := doc.lookupEmbeddedDoc(name)\n+\t\t\t\t\t\t\t\tif edoc != nil {\n+\t\t\t\t\t\t\t\t\ttdoc.embedded = append(tdoc.embedded, edoc)\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n \t\t\t\t}\n \t\t\t}\n \t\t}\

TypeDoc 構造体の変更(Embedded フィールド追加)

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -408,6 +441,7 @@ type TypeDoc struct {
 	Vars      []*ValueDoc
 	Factories []*FuncDoc
 	Methods   []*FuncDoc
+	Embedded  []*FuncDoc
 	Decl      *ast.GenDecl
 	order     int
 }

makeTypeDocs 関数の変更(TODOコメント追加)

--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -452,6 +486,7 @@ func (doc *docReader) makeTypeDocs(m map[string]*typeDoc) []*TypeDoc {
 	\t\t\tt.Vars = makeValueDocs(old.values, token.VAR)\n \t\t\t\tt.Factories = makeFuncDocs(old.factories)\n \t\t\t\tt.Methods = makeFuncDocs(old.methods)\n+\t\t\t// TODO(gri) compute list of embedded methods \n \t\t\t\tt.Decl = old.decl\n \t\t\t\tt.order = i\n \t\t\t\td[i] = t\

コアとなるコードの解説

typeDoc 構造体への embedded []*typeDoc の追加

これは、ある型が匿名フィールドとして他の型を埋め込んでいる場合に、その埋め込まれた型の typeDoc への参照を保持するためのリストです。これにより、go/doc は型の埋め込み関係を内部的に追跡できるようになります。将来的には、このリストを使って埋め込み型のメソッドを収集し、ドキュメントに含めることが可能になります。

docReader 構造体への embedded map[string]*typeDoc の追加と init メソッドでの初期化

docReader はパッケージ全体のドキュメンテーション収集を管理する構造体です。embedded マップは、パッケージ内で見つかったすべての埋め込み型(エクスポートされているかどうかにかかわらず)を、その型名をキーとして typeDoc にマッピングするために使用されます。これにより、go/doc は埋め込み型を一元的に管理し、必要に応じてルックアップできるようになります。init メソッドでの初期化は、マップが使用される前に適切に準備されることを保証します。

lookupEmbeddedDoc 関数の追加

この新しいヘルパー関数は、docReader.embedded マップから特定の名前の埋め込み型に対応する typeDoc を取得します。もしその名前の typeDoc がまだマップに存在しない場合は、新しい typeDoc を作成してマップに追加し、それを返します。これにより、埋め込み型が初めて検出されたときに、その情報を docReader に登録するメカニズムが提供されます。

baseTypeName 関数の allTypes 引数の追加と使用箇所の変更

baseTypeName 関数は、ASTノードから型の基本名を抽出します。元の実装では、型がエクスポートされていない場合、空文字列を返していました。しかし、埋め込み型のメソッドを収集するためには、非エクスポートの匿名フィールドの型名も知る必要があります。 allTypes 引数が true の場合、t.IsExported() のチェックをスキップし、型がエクスポートされているかどうかにかかわらずその名前を返します。これにより、go/doc は非公開の匿名フィールドの型名も取得できるようになり、その型に関連するメソッドを追跡する可能性が開かれます。 addValue, addFunc 関数内での baseTypeName の呼び出しでは、allTypesfalse に設定しています。これは、これらのコンテキストでは通常、エクスポートされた型のみが関連するためです。

addDecl 関数における匿名フィールドの処理ロジックの追加

この変更は、型宣言(token.TYPE)を処理する際に、構造体やインターフェースのフィールドを走査し、匿名フィールドを識別するものです。

  • spec.(*ast.TypeSpec).Type から、それが *ast.StructType または *ast.InterfaceType であるかを判断し、それぞれの Fields または Methods を取得します。
  • 取得したフィールドリストをループし、len(field.Names) == 0 で匿名フィールドを検出します。
  • 匿名フィールドが見つかった場合、baseTypeName(field.Type, true) を呼び出して、その匿名フィールドの型名(エクスポートされているかどうかにかかわらず)を取得します。
  • 取得した型名を使って doc.lookupEmbeddedDoc(name) を呼び出し、対応する typeDoc を取得します。
  • 最後に、取得した edoc (embedded typeDoc) を、現在の型の tdoc.embedded リストに追加します。

このロジックにより、go/doc は、構造体やインターフェースがどのような型を匿名フィールドとして埋め込んでいるかを、ASTを解析する過程で正確に記録できるようになります。

TypeDoc 構造体への Embedded []*FuncDoc の追加

これは、go/doc パッケージが最終的に生成する公開APIの一部である TypeDoc 構造体への変更です。Embedded フィールドは、その型が埋め込んでいる型のメソッドを保持するためのものです。現時点では、このフィールドは makeTypeDocs 関数内で // TODO(gri) compute list of embedded methods というコメントと共に空のままですが、将来的に埋め込み型のメソッドが収集され、ここに格納されることを意図しています。

これらの変更は、go/doc がGoの埋め込み型のセマンティクスをより深く理解し、最終的にそれらのメソッドをドキュメントに含めるための重要な内部的なステップです。

関連リンク

参考にした情報源リンク

  • GitHub: golang/go commit 97853b46a08e984048e65f1d9c359bb48b8f22e4
  • Go言語の埋め込み型に関する公式ドキュメントやチュートリアル (一般的なGoの知識として参照)
  • GoのASTに関する公式ドキュメントやチュートリアル (一般的なGoの知識として参照)
  • go/doc パッケージのドキュメント (一般的なGoの知識として参照)