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

[インデックス 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サーフェスに含めるようにしたことです。

  1. 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 関数が、このマップに含まれるパッケージの機能のみを出力するように変更されました。
  2. パッケージのロード状態管理と依存関係の解決:

    • WalkPackage(name string) 関数が大幅に修正されました。以前はパッケージのディレクトリパスも引数に取っていましたが、build.Tree を利用してパスを解決するように変更されました。
    • packageState を利用して、パッケージが loading 状態の場合は循環インポートを検出してエラーを報告し、loaded 状態の場合は処理をスキップします。
    • fileDeps(f *ast.File) 関数が新しく追加され、Goのソースファイル内のインポートパスを抽出します。
    • WalkPackage は、現在のパッケージの依存関係(インポートしている他のパッケージ)を再帰的に WalkPackage することで、必要なすべてのパッケージのAST情報をロードするように変更されました。
  3. インターフェース情報の収集と展開ロジック:

    • recordTypes(file *ast.File) 関数が新しく追加されました。この関数は、walkFile の前に実行され、ファイル内のすべてのエクスポートされたインターフェース型を w.interfaces マップに記録します。これは、埋め込みインターフェースのメソッドを展開する際に、埋め込まれるインターフェースの定義が既に利用可能であることを保証するために重要です。
    • noteInterface(name string, it *ast.InterfaceType) 関数が追加され、インターフェースのASTノードを w.interfaces に保存します。
    • interfaceMethods(pkg, iname string) 関数が新しく追加され、インターフェースのメソッドを再帰的に展開するコアロジックを担います。
      • この関数は、指定されたパッケージとインターフェース名に対応する *ast.InterfaceTypew.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機能として出力するように変更されました。
  4. テストの変更:

    • 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言語の公式ドキュメント (Interfaces, Embedding)
  • go/ast パッケージのドキュメント
  • go/build パッケージのドキュメント
  • Go言語のAPI互換性に関する一般的な情報