[インデックス 11461] ファイルの概要
このコミットは、Go言語のAPIサーフェスを抽出・比較するためのツールである cmd/goapi
の機能を拡張するものです。具体的には、Goのインターフェースに埋め込まれた他のインターフェース(embedded interfaces)のメソッドを正しく展開し、API定義に含めるように改善しています。これにより、cmd/goapi
が生成するAPIレポートが、Goのインターフェースのセマンティクスをより正確に反映するようになります。
コミット
commit a94bd4d7c324648f1736e8f7fb1a0fd4b13bacc6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun Jan 29 21:04:13 2012 -0800
cmd/goapi: expand embedded interfaces
Fixes #2801
R=rsc
CC=golang-dev
https://golang.org/cl/5576068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a94bd4d7c324648f1736e8f7fb1a0fd4b13bacc6
元コミット内容
cmd/goapi: expand embedded interfaces
このコミットは、Go言語のAPI定義を抽出する cmd/goapi
ツールにおいて、インターフェースに埋め込まれた他のインターフェースのメソッドを適切に展開する機能を追加します。これにより、APIの完全なシグネチャが正確に表現されるようになります。Issue #2801 を修正します。
変更の背景
Go言語のインターフェースは、他のインターフェースを埋め込むことができます。例えば、io.Reader
インターフェースを埋め込んだ io.ReadCloser
インターフェースは、Read
メソッドと Close
メソッドの両方を持つことになります。しかし、cmd/goapi
ツールは、これまでこの「埋め込み」のセマンティクスを完全に理解し、埋め込まれたインターフェースのメソッドを明示的に展開してAPIサーフェスに含めることができていませんでした。
この問題は、GoのAPI互換性をチェックする際に重要となります。API互換性ツールは、あるバージョンのライブラリが提供するAPIが、以前のバージョンと互換性があるかどうかを判断する必要があります。インターフェースの埋め込みが正しく展開されないと、APIの変更が誤って検出されたり、逆に検出されなかったりする可能性があります。
Issue #2801 はこの問題を具体的に指摘しており、このコミットはその修正を目的としています。埋め込みインターフェースのメソッドを明示的に展開することで、cmd/goapi
が生成するAPI定義がより正確になり、API互換性チェックの信頼性が向上します。
前提知識の解説
Go言語のインターフェースと埋め込み
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。
Goのインターフェースは、他のインターフェースを「埋め込む」ことができます。これは、埋め込まれたインターフェースのすべてのメソッドが、埋め込み元のインターフェースのメソッドセットに含まれることを意味します。例えば:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader // Readerインターフェースを埋め込む
Closer // Closerインターフェースを埋め込む
}
この ReadCloser
インターフェースは、Read
メソッドと Close
メソッドの両方を持つことになります。
go/ast
パッケージ
go/ast
パッケージは、Goのソースコードの抽象構文木(AST: Abstract Syntax Tree)を表現するためのデータ構造を提供します。Goのコンパイラやツールは、ソースコードを解析してASTを構築し、そのASTを操作することで様々な処理を行います。
このコミットでは、ast.InterfaceType
(インターフェース型を表すASTノード)、ast.FuncType
(関数型を表すASTノード)、ast.Ident
(識別子を表すASTノード)、ast.SelectorExpr
(セレクタ式、例: pkg.Name
を表すASTノード)などが利用されています。
cmd/goapi
ツールの目的
cmd/goapi
は、Goの標準ライブラリやその他のGoパッケージの公開APIサーフェスを抽出するために設計されたツールです。このツールは、パッケージ内のエクスポートされた型、関数、変数、定数、およびインターフェースのメソッドなどを列挙し、そのAPI定義を特定の形式で出力します。この出力は、GoのAPI互換性チェックやドキュメント生成などに利用されます。
APIサーフェスと互換性
APIサーフェスとは、ライブラリやモジュールが外部に公開している機能の集合体です。これには、公開された関数、型、メソッド、定数などが含まれます。API互換性とは、新しいバージョンのライブラリが、古いバージョンを使用していた既存のコードを壊すことなく動作するかどうかを指します。
Goでは、インターフェースのメソッドセットが変更されると、そのインターフェースを実装していた型や、そのインターフェースを使用していたコードに影響が出る可能性があります。埋め込みインターフェースのメソッドがAPIサーフェスに正しく反映されないと、APIの変更が正確に追跡できず、互換性問題を見落とす原因となります。
技術的詳細
このコミットの主要な変更点は、cmd/goapi
がインターフェースの埋め込みを再帰的に解決し、その結果として得られるすべてのメソッドをAPIサーフェスに含めるようにしたことです。
-
Walker
構造体の拡張:packageState map[string]loadState
: パッケージのロード状態(未ロード、ロード中、ロード済み)を管理するためのマップが追加されました。これにより、循環インポートによる無限ループを防ぎます。interfaces map[pkgSymbol]*ast.InterfaceType
: パッケージ名とインターフェース名をキーとして、*ast.InterfaceType
(ASTのインターフェースノード)を保存するマップが追加されました。これにより、後で埋め込みインターフェースのメソッドを展開する際に、そのインターフェースの定義を素早く参照できます。selectorFullPkg map[string]string
: インポートエイリアス(例:ptwo "p2"
のptwo
)から完全なパッケージパス(例:p2
)へのマッピングを保存します。これにより、異なるパッケージからの埋め込みインターフェースを解決できます。wantedPkg map[string]bool
: コマンドラインで指定された、API情報を抽出したいパッケージを追跡します。emitFeature
関数が、このマップに含まれるパッケージの機能のみを出力するように変更されました。
-
パッケージのロード状態管理と依存関係の解決:
WalkPackage(name string)
関数が大幅に修正されました。以前はパッケージのディレクトリパスも引数に取っていましたが、build.Tree
を利用してパスを解決するように変更されました。packageState
を利用して、パッケージがloading
状態の場合は循環インポートを検出してエラーを報告し、loaded
状態の場合は処理をスキップします。fileDeps(f *ast.File)
関数が新しく追加され、Goのソースファイル内のインポートパスを抽出します。WalkPackage
は、現在のパッケージの依存関係(インポートしている他のパッケージ)を再帰的にWalkPackage
することで、必要なすべてのパッケージのAST情報をロードするように変更されました。
-
インターフェース情報の収集と展開ロジック:
recordTypes(file *ast.File)
関数が新しく追加されました。この関数は、walkFile
の前に実行され、ファイル内のすべてのエクスポートされたインターフェース型をw.interfaces
マップに記録します。これは、埋め込みインターフェースのメソッドを展開する際に、埋め込まれるインターフェースの定義が既に利用可能であることを保証するために重要です。noteInterface(name string, it *ast.InterfaceType)
関数が追加され、インターフェースのASTノードをw.interfaces
に保存します。interfaceMethods(pkg, iname string)
関数が新しく追加され、インターフェースのメソッドを再帰的に展開するコアロジックを担います。- この関数は、指定されたパッケージとインターフェース名に対応する
*ast.InterfaceType
をw.interfaces
から取得します。 - インターフェースのフィールド(メソッドまたは埋め込みインターフェース)をイテレートします。
*ast.FuncType
の場合は、そのメソッド名とシグネチャを直接追加します。*ast.Ident
の場合は、同じパッケージ内の埋め込みインターフェース(例:Namer
)を表します。この場合、interfaceMethods
を再帰的に呼び出して、埋め込まれたインターフェースのメソッドを展開します。特別なケースとして、組み込みのerror
インターフェース(error
型)も処理し、Error() string
メソッドを追加します。*ast.SelectorExpr
の場合は、異なるパッケージからの埋め込みインターフェース(例:ptwo.Twoer
)を表します。w.selectorFullPkg
を使用して完全なパッケージパスを解決し、interfaceMethods
を再帰的に呼び出してメソッドを展開します。
- この関数は、指定されたパッケージとインターフェース名に対応する
walkInterfaceType(name string, t *ast.InterfaceType)
関数が修正され、interfaceMethods
を呼び出して展開されたメソッドのリストを取得し、それらをソートしてAPI機能として出力するように変更されました。
-
テストの変更:
testdata
ディレクトリの構造が変更され、src/pkg
サブディレクトリが導入されました。これは、Goのパッケージ構造をより正確にシミュレートするためです。- 新しいテストパッケージ
p2
が追加され、異なるパッケージからの埋め込みインターフェースのテストケースが追加されました。 goapi_test.go
がこれらの変更に合わせて更新され、build.Tree
の設定やWalkPackage
の呼び出しが修正されました。
これらの変更により、cmd/goapi
はGoのインターフェースの埋め込みを完全に理解し、APIサーフェスをより正確に表現できるようになりました。
コアとなるコードの変更箇所
主要な変更は src/cmd/goapi/goapi.go
に集中しています。特に以下の関数と構造体がコアとなる変更です。
type Walker struct
の定義func NewWalker() *Walker
func (w *Walker) WalkPackage(name string)
func fileDeps(f *ast.File) (pkgs []string)
func (w *Walker) recordTypes(file *ast.File)
func (w *Walker) noteInterface(name string, it *ast.InterfaceType)
type method struct
func (w *Walker) interfaceMethods(pkg, iname string) (methods []method)
func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType)
func (w *Walker) emitFeature(feature string)
コアとなるコードの解説
type Walker struct
の変更
type Walker struct {
// ... 既存フィールド ...
tree *build.Tree
packageState map[string]loadState
interfaces map[pkgSymbol]*ast.InterfaceType
selectorFullPkg map[string]string // "http" => "net/http", updated by imports
wantedPkg map[string]bool // packages requested on the command line
}
Walker
はASTを走査しAPI情報を抽出する主要な構造体です。上記のフィールドが追加され、パッケージのロード状態、インターフェース定義、インポートエイリアスの解決、および対象パッケージのフィルタリングを管理できるようになりました。
func (w *Walker) WalkPackage(name string)
func (w *Walker) WalkPackage(name string) {
switch w.packageState[name] {
case loading:
log.Fatalf("import cycle loading package %q?", name)
case loaded:
return
}
w.packageState[name] = loading
defer func() {
w.packageState[name] = loaded
}()
dir := filepath.Join(w.tree.SrcDir(), filepath.FromSlash(name))
// ... パッケージ内のファイルを解析 ...
for _, afile := range apkg.Files {
for _, dep := range fileDeps(afile) {
w.WalkPackage(dep) // 依存パッケージを再帰的にロード
}
}
// ... パッケージ内の型を記録 (recordTypes) ...
// ... パッケージ内の要素を走査 (walkFile) ...
}
この関数は、指定されたパッケージを走査し、そのAPI情報を抽出します。
packageState
を利用して、パッケージのロード状態を管理し、循環インポートを検出します。- パッケージ内の各ソースファイルを解析し、
fileDeps
を使ってそのファイルがインポートしている他のパッケージを特定します。 - 特定された依存パッケージに対して
WalkPackage
を再帰的に呼び出し、必要なすべてのパッケージのAST情報をロードします。これにより、埋め込みインターフェースが異なるパッケージに定義されていても解決できるようになります。 - まず
recordTypes
を呼び出してインターフェース定義を収集し、その後walkFile
を呼び出してAPI要素を走査します。
func (w *Walker) recordTypes(file *ast.File)
func (w *Walker) recordTypes(file *ast.File) {
for _, di := range file.Decls {
switch d := di.(type) {
case *ast.GenDecl:
switch d.Tok {
case token.TYPE:
for _, sp := range d.Specs {
ts := sp.(*ast.TypeSpec)
name := ts.Name.Name
if ast.IsExported(name) {
if it, ok := ts.Type.(*ast.InterfaceType); ok {
w.noteInterface(name, it) // エクスポートされたインターフェースを記録
}
}
}
}
}
}
}
この関数は、Goソースファイル内のすべてのエクスポートされたインターフェース型を事前にスキャンし、w.interfaces
マップにそのASTノードを保存します。これにより、後で埋め込みインターフェースのメソッドを展開する際に、埋め込まれるインターフェースの定義を素早く参照できるようになります。
func (w *Walker) noteInterface(name string, it *ast.InterfaceType)
func (w *Walker) noteInterface(name string, it *ast.InterfaceType) {
w.interfaces[pkgSymbol{w.curPackageName, name}] = it
}
recordTypes
から呼び出され、現在のパッケージ名とインターフェース名をキーとして、インターフェースのASTノードを w.interfaces
マップに格納します。
func (w *Walker) interfaceMethods(pkg, iname string) (methods []method)
type method struct {
name string // "Read"
sig string // "([]byte) (int, error)", from funcSigString
}
func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) {
t, ok := w.interfaces[pkgSymbol{pkg, iname}]
if !ok {
log.Fatalf("failed to find interface %s.%s", pkg, iname)
}
for _, f := range t.Methods.List {
typ := f.Type
switch tv := typ.(type) {
case *ast.FuncType: // 通常のメソッド
// ... メソッド名とシグネチャを追加 ...
case *ast.Ident: // 同じパッケージ内の埋め込みインターフェース (例: Namer)
embedded := typ.(*ast.Ident).Name
if embedded == "error" { // 特別なケース: error インターフェース
// ... Error() string メソッドを追加 ...
continue
}
// ... 再帰的に interfaceMethods を呼び出し、メソッドを展開 ...
methods = append(methods, w.interfaceMethods(pkg, embedded)...)
case *ast.SelectorExpr: // 異なるパッケージからの埋め込みインターフェース (例: ptwo.Twoer)
lhs := w.nodeString(tv.X) // パッケージエイリアス (例: ptwo)
rhs := w.nodeString(tv.Sel) // インターフェース名 (例: Twoer)
fpkg, ok := w.selectorFullPkg[lhs] // フルパッケージパスを解決
if !ok {
log.Fatalf("can't resolve selector %q in interface %s.%s", lhs, pkg, iname)
}
// ... 再帰的に interfaceMethods を呼び出し、メソッドを展開 ...
methods = append(methods, w.interfaceMethods(fpkg, rhs)...)
// ... default: 未知の型の場合のエラーハンドリング ...
}
}
return
}
この関数は、埋め込みインターフェースのメソッドを再帰的に展開する核心部分です。
- 指定されたインターフェースのASTノードを取得します。
- インターフェースのメソッドリストを走査します。
- 通常のメソッド (
*ast.FuncType
) はそのまま追加します。 - 埋め込みインターフェース (
*ast.Ident
または*ast.SelectorExpr
) を検出した場合、その埋め込みインターフェースのメソッドを再帰的にinterfaceMethods
を呼び出して取得し、現在のインターフェースのメソッドリストに追加します。 error
インターフェースは特別に処理されます。selectorFullPkg
を使用して、異なるパッケージからのインポートエイリアスを解決します。
func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType)
func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
methNames := []string{}
pop := w.pushScope("type " + name + " interface")
for _, m := range w.interfaceMethods(w.curPackageName, name) { // interfaceMethods を呼び出して展開されたメソッドを取得
methNames = append(methNames, m.name)
w.emitFeature(fmt.Sprintf("%s%s", m.name, m.sig)) // 各メソッドをAPI機能として出力
}
pop()
sort.Strings(methNames)
if len(methNames) == 0 {
w.emitFeature(fmt.Sprintf("type %s interface {}", name))
} else {
w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methNames, ", ")))
}
}
この関数は、インターフェース型を走査し、そのAPI情報を出力します。
interfaceMethods
を呼び出して、埋め込みインターフェースを含むすべてのメソッドのリストを取得します。- 取得した各メソッドについて、その名前とシグネチャを
emitFeature
を使ってAPI機能として出力します。 - 最後に、インターフェース全体の定義(例:
type I interface { Get, Set }
)を出力します。
func (w *Walker) emitFeature(feature string)
func (w *Walker) emitFeature(feature string) {
if !w.wantedPkg[w.curPackageName] { // wantedPkg に含まれるパッケージのみ出力
return
}
f := strings.Join(w.scope, ", ") + ", " + feature
if _, dup := w.features[f]; dup {
panic("duplicate feature inserted: " + f)
}
}
この関数は、抽出されたAPI機能を内部の features
マップに追加します。変更点として、w.wantedPkg
マップをチェックし、コマンドラインで指定されたパッケージの機能のみを記録するようにフィルタリングが追加されました。これにより、不要なAPI情報の抽出を防ぎます。
これらの変更により、cmd/goapi
はGoのインターフェースの埋め込みを正確に処理し、APIサーフェスをより網羅的かつ正確に表現できるようになりました。
関連リンク
- Go Issue 2801: https://github.com/golang/go/issues/2801
- Gerrit Change-ID: https://golang.org/cl/5576068
参考にした情報源リンク
- Go言語の公式ドキュメント (Interfaces, Embedding)
go/ast
パッケージのドキュメントgo/build
パッケージのドキュメント- Go言語のAPI互換性に関する一般的な情報