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

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

このコミットは、Go言語の公式リポジトリに cmd/goapi という新しいツールを導入するものです。このツールは、Goパッケージのエクスポートされた(公開された)APIを時系列で追跡し、将来のリリースにおけるAPIの互換性を保証することを目的としています。

コミット

commit 5c04272ff33d90f2417c1db40be8675dd74fdad9
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 25 17:47:57 2012 -0800

    cmd/goapi: new tool for tracking exported API over time

    The idea is that we add files to the api/ directory which
    are sets of promises for the future. Each line in a file
    is a stand-alone feature description.

    When we do a release, we make sure we haven't broken or changed
    any lines from the past (only added them).

    We never change old files, only adding new ones. (go-1.1.txt,
    etc)

    R=dsymonds, adg, r, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/5570051

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

https://github.com/golang/go/commit/5c04272ff33d90f2417c1db40be8675dd74fdad9

元コミット内容

cmd/goapi: エクスポートされたAPIを時系列で追跡するための新しいツール。

このツールの目的は、api/ ディレクトリに将来のAPIの約束(promises)を記述したファイルを追加することです。各ファイル内の各行は、独立した機能記述を表します。

リリースを行う際には、過去のAPI記述が壊れたり変更されたりしていないことを確認します(追加のみが許可されます)。

古いファイルは決して変更せず、常に新しいファイル(例: go-1.1.txt など)を追加していきます。

変更の背景

Go言語は、その初期段階から後方互換性を非常に重視してきました。特に、標準ライブラリのAPIの安定性は、Goエコシステム全体の健全性を保つ上で極めて重要です。このコミットが行われた2012年1月は、Go 1のリリースが近づいていた時期であり、APIの安定化と将来にわたる互換性の保証が喫緊の課題でした。

cmd/goapiツールの導入は、以下の課題を解決するために考案されました。

  1. APIの意図しない変更の防止: 開発者がコードを変更する際に、意図せず公開APIを変更したり削除したりするリスクを低減する。
  2. リリースプロセスの支援: リリース時に、APIの互換性が維持されていることを自動的に検証するメカニズムを提供する。
  3. API変更の明確な記録: どのAPIがどのバージョンで追加されたかを明確に記録し、開発者やユーザーがAPIの進化を追跡できるようにする。
  4. 後方互換性の保証: Go 1のリリース以降、標準ライブラリのAPIは「Go 1互換性保証」として知られる厳格な後方互換性ポリシーの対象となります。このツールは、そのポリシーを技術的に強制し、違反を検出するための基盤となります。

このツールは、Go言語が長期的な安定性と信頼性を提供するための重要なインフラの一部として位置づけられています。

前提知識の解説

このコミットの理解には、以下のGo言語およびソフトウェア開発の概念に関する知識が役立ちます。

  • Go言語のパッケージとエクスポートされた識別子: Go言語では、パッケージ内の識別子(変数、定数、関数、型、メソッドなど)が、その名前の最初の文字が大文字である場合に「エクスポートされる」(公開される)と見なされます。エクスポートされた識別子のみが、そのパッケージをインポートする他のパッケージからアクセス可能です。cmd/goapiは、このエクスポートされたAPIを対象とします。
  • GoのAST (Abstract Syntax Tree): Goコンパイラは、ソースコードを解析して抽象構文木(AST)を生成します。ASTは、プログラムの構造を木構造で表現したものです。cmd/goapiは、go/astgo/parsergo/tokenといった標準ライブラリのパッケージを使用して、GoソースコードのASTを解析し、エクスポートされたAPIの情報を抽出します。
  • go/docパッケージ: Goの標準ライブラリには、Goソースコードからドキュメントを生成するためのgo/docパッケージがあります。このパッケージは、ASTからパッケージ、型、関数、メソッドなどの情報を抽出し、構造化された形式で提供します。cmd/goapiもこのパッケージを利用してAPI情報を取得しています。
  • 後方互換性 (Backward Compatibility): ソフトウェアの新しいバージョンが、古いバージョン向けに作成されたデータ、コード、またはシステムと引き続き連携できる特性を指します。Go言語の標準ライブラリでは、Go 1以降、厳格な後方互換性ポリシーが適用されており、既存のコードを壊すようなAPI変更は原則として行われません。
  • API (Application Programming Interface): ソフトウェアコンポーネントが互いに通信するために使用する一連の定義とプロトコル。Go言語の文脈では、主に公開された関数、型、メソッド、変数などを指します。
  • go listコマンド: Goのビルドシステムの一部であり、Goパッケージに関する情報を表示するために使用されます。このコミットでは、go list stdを使用して標準ライブラリのパッケージリストを取得しています。
  • go/buildパッケージ: Goのビルドプロセスをプログラム的に操作するためのパッケージです。ソースファイルの検索やパッケージの解決などに使用されます。

技術的詳細

cmd/goapiツールは、Go言語のソースコードを静的に解析し、各パッケージからエクスポートされたAPI要素(関数、メソッド、型、定数、変数)を特定します。これらのAPI要素は、特定のフォーマットで「機能記述(feature description)」として表現され、api/ディレクトリ内のテキストファイルに保存されます。

ツールの主要なロジックは以下の通りです。

  1. パッケージの走査: main.gomain関数は、引数で指定されたパッケージ、またはgo list stdで取得した標準ライブラリの全パッケージを走査します。cmd/exp/old/で始まるパッケージはスキップされます。
  2. ASTの解析: Walker構造体がGoソースファイルのASTを走査します。go/parser.ParseFileを使用してソースファイルを解析し、go/astパッケージの機能を利用してASTノードを巡回します。
  3. エクスポートされた識別子の特定: ast.IsExported関数を使用して、識別子がエクスポートされているかどうかを判断します。エクスポートされた識別子のみがAPIとして考慮されます。
  4. API要素の抽出とフォーマット:
    • 定数 (const): walkConst関数が処理します。定数の名前とその型(例: ideal-int, stringなど)を抽出します。型推論が難しいケース(iotaや特定のパッケージの定数)にはハードコードされたロジックも含まれます。
    • 変数 (var): walkVar関数が処理します。変数の名前とその型を抽出します。
    • 型 (type): walkTypeSpec関数が処理します。
      • 構造体 (struct) の場合: walkStructTypeが構造体名と、エクスポートされたフィールドの名前と型を抽出します。埋め込みフィールドも考慮されます。
      • インターフェース (interface) の場合: walkInterfaceTypeがインターフェース名と、エクスポートされたメソッドのシグネチャを抽出します。
      • その他の型の場合: 型名とその基底型を抽出します。
    • 関数 (func) およびメソッド: go/doc.Newを使用してパッケージのドキュメントモデルを生成した後、dpkg.Funcst.Methodsから関数とメソッドの宣言を取得し、walkFuncDeclで処理します。関数名またはメソッド名、レシーバ(メソッドの場合)、および関数シグネチャ(引数と戻り値の型)を抽出します。引数や戻り値の変数名はAPIの一部ではないため、namelessType関数で除去されます。
  5. 機能記述の生成: 抽出されたAPI要素は、pkg <package_name>, <element_type> <element_name> <signature/type>のような形式の文字列として表現され、Walker.featuresマップに保存されます。例えば、pkg p1, func Bar(int8, int16, int64)のようになります。
  6. APIファイルの比較と出力:
    • -c <filename>フラグが指定された場合、現在のAPIと指定されたファイル(過去のAPIスナップショット)を比較します。差分(追加されたAPIと削除されたAPI)を出力します。これにより、APIの互換性違反を検出できます。
    • フラグが指定されない場合、現在のパッケージから抽出された全てのAPI機能記述を標準出力に出力します。

このツールは、GoのASTとドキュメンテーション生成のメカニズムを深く利用しており、Go言語のセマンティクスを理解した上でAPIを正確に抽出するよう設計されています。特に、定数や変数の型推論、埋め込みフィールドの処理、関数シグネチャの正規化など、Go言語の特性に合わせた複雑なロジックが含まれています。

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

このコミットでは、主に以下の4つの新しいファイルが追加されています。

  1. src/cmd/goapi/goapi.go:

    • main関数: コマンドライン引数の解析、パッケージの走査、Walkerの初期化と実行、APIの比較または出力ロジックが含まれます。
    • Walker構造体: AST走査の状態(ファイルセット、スコープ、抽出された機能、定数の型情報など)を保持します。
    • NewWalker関数: Walkerのコンストラクタ。
    • Featuresメソッド: 抽出された全てのAPI機能記述をソートして返します。
    • WalkPackage, walkFile, walkConst, walkVar, walkTypeSpec, walkStructType, walkInterfaceType, walkFuncDeclなどのwalkメソッド群: ASTを再帰的に走査し、エクスポートされたAPI要素を特定して抽出します。
    • constValueType, varValueType: 定数や変数の値から型を推論するロジック。
    • resolveName: 識別子を解決し、対応するASTノードを検索するヘルパー関数。
    • nodeString, nodeDebug: ASTノードを文字列に変換するヘルパー関数。
    • funcSigString, namelessType, namelessFieldList, namelessField: 関数シグネチャを正規化し、変数名を除去するためのヘルパー関数。
    • emitFeature: 抽出されたAPI機能記述をWalker.featuresマップに追加する関数。
    • pushScope: スコープ管理のためのヘルパー関数。
    • hardCodedConstantType: 型推論が難しい特定の定数に対するハードコードされた型情報。
  2. src/cmd/goapi/goapi_test.go:

    • TestGolden関数: goapiツールのテストスイート。testdataディレクトリ内のパッケージに対してgoapiを実行し、生成されたAPI記述がgolden.txtファイルの内容と一致するかを検証します。-updategoldenフラグでgolden.txtを更新する機能も提供します。
  3. src/cmd/goapi/testdata/p1/golden.txt:

    • p1パッケージの期待されるAPI記述を含むゴールデンファイル。goapi_test.goによって参照されます。
  4. src/cmd/goapi/testdata/p1/p1.go:

    • goapiツールのテストに使用されるサンプルGoパッケージ。様々なGoの言語機能(定数、変数、構造体、インターフェース、関数、メソッド、埋め込みフィールドなど)を含むように設計されており、goapiがこれらの要素を正しく抽出できるかを検証します。

これらのファイルは、cmd/goapiツールの完全な実装とテストケースを構成しています。

コアとなるコードの解説

goapi.goの主要なロジックは、Walker構造体とそのwalkメソッド群に集約されています。

Walker構造体

type Walker struct {
	fset           *token.FileSet
	scope          []string
	features       map[string]bool // set
	lastConstType  string
	curPackageName string
	curPackage     *ast.Package
	prevConstType  map[string]string // identifer -> "ideal-int"
}
  • fset: go/token.FileSetは、ソースファイル内の位置情報を管理します。ASTノードから正確な位置情報を取得するために必要です。
  • scope: 現在走査しているASTのスコープ(例: pkg compress/gzip, type MyStruct structなど)を文字列のスタックとして保持します。これにより、生成される機能記述にコンテキストを追加できます。
  • features: 抽出された全てのAPI機能記述を保持するマップ。重複を避けるためにセットとして使用されます。
  • lastConstType, prevConstType: Goの定数宣言におけるiotaや型推論の挙動を正確に追跡するために、直前の定数の型や以前に解決された定数の型を記憶します。
  • curPackageName, curPackage: 現在処理中のパッケージの名前とAST表現。

walkFuncDecl関数 (メソッドの例)

func (w *Walker) walkFuncDecl(f *ast.FuncDecl) {
	if !ast.IsExported(f.Name.Name) {
		return
	}
	if f.Recv != nil {
		// Method.
		recvType := w.nodeString(f.Recv.List[0].Type)
		keep := ast.IsExported(recvType) ||
			(strings.HasPrefix(recvType, "*") &&
				ast.IsExported(recvType[1:]))
		if !keep {
			return
		}
		w.emitFeature(fmt.Sprintf("method (%s) %s%s", recvType, f.Name.Name, w.funcSigString(f.Type)))
		return
	}
	// Else, a function
	w.emitFeature(fmt.Sprintf("func %s%s", f.Name.Name, w.funcSigString(f.Type)))
}

この関数は、関数またはメソッドの宣言を処理します。

  1. ast.IsExported(f.Name.Name)で、関数/メソッド名がエクスポートされているかを確認します。エクスポートされていない場合はスキップします。
  2. f.Recv != nilで、それがメソッドであるか(レシーバを持つか)を判断します。
  3. メソッドの場合、レシーバの型(例: (MyType)(*MyType)) を抽出し、その型自体がエクスポートされているかを確認します。エクスポートされていない型に紐づくメソッドはAPIとして扱われません。
  4. fmt.Sprintfw.funcSigString(f.Type)を使って、正規化された関数/メソッドシグネチャ(引数名を含まない型のみのシグネチャ)を生成し、emitFeatureで記録します。

funcSigString関数

func (w *Walker) funcSigString(ft *ast.FuncType) string {
	var b bytes.Buffer
	b.WriteByte('(')
	if ft.Params != nil {
		for i, f := range ft.Params.List {
			if i > 0 {
				b.WriteString(", ")
			}
			b.WriteString(w.nodeString(w.namelessType(f.Type)))
		}
	}
	b.WriteByte(')')
	if ft.Results != nil {
		if nr := len(ft.Results.List); nr > 0 {
			b.WriteByte(' ')
			if nr > 1 {
				b.WriteByte('(')
			}
			for i, f := range ft.Results.List {
				if i > 0 {
					b.WriteString(", ")
				}
				b.WriteString(w.nodeString(w.namelessType(f.Type)))
			}
			if nr > 1 {
				b.WriteByte(')')
			}
		}
	}
	return b.String()
}

この関数は、*ast.FuncType(関数シグネチャのAST表現)を受け取り、引数名や戻り値の変数名を含まない、型のみの正規化された文字列シグネチャを生成します。これは、APIの互換性をチェックする際に、変数名の変更がAPIの変更と見なされないようにするために重要です。w.namelessType(f.Type)が、この「変数名除去」の役割を担っています。

これらのコアな変更箇所は、Go言語のASTを深く理解し、APIの定義と互換性に関するGoの設計思想を反映したものです。

関連リンク

  • Go 1 Compatibility Guarantee: Go言語の公式ドキュメントにおける後方互換性保証に関する説明。cmd/goapiはこの保証を技術的に支えるツールの一つです。
  • Go AST Package Documentation: go/astパッケージの公式ドキュメント。
  • Go Doc Package Documentation: go/docパッケージの公式ドキュメント。

参考にした情報源リンク