[インデックス 13866] ファイルの概要
このコミットは、Go言語のcmd/api
ツールにおけるインターフェースの扱いに関する修正です。具体的には、エクスポートされていない(unexported)メソッドを持つインターフェースのAPIレポート方法を改善し、Go 1互換性チェックの精度を高めています。
コミット
commit a29f3136b40c5a3b5da4034fe5def863d4ad2733
Author: Russ Cox <rsc@golang.org>
Date: Tue Sep 18 15:57:03 2012 -0400
cmd/api: allow extension of interfaces with unexported methods
Fixes #4061.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6525047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a29f3136b40c5a3b5da4034fe5def863d4ad2733
元コミット内容
cmd/api: allow extension of interfaces with unexported methods
このコミットは、エクスポートされていないメソッドを持つインターフェースの拡張を許可するようにcmd/api
ツールを変更します。
Fixes #4061.
Go issue #4061を修正します。
変更の背景
Go言語では、インターフェースはメソッドの集合を定義します。メソッドには、パッケージ外からアクセス可能なエクスポートされた(大文字で始まる)ものと、パッケージ内でのみアクセス可能なエクスポートされていない(小文字で始まる)ものがあります。
cmd/api
ツールは、Go言語の標準ライブラリのAPIサーフェスを追跡し、Go 1互換性を維持するために使用されます。このツールは、APIの変更を検出し、互換性のない変更が行われていないかを確認します。
以前のcmd/api
の実装では、インターフェースがエクスポートされていないメソッドを含んでいる場合、そのインターフェースのAPIレポートが不完全または誤解を招く可能性がありました。特に、エクスポートされていないメソッドを持つインターフェースは、そのメソッドが定義されているパッケージ内でのみ完全に実装され、拡張されることができます。パッケージ外からは、エクスポートされたメソッドのみが可視であるため、インターフェースの完全なメソッドセットを外部から把握することはできません。
このコミットは、Go issue #4061で報告された問題を解決することを目的としています。この問題は、エクスポートされていないメソッドを含むインターフェースが、cmd/api
によって適切に扱われないことに関連していると考えられます。具体的には、そのようなインターフェースがAPIサーフェスの一部として報告される際に、その特殊な性質(同じパッケージ内でのみ完全に拡張可能であること)が考慮されていなかった可能性があります。これにより、API互換性チェックが不正確になる恐れがありました。
前提知識の解説
- Go言語のインターフェース: Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goでは、型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを「暗黙的に」実装しているとみなされます。
interface{}
は、メソッドを一つも持たない空のインターフェースであり、すべての型がこれを実装します。 - エクスポートされたメソッドとエクスポートされていないメソッド: Goでは、識別子(変数名、関数名、メソッド名など)が大文字で始まる場合、それはエクスポートされ、パッケージ外からアクセス可能です。小文字で始まる場合、それはエクスポートされず、その識別子が定義されているパッケージ内でのみアクセス可能です。これはメソッドにも適用されます。
cmd/api
ツール: Go言語のソースコードを解析し、公開されているAPI(エクスポートされた型、関数、メソッドなど)のリストを生成するツールです。このリストは、Go 1の互換性保証の基盤となります。Go 1の互換性ポリシーでは、既存の公開APIを変更することは厳しく制限されており、cmd/api
はその変更を検出するために使用されます。- Go 1互換性: Go 1は、そのリリース以降、既存のプログラムが新しいバージョンのGoでも動作し続けることを保証する「Go 1互換性保証」を提供しています。これは、公開APIの変更を厳しく管理することで実現されています。
- インターフェースの拡張(埋め込み): Goのインターフェースは、他のインターフェースを埋め込むことで拡張できます。例えば、
interface { io.Reader; io.Writer }
のように記述することで、io.Reader
とio.Writer
のすべてのメソッドを含む新しいインターフェースを定義できます。
エクスポートされていないメソッドを持つインターフェースは、そのメソッドがパッケージ外から呼び出せないため、そのインターフェースを完全に実装できるのは、そのメソッドが定義されているパッケージ内の型に限られます。これは、APIの互換性を考える上で重要な側面です。
技術的詳細
このコミットの核心は、cmd/api
ツールがインターフェースのメソッドセットを解析し、そのインターフェースがエクスポートされていないメソッドを含んでいるかどうかを正確に識別する能力を向上させることにあります。
変更前は、cmd/api
はインターフェースのすべてのメソッド(エクスポートされたものとされていないもの両方)を列挙しようとしていた可能性があります。しかし、エクスポートされていないメソッドはパッケージの内部実装の詳細であり、外部APIサーフェスの一部とは見なされません。したがって、api/go1.txt
のようなAPIリストにそれらを詳細に含めることは、誤解を招くか、不必要な詳細を提供することになります。
このコミットでは、src/cmd/api/goapi.go
内のinterfaceMethods
関数が変更されています。この関数は、インターフェースが持つメソッドのリストを返すだけでなく、そのインターフェースがエクスポートされていないメソッドを含んでいるかどうかを示すブーリアン値complete
も返すようになりました。
complete
がtrue
の場合、そのインターフェースはエクスポートされたメソッドのみで構成されており、そのメソッドセットは外部から完全に把握できます。complete
がfalse
の場合、そのインターフェースはエクスポートされていないメソッドを含んでおり、その完全なメソッドセットは定義元のパッケージ内でのみ意味を持ちます。
walkInterfaceType
関数は、このcomplete
フラグを利用して、api/go1.txt
への出力形式を調整します。
complete
がtrue
であれば、これまで通りインターフェースの各エクスポートされたメソッドを個別に列挙します。complete
がfalse
であれば、個々のメソッドを列挙する代わりに、unexported methods
という一般的な記述を出力します。これにより、APIの利用者は、このインターフェースが内部的なメソッドを持っていることを認識しつつも、その詳細に依存する必要がないことが明確になります。
この変更により、cmd/api
はGo 1互換性保証の観点から、より正確なAPIサーフェスを報告できるようになります。エクスポートされていないメソッドは、パッケージの内部実装の一部であり、外部の利用者が依存すべきものではないため、APIリストからその詳細を省略することは適切です。
コアとなるコードの変更箇所
-
api/go1.txt
:pkg go/ast, type Decl interface { End, Pos }
のような行がpkg go/ast, type Decl interface, unexported methods
のように変更されています。これは、該当するインターフェースがエクスポートされていないメソッドを持つことを示す新しい表記です。- 同様に、
Expr
,Spec
,Stmt
,reflect.Type
,syscall.RoutingMessage
,syscall.Sockaddr
などのインターフェースの記述も変更されています。
-
src/cmd/api/goapi.go
:interfaceMethods
関数のシグネチャが変更され、complete bool
という戻り値が追加されました。// 変更前: func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { // 変更後: func (w *Walker) interfaceMethods(pkg, iname string) (methods []method, complete bool) {
interfaceMethods
関数内で、メソッドがエクスポートされていない場合(ast.IsExported(mname.Name)
がfalse
の場合)、complete
フラグがfalse
に設定されます。- 埋め込みインターフェース(
ast.Ident
やast.SelectorExpr
の場合)を処理する際にも、再帰的にinterfaceMethods
を呼び出し、その結果のcomplete
フラグを現在のcomplete
フラグと論理ANDで結合しています(complete = complete && c
)。これにより、埋め込まれたインターフェースにエクスポートされていないメソッドが含まれていれば、親インターフェースもcomplete: false
となります。 walkInterfaceType
関数内で、interfaceMethods
の戻り値であるcomplete
フラグがチェックされます。!complete
の場合、つまりインターフェースにエクスポートされていないメソッドが含まれる場合、w.emitFeature("unexported methods")
が呼び出され、個々のメソッド名ではなく、一般的な「unexported methods」という特徴がAPIリストに記録されます。!complete
の場合、メソッド名のソートとtype %s interface {}
の出力はスキップされます。
-
src/cmd/api/testdata/src/pkg/p1/golden.txt
:- テストデータである
golden.txt
も更新され、pkg p1, type I interface, unexported methods
やpkg p1, type Private interface, unexported methods
といった新しい表記が含まれています。
- テストデータである
-
src/cmd/api/testdata/src/pkg/p1/p1.go
:- 新しいテスト用のインターフェース
Public
とPrivate
が追加されています。type Public interface { X() Y() } type Private interface { X() y() // エクスポートされていないメソッド }
Private
インターフェースはエクスポートされていないメソッドy()
を持つため、この変更のテストケースとして機能します。
- 新しいテスト用のインターフェース
コアとなるコードの解説
src/cmd/api/goapi.go
のinterfaceMethods
関数とwalkInterfaceType
関数がこの変更の核心です。
interfaceMethods
関数
func (w *Walker) interfaceMethods(pkg, iname string) (methods []method, complete bool) {
// ... (インターフェースの検索と初期化) ...
complete = true // 初期値はtrue。エクスポートされていないメソッドが見つかればfalseにする。
for _, f := range t.Methods.List {
typ := f.Type
switch tv := typ.(type) {
case *ast.Field: // 通常のメソッド
if mname := tv.Names[0]; ast.IsExported(mname.Name) {
// エクスポートされたメソッドの場合、リストに追加
methods = append(methods, method{
name: mname.Name,
sig: w.funcSigString(ft),
})
} else {
// エクスポートされていないメソッドの場合、completeをfalseにする
complete = false
}
case *ast.Ident: // 埋め込みインターフェース (同じパッケージ内)
// ...
m, c := w.interfaceMethods(pkg, embedded) // 再帰呼び出し
methods = append(methods, m...)
complete = complete && c // 埋め込みインターフェースがcompleteでなければ、全体もcompleteでない
case *ast.SelectorExpr: // 埋め込みインターフェース (異なるパッケージ)
// ...
m, c := w.interfaceMethods(fpkg, rhs) // 再帰呼び出し
methods = append(methods, m...)
complete = complete && c // 埋め込みインターフェースがcompleteでなければ、全体もcompleteでない
// ...
}
}
return
}
この関数は、指定されたインターフェースのメソッドを走査し、エクスポートされたメソッドをmethods
スライスに追加します。同時に、エクスポートされていないメソッドが見つかった場合、または埋め込まれたインターフェースがエクスポートされていないメソッドを含む場合、complete
フラグをfalse
に設定します。これにより、インターフェースの「公開された」メソッドセットが完全に把握できるかどうかを正確に判断できます。
walkInterfaceType
関数
func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
methNames := []string{}
pop := w.pushScope("type " + name + " interface")
methods, complete := w.interfaceMethods(w.curPackageName, name) // completeフラグを取得
for _, m := range methods {
methNames = append(methNames, m.name)
w.emitFeature(fmt.Sprintf("%s%s", m.name, m.sig)) // エクスポートされたメソッドを出力
}
if !complete {
// メソッドセットにエクスポートされていないメソッドが含まれる場合
// すべての実装が同じパッケージによって提供されるため、メソッドセットは拡張可能。
// そのため、メソッドの完全なセットを記録する代わりに、
// エクスポートされていないメソッドがあったことだけを記録する。
// (インターフェースが縮小した場合、前回のループで出力されたメソッドシグネチャが
// 消えるため、それに気づくことができる。)
w.emitFeature("unexported methods")
}
pop()
if !complete {
return // completeでない場合、これ以降の処理(メソッド名のソートや空インターフェースの出力)はスキップ
}
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, ", ")))
}
}
この関数は、interfaceMethods
から返されたcomplete
フラグに基づいて、APIリストへの出力形式を決定します。complete
がfalse
の場合、個々のエクスポートされたメソッドのリストではなく、unexported methods
という一般的な特徴を記録します。これは、エクスポートされていないメソッドがAPI互換性保証の対象外であり、その詳細を公開する必要がないというGoの設計思想を反映しています。
関連リンク
- Go issue #4061: https://github.com/golang/go/issues/4061
- Go CL 6525047: https://golang.org/cl/6525047 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- Go言語の公式ドキュメント (特にパッケージ、エクスポート、インターフェースに関するセクション)
- Go 1 Compatibility Guarantee: https://go.dev/doc/go1compat
- Go ASTパッケージのドキュメント (Goのソースコードを解析するための抽象構文木に関する情報)
- Go言語のインターフェースに関する解説記事 (エクスポートされた/されていないメソッドの挙動を含む)
cmd/api
ツールの目的と機能に関する情報 (Goのソースコードリポジトリ内の関連ドキュメントやコメント)- Go issue #4061の議論内容 (問題の詳細と解決策の方向性)
- Goのソースコード (特に
src/cmd/api/
ディレクトリ内のファイル) - Goのテストコード (特に
src/cmd/api/testdata/
ディレクトリ内のファイル)