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

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

このコミットは、Go言語のAPIチェッカーツールである cmd/api を、Goの公式型チェッカーパッケージである go/types を使用するように大幅に書き換えるものです。これにより、ツールの正確性と堅牢性が向上し、Go言語の進化する型システムとの整合性が保たれます。

コミット

commit 9449c18ac8c130df000283d5d533f4cecf6b3afe
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Aug 8 14:10:59 2013 -0700

    cmd/api: rewrite using go/types
    
    - adjusted test files so that they actually type-check
    - adjusted go1.txt, go1.1.txt, next.txt
    - to run, provide build tag: api_tool
    
    Fixes #4538.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/12300043

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

https://github.com/golang/go/commit/9449c18ac8c130df000283d5d533f4cecf6b3afe

元コミット内容

cmd/api ツールを go/types パッケージを使って書き直した。

  • テストファイルが実際に型チェックされるように調整した。
  • go1.txt, go1.1.txt, next.txt を調整した。
  • 実行するには、ビルドタグ api_tool を指定する必要がある。 Issue #4538 を修正。

変更の背景

Go言語の cmd/api ツールは、Goの標準ライブラリの公開APIを抽出し、その変更を追跡するために使用されていました。このツールは、Go言語の互換性ポリシー(Go 1の互換性保証など)を維持するために不可欠です。しかし、このコミット以前の cmd/api ツールは、go/ast パッケージを用いてAST(抽象構文木)を直接走査し、独自の型推論ロジックを実装していました。

このアプローチにはいくつかの問題がありました。

  1. 正確性の限界: go/ast は構文情報を提供するだけであり、完全な型情報(例えば、インターフェースの実装関係、型の同一性、メソッドセットなど)は提供しません。そのため、cmd/api は独自のヒューリスティックやハックを用いて型情報を推測する必要があり、これが不正確さやメンテナンスの困難さにつながっていました。コミットメッセージにある "Note that this tool is only currently suitable for use on the Go standard library, not arbitrary packages. Once the Go AST has type information, this tool will be more reliable without hard-coded hacks throughout." というコメントが、この問題意識を明確に示しています。
  2. Go言語の進化への追従: Go言語の型システムは進化しており、新しい機能(例えば、ジェネリクスなど)が導入されるたびに、cmd/api のカスタムロジックも更新する必要がありました。これは開発コストを増大させ、バグの原因となる可能性がありました。
  3. 重複: Go 1.0のリリース後、Goチームは公式の型チェッカーパッケージ go/types の開発を進めていました。このパッケージは、Goのコンパイラが使用するのと同じ厳密な型チェックロジックを提供するため、cmd/api が独自に型推論を行う必要がなくなります。

このコミットは、これらの問題を解決するために、cmd/apigo/types パッケージに依存するように全面的に書き換えることを目的としています。これにより、ツールの正確性、堅牢性、および将来のGo言語の変更への適応性が大幅に向上します。

Fixes #4538 は、このコミットが解決する特定のIssueを示しています。Issue #4538は「cmd/api: use go/types」というタイトルで、まさにこの変更の必要性を提起していました。

前提知識の解説

Go言語のAST (Abstract Syntax Tree) と go/ast パッケージ

Go言語のソースコードは、コンパイルされる前にASTにパースされます。ASTは、コードの構造を木構造で表現したものです。go/ast パッケージは、GoのソースコードをパースしてASTを生成し、そのASTを走査するための機能を提供します。しかし、go/ast はあくまで構文レベルの情報を提供するものであり、変数や関数の型、インターフェースの実装関係といったセマンティックな型情報は直接は提供しません。

Go言語の型システムと go/types パッケージ

go/types パッケージは、Go言語の型システムをプログラム的に扱うための公式パッケージです。これはGoコンパイラの型チェッカーの基盤となっており、Goのソースコードを解析して、変数、関数、型、メソッドなどの完全な型情報を構築します。go/types は、名前解決、型推論、型チェックといった複雑な処理を正確に行うことができます。これにより、Goのコードベースを分析したり、リファクタリングツールやリンターのような静的解析ツールを構築したりする際に、非常に強力な基盤となります。

cmd/api ツール

cmd/api は、Goの標準ライブラリの公開APIを抽出するためのツールです。Go言語は後方互換性を非常に重視しており、Go 1の互換性保証では、既存のGo 1プログラムが新しいバージョンのGoでもコンパイルされ、実行されることが保証されています。cmd/api は、この互換性を維持するために、GoのAPIが意図せず変更されていないかをチェックする役割を担っています。具体的には、Goの各リリースで公開APIのリスト(go1.txt, go1.1.txt, next.txt など)を生成し、以前のバージョンとの差分を比較することで、互換性のない変更を検出します。

ビルドタグ

Goのビルドタグは、特定の条件に基づいてソースファイルをコンパイルに含めるか除外するかを制御するためのメカニズムです。ソースファイルの先頭に // +build tag_name の形式でコメントを記述することで、そのファイルが特定のビルドタグが有効な場合にのみコンパイルされるように指定できます。このコミットでは、cmd/api ツールを実行するために api_tool というビルドタグが必要とされています。これは、このツールが通常のGoプログラムとは異なる特殊なビルド要件を持つことを示しています。

技術的詳細

このコミットの主要な変更点は、src/cmd/api/goapi.go ファイルにおける Walker 構造体の再設計と、それに伴うAPI抽出ロジックの全面的な変更です。

旧実装の課題: 旧 cmd/api は、go/ast を使ってASTを走査し、独自のロジックで型情報を推測していました。例えば、clone.go はASTノードをディープコピーするためのカスタムロジックを含んでおり、これはASTが直接変更される可能性を考慮したものでしたが、go/types の導入により不要になりました。また、constValueTypevarValueType といった関数は、リテラルや式の型を推測するための複雑なロジックを持っており、特に定数間の型変換(ideal-intideal-float の間の変換など)を扱うために多くのハックが含まれていました。インターフェースのメソッドセットの解決も、interfaceMethods 関数で手動で行われていました。

新実装 (go/types の導入): 新しい Walker 構造体は、*go/types.Package*go/types.Config を利用します。

  1. パッケージのインポートと型チェック:
    • Walker.Import(name string) 関数が導入されました。この関数は、指定されたパッケージ名 (name) に対応する *types.Package オブジェクトを返します。
    • この関数内で、build.Context.ImportDir を使用してパッケージのソースファイルを特定し、parser.ParseFile でASTをパースします。
    • 最も重要なのは、types.Config.Check メソッドを使用して、パースされたファイル群に対して型チェックを実行している点です。これにより、Goコンパイラと同じ厳密な型情報が *types.Package オブジェクトに格納されます。
    • types.Config.Import フィールドにカスタムのインポート関数を設定することで、go/types が依存パッケージを解決する際に Walker.Import を再帰的に呼び出すようにしています。これにより、依存関係グラフ全体が正確に型チェックされます。
    • unsafe パッケージや cmd/ 以下のパッケージはAPI抽出の対象外とされています。
    • runtime パッケージの zgoos_*.gozgoarch_*.go のようなコンテキスト依存のファイルは、build.Context に基づいて手動で追加され、型チェックの対象に含まれます。
  2. APIの抽出:
    • Walker.export(pkg *types.Package) 関数が、パッケージの公開APIを抽出する主要なエントリポイントとなります。
    • pkg.Scope().Names() を使用して、パッケージスコープ内のすべての名前を取得し、ast.IsExported(name) で公開されている識別子のみをフィルタリングします。
    • scope.Lookup(name)types.Object を取得し、Walker.emitObj(obj types.Object) に渡します。
    • emitObj は、types.Const, types.Var, types.TypeName, types.Func の各オブジェクトタイプに応じて、適切な emit 関数(emitf, emitType, emitFunc など)を呼び出します。
  3. 型の文字列化:
    • Walker.writeType および Walker.typeString 関数は、go/typestypes.Type オブジェクトを、cmd/api の出力形式に合わせた文字列に変換します。
    • types.Basic 型(int, string など)や、types.Array, types.Slice, types.Struct, types.Pointer, types.Signature, types.Interface, types.Map, types.Chan, types.Named など、Goのあらゆる型を正確に表現できます。
    • 特に、ideal-int, ideal-char などの「理想型」の概念は、Goの定数における型なしの性質を表現するために維持されています。
    • インターフェースのメソッドは types.Interface.MethodSet() を使用して取得され、ソートされて出力されます。これにより、インターフェースのメソッドセットが正確に反映されます。
  4. 構造体とインターフェースの処理:
    • emitStructType は、構造体の公開フィールドを抽出します。埋め込みフィールドも適切に処理されます。
    • emitIfaceType は、インターフェースの公開メソッドを抽出し、unexported methods の存在も適切に記録します。これは、インターフェースのメソッドセットが非公開メソッドによって拡張される可能性があるため、互換性チェックにおいて重要な情報です。
  5. メソッドの処理:
    • emitType 内で、値レシーバとポインタレシーバのメソッドが types.Type.MethodSet() を使用して抽出され、emitMethod で出力されます。これにより、型の完全なメソッドセットが正確に反映されます。

ファイル変更の概要:

  • src/cmd/api/clone.go の削除: go/types が提供する型情報により、ASTのディープコピーが不要になったため。
  • src/cmd/api/goapi.go の大幅な書き換え: go/types パッケージの導入と、それに伴うAPI抽出ロジックの全面的な変更。
  • api/go1.txt, api/go1.1.txt, api/next.txt の変更: cmd/api ツールの出力形式や、Go APIの変更を反映。特に、crypto/ecdsa パッケージに多くのメソッドが追加されているのが目立ちます。これは、go/types がより正確なメソッドセットを検出できるようになった結果である可能性があります。また、syscall パッケージのWindows関連の構造体定義で、配列のサイズが具体的な数値に置き換えられている箇所が多数見られます。これは、go/types がより具体的な型情報を提供できるようになったことによる調整かもしれません。
  • src/cmd/api/goapi_test.go および src/cmd/api/testdata/src/pkg/p1/ 以下のテストファイルの調整: 新しい型チェックロジックに合わせてテストが更新されています。

この変更により、cmd/api はGo言語の型システムをより深く理解し、より正確なAPI情報を抽出できるようになりました。これは、Go言語の長期的な互換性維持戦略において重要なマイルストーンとなります。

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

このコミットのコアとなる変更は、src/cmd/api/goapi.go ファイルに集中しています。

  1. import 文の変更:

    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -1,39 +1,33 @@
    +// +build api_tool
    +
     // Copyright 2011 The Go Authors.  All rights reserved.
     // Use of this source code is governed by a BSD-style
     // license that can be found in the LICENSE file.
     
     -// Api computes the exported API of a set of Go packages.
     -//
     -// BUG(bradfitz): Note that this tool is only currently suitable
     -// for use on the Go standard library, not arbitrary packages.\n-// Once the Go AST has type information, this tool will be more
     -// reliable without hard-coded hacks throughout.\n+// Binary api computes the exported API of a set of Go packages.
      package main
     
      import (
      	"bufio"
      	"bytes"
     -"errors"
      	"flag"
      	"fmt"
      	"go/ast"
      	"go/build"
     -"go/doc"
      	"go/parser"
     -"go/printer"
      	"go/token"
      	"io"
      	"io/ioutil"
      	"log"
      	"os"
      	"os/exec"
     -"path"
      	"path/filepath"
      	"regexp"
      	"runtime"
      	"sort"
     -"strconv"
      	"strings"
     +
     +	"code.google.com/p/go.tools/go/types"
     )
    

    go/types パッケージが追加され、errors, go/doc, go/printer, path, strconv など、以前のカスタムロジックで必要だったパッケージが削除されています。

  2. Walker 構造体の再定義:

    --- a/src/cmd/api/goapi.go
    +++ b/src/cmd/api/goapi.go
    @@ -304,26 +310,18 @@ func fileFeatures(filename string) []string {
     var fset = token.NewFileSet()
     
     type Walker struct {
    -	context         *build.Context
    -	root            string
    -	scope           []string
    -	features        map[string]bool // set
    -	lastConstType   string
    -	curPackageName  string
    -	curPackage      *ast.Package
    -	prevConstType   map[pkgSymbol]string
    -	constDep        map[string]string // key's const identifier has type of future value const identifier
    -	packageState    map[string]loadState
    -	interfaces      map[pkgSymbol]*ast.InterfaceType
    -	functionTypes   map[pkgSymbol]string // symbol => return type
    -	selectorFullPkg map[string]string    // "http" => "net/http", updated by imports
    -	wantedPkg       map[string]bool      // packages requested on the command line
    +	context  *build.Context
    +	root     string
    +	scope    []string
    +	current  *types.Package
    +	features map[string]bool           // set
    +	imported map[string]*types.Package // packages already imported
     }
     
    -func NewWalker() *Walker {
    +func NewWalker(context *build.Context, root string) *Walker {
     	return &Walker{
    -		features:        make(map[string]bool),\n-		packageState:    make(map[string]loadState),\n-		interfaces:      make(map[pkgSymbol]*ast.InterfaceType),\n-		functionTypes:   make(map[pkgSymbol]string),\n-		selectorFullPkg: make(map[string]string),\n-		wantedPkg:       make(map[string]bool),\n-		prevConstType:   make(map[pkgSymbol]string),\n-		root:            filepath.Join(build.Default.GOROOT, "src/pkg"),\n+		context:  context,\n+		root:     root,\n+		features: map[string]bool{},\n+		imported: map[string]*types.Package{"unsafe": types.Unsafe},\n     	}
     }
    

    Walker 構造体から、以前のカスタム型推論や状態管理のためのフィールド(lastConstType, curPackageName, prevConstType, constDep, packageState, interfaces, functionTypes, selectorFullPkg, wantedPkg)が削除され、代わりに current *types.Packageimported map[string]*types.Package が追加されています。これは、型情報の管理を go/types に完全に委ねることを意味します。

  3. Import メソッドの追加:

    func (w *Walker) Import(name string) (pkg *types.Package) {
        // ... (パッケージのファイル特定、ASTパース) ...
    
        // Type-check package files.
        conf := types.Config{
            IgnoreFuncBodies: true,
            FakeImportC:      true,
            Import: func(imports map[string]*types.Package, name string) (*types.Package, error) {
                pkg := w.Import(name)
                imports[name] = pkg
                return pkg, nil
            },
        }
        pkg, err = conf.Check(name, fset, files, nil)
        if err != nil {
            // ... (エラーハンドリング) ...
        }
    
        w.imported[name] = pkg
        return
    }
    

    このメソッドは、指定されたパッケージを型チェックし、その *types.Package オブジェクトを返します。types.Config.Check の呼び出しが、型チェックの核心部分です。

  4. export メソッドの追加:

    func (w *Walker) export(pkg *types.Package) {
        // ...
        scope := pkg.Scope()
        for _, name := range scope.Names() {
            if ast.IsExported(name) {
                w.emitObj(scope.Lookup(name))
            }
        }
        // ...
    }
    

    export メソッドは、パッケージの公開されたオブジェクトを走査し、emitObj を呼び出してAPI情報を出力します。

  5. emitObj, writeType, writeSignature などのヘルパー関数の追加: これらの関数は、go/types のオブジェクトからAPIの文字列表現を生成するために導入されました。

    • emitObj: types.Object の種類に応じて適切な emit 関数をディスパッチします。
    • writeType: types.Type を文字列に変換します。Goのあらゆる型(基本型、配列、スライス、構造体、ポインタ、関数シグネチャ、インターフェース、マップ、チャネル、名前付き型)に対応しています。
    • writeSignature: 関数シグネチャを文字列に変換します。
    • emitType, emitStructType, emitIfaceType, emitMethod: それぞれ型、構造体、インターフェース、メソッドのAPI情報を出力します。
  6. src/cmd/api/clone.go の削除: このファイルは、ASTノードをディープコピーするためのカスタムロジックを含んでいましたが、go/types の導入により不要になったため削除されました。

コアとなるコードの解説

Walker 構造体と NewWalker 関数

type Walker struct {
	context  *build.Context
	root     string
	scope    []string
	current  *types.Package
	features map[string]bool           // set
	imported map[string]*types.Package // packages already imported
}

func NewWalker(context *build.Context, root string) *Walker {
	return &Walker{
		context:  context,
		root:     root,
		features: map[string]bool{},
		imported: map[string]*types.Package{"unsafe": types.Unsafe},
	}
}

Walker 構造体は、API抽出プロセスの状態を保持します。

  • context: ビルドコンテキスト(OS、アーキテクチャなど)を保持し、これに基づいてファイルパスを解決したり、コンテキスト依存のファイルを生成したりします。
  • root: Goのソースコードのルートディレクトリ(通常は $GOROOT/src/pkg)です。
  • scope: 現在処理中のAPI要素の階層を示す文字列スライスです(例: ["pkg net/http", "type Client"])。
  • current: 現在型チェック中の *go/types.Package オブジェクトです。
  • features: 抽出されたAPI要素("features")を格納するセットです。重複を避けるために map[string]bool が使われています。
  • imported: 既にインポートされ、型チェックが完了したパッケージをキャッシュするためのマップです。"unsafe" パッケージは特別に types.Unsafe として初期化されています。

NewWalkerWalker のコンストラクタで、必要な初期化を行います。

Walker.Import(name string) (pkg *types.Package)

func (w *Walker) Import(name string) (pkg *types.Package) {
	pkg = w.imported[name]
	if pkg != nil {
		if pkg == &importing {
			log.Fatalf("cycle importing package %q", name)
		}
		return pkg
	}
	w.imported[name] = &importing // Sentinel to detect import cycles

	dir := filepath.Join(w.root, filepath.FromSlash(name))
	// ... (ディレクトリの存在チェック、Goファイルの特定) ...

	// Certain files only exist when building for the specified context.
	// Add them manually.
	if name == "runtime" {
		// ... (zgoos_*.go, zgoarch_*.go の追加) ...
	}

	// Parse package files.
	var files []*ast.File
	for _, file := range filenames {
		f, err := w.parseFile(dir, file)
		if err != nil {
			log.Fatalf("error parsing package %s: %s", name, err)
		}
		files = append(files, f)
	}

	// Type-check package files.
	conf := types.Config{
		IgnoreFuncBodies: true, // API抽出なので関数本体は不要
		FakeImportC:      true, // Cgoのインポートを偽装
		Import: func(imports map[string]*types.Package, name string) (*types.Package, error) {
			// go/types が依存パッケージを解決する際に、このWalkerのImportメソッドを再帰的に呼び出す
			pkg := w.Import(name)
			imports[name] = pkg
			return pkg, nil
		},
	}
	pkg, err = conf.Check(name, fset, files, nil)
	if err != nil {
		// ... (エラーハンドリング) ...
	}

	w.imported[name] = pkg // 型チェックが完了したパッケージをキャッシュ
	return
}

このメソッドは、Goのパッケージをインポートし、go/types を使ってそのパッケージの型情報を構築します。

  • キャッシュと循環検出: w.imported マップを使って、既にインポート中のパッケージやインポート済みのパッケージを管理し、インポートの循環を検出します。
  • ファイル特定とパース: build.Context.ImportDir を使ってパッケージのソースファイルを特定し、parser.ParseFile でASTをパースします。runtime パッケージのような特殊なケースでは、ビルドコンテキストに応じたファイル(zgoos_*.go, zgoarch_*.go)を動的に追加します。
  • 型チェック: types.Config を設定し、conf.Check を呼び出すことで、Goコンパイラと同じ厳密な型チェックを実行します。IgnoreFuncBodies: true は、API抽出には関数本体の解析が不要であることを示します。FakeImportC: true は、Cgoのインポートを適切に処理するための設定です。
  • 再帰的なインポート: types.Config.Import フィールドに自身の Walker.Import メソッドを渡すことで、go/types が依存パッケージを解決する際に、この Walker が管理するインポートロジックを使用するようにします。これにより、パッケージの依存関係グラフ全体が正確に型チェックされます。

Walker.export(pkg *types.Package)

func (w *Walker) export(pkg *types.Package) {
	if *verbose {
		log.Println(pkg)
	}
	pop := w.pushScope("pkg " + pkg.Path()) // スコープをプッシュ
	w.current = pkg // 現在のパッケージを設定
	scope := pkg.Scope() // パッケージのスコープを取得
	for _, name := range scope.Names() {
		if ast.IsExported(name) { // 公開されている識別子のみを対象
			w.emitObj(scope.Lookup(name)) // オブジェクトを抽出し、API情報を出力
		}
	}
	pop() // スコープをポップ
}

このメソッドは、与えられた *types.Package から公開APIを抽出します。

  • パッケージのスコープ (pkg.Scope()) からすべての名前を取得し、ast.IsExported を使って公開されている識別子のみをフィルタリングします。
  • 各公開識別子について、scope.Lookup(name) で対応する types.Object を取得し、emitObj メソッドに渡してAPI情報を出力させます。

Walker.emitObj(obj types.Object)

func (w *Walker) emitObj(obj types.Object) {
	switch obj := obj.(type) {
	case *types.Const:
		w.emitf("const %s %s", obj.Name(), w.typeString(obj.Type()))
	case *types.Var:
		w.emitf("var %s %s", obj.Name(), w.typeString(obj.Type()))
	case *types.TypeName:
		w.emitType(obj)
	case *types.Func:
		w.emitFunc(obj)
	default:
		panic("unknown object: " + obj.String())
	}
}

emitObj は、go/types が提供する types.Object の種類(定数、変数、型名、関数)に応じて、適切な emit ヘルパー関数を呼び出します。これにより、各種類のAPI要素が正しい形式で出力されます。

Walker.writeType(buf *bytes.Buffer, typ types.Type)

func (w *Walker) writeType(buf *bytes.Buffer, typ types.Type) {
	switch typ := typ.(type) {
	case *types.Basic:
		// ... (基本型の処理、ideal-int, ideal-char など) ...
	case *types.Array:
		// ... (配列型の処理) ...
	case *types.Slice:
		// ... (スライス型の処理) ...
	case *types.Struct:
		buf.WriteString("struct") // 構造体は "struct" とだけ出力
	case *types.Pointer:
		// ... (ポインタ型の処理) ...
	case *types.Signature:
		// ... (関数シグネチャの処理) ...
	case *types.Interface:
		// ... (インターフェース型の処理、メソッド名をソートして出力) ...
	case *types.Map:
		// ... (マップ型の処理) ...
	case *types.Chan:
		// ... (チャネル型の処理) ...
	case *types.Named:
		// ... (名前付き型の処理、パッケージ名も考慮) ...
	default:
		panic(fmt.Sprintf("unknown type %T", typ))
	}
}

writeType は、go/typestypes.Type オブジェクトを、cmd/api の出力形式に合わせた文字列に変換する中心的な関数です。Goの複雑な型システムを正確に表現するために、様々な型アサーションと再帰的な呼び出しが行われます。特に、インターフェースのメソッドは sortedMethodNames を使ってソートされ、一貫した出力が得られるようにしています。

これらの変更により、cmd/api はGo言語の型システムをより深く、より正確に理解できるようになり、APIの抽出と互換性チェックの信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク