[インデックス 15097] ファイルの概要
このコミットは、Go言語のテストツール go test におけるExample関数の実行順序に関する重要な変更を導入しています。これまではExample関数が名前順で実行されていましたが、この変更によりソースコード上での出現順(ソースオーダー)で実行されるようになります。この修正は、Example関数の出力がその実行順序に依存する場合に発生する非決定的な動作や予期せぬ結果を防ぐことを目的としています。具体的には、go/doc パッケージの Example 構造体に Order フィールドが追加され、Example関数が読み込まれた順序を記録し、その順序に基づいてソートされるように go test コマンドが修正されました。
コミット
commit 18178fd1381615919e6a76da57b5a745cf7db7bf
Author: Russ Cox <rsc@golang.org>
Date: Sat Feb 2 16:26:12 2013 -0500
cmd/go: run examples in source order, not name order
Add Order field to doc.Example and write doc comments there.
Fixes #4662.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7229071
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/18178fd1381615919e6a76da57b5a745cf7db7bf
元コミット内容
このコミットは、go test コマンドがExample関数を実行する際の順序を、これまでの「名前順」から「ソースコード上の出現順(ソースオーダー)」に変更することを目的としています。
変更の主な内容は以下の通りです。
go/docパッケージ内のExample構造体にOrderという新しいフィールドを追加しました。このフィールドは、Example関数がソースファイル内で見つかった順序を記録するために使用されます。go/doc.Examples関数がExample関数を解析する際に、このOrderフィールドに適切な値を設定するように変更されました。cmd/goパッケージのtest.goファイルにおいて、Example関数をロードした後、この新しいOrderフィールドに基づいてソートするように修正されました。これにより、go testがExampleを実行する際に、ソースコードに記述された順序が尊重されるようになります。- この変更が正しく機能することを確認するための新しいテストケースが
testdata/example1_test.goとtestdata/example2_test.goとして追加されました。これらのテストは、Example関数の名前がアルファベット順ではないにもかかわらず、ソースコード上の順序で実行されることを検証します。 - この変更は、Go issue #4662 を解決します。
変更の背景
この変更の背景には、Go言語のExampleテストが持つ特定の課題がありました。GoのExampleテストは、コードの動作例を示すだけでなく、その出力が期待される出力と一致するかどうかを検証する機能も持っています。これは、ドキュメントとテストを兼ねる非常に強力な機能です。
しかし、このコミット以前は、go test コマンドがExample関数を検出して実行する際に、それらを関数名のアルファベット順にソートして実行していました。この「名前順」での実行には、以下のような問題がありました。
- 非決定的な出力: 複数のExample関数が同じグローバル変数や共有リソースを操作する場合、それらの実行順序が名前によって決定されるため、開発者が意図しない順序で実行される可能性がありました。特に、Example関数が何らかの状態を変更し、その変更が後続のExample関数の出力に影響を与える場合、名前順では予期せぬ出力やテストの失敗につながることがありました。例えば、
Example_AとExample_Zという関数があった場合、名前順ではExample_Aが先に実行されますが、開発者がExample_Zが先に実行されることを期待してコードを記述していると、テストが失敗する原因となります。 - ドキュメントとしての整合性の欠如: Example関数は、コードの利用方法を順序立てて説明するドキュメントとしての側面も持ちます。しかし、名前順で実行されると、ドキュメントとしての論理的な流れが崩れる可能性がありました。開発者がExampleを記述した意図は、通常、ソースコード上での出現順に沿ったものです。
- Issue #4662 の解決: この問題は、GoのIssueトラッカーで #4662 として報告されていました。このIssueでは、Example関数の実行順序が非決定的な問題を引き起こすことが指摘されており、ソースコード上の順序を尊重するよう求める声がありました。このコミットは、この長年の問題を解決するためのものです。
この変更により、Example関数はソースコードに記述された通りの順序で実行されるようになり、Exampleテストの信頼性とドキュメントとしての正確性が向上しました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とメカニズムについて理解しておく必要があります。
1. go test コマンドとExampleテスト
go test はGo言語の標準テストツールです。Goのテストは、_test.go で終わるファイルに記述され、Test、Benchmark、Example の3種類の関数をサポートします。
-
Example関数:
Exampleというプレフィックスを持つ関数で、特定のパッケージ、関数、型などの使用例を示します。Example関数は、その関数名の後に_と任意の文字列が続く形式(例:Example_MyFunction)や、パッケージ全体を示すExampleという名前(例:Example)で定義されます。 Example関数の特徴は、その関数内のコメントに// Output:という行を含めることで、そのExampleを実行した際の標準出力が期待される出力と一致するかどうかをgo testが自動的に検証する点です。これにより、コードの動作例が常に最新かつ正確であることを保証できます。package main import "fmt" func ExampleHello() { fmt.Println("Hello") // Output: Hello } func Example_Greeting() { fmt.Println("Greetings from Example_Greeting") // Output: Greetings from Example_Greeting }
2. go/doc パッケージ
go/doc パッケージは、Goのソースコードからドキュメンテーションを抽出するためのツールを提供します。go doc コマンドや pkg.go.dev のようなGoの公式ドキュメントサイトは、このパッケージを利用してGoのソースコードからコメント、関数シグネチャ、Example関数などを解析し、構造化されたドキュメントを生成します。
doc.Example構造体:go/docパッケージ内で定義されている構造体で、Goのソースコードから抽出されたExample関数の情報を保持します。この構造体には、Example関数の名前 (Name)、関連するドキュメント文字列 (Doc)、コードブロック (Code)、期待される出力 (Output) などのフィールドが含まれます。このコミットでは、この構造体にOrderフィールドが追加されました。doc.Examples関数: ソースコードファイル(*ast.File)のリストを受け取り、そこから見つかったすべてのExample関数を[]*doc.Exampleのスライスとして返します。
3. Go言語のソートインターフェース (sort.Interface)
Go言語では、任意のコレクションをソートするために sort パッケージが提供されています。このパッケージは、カスタム型をソート可能にするための sort.Interface インターフェースを定義しています。このインターフェースは以下の3つのメソッドで構成されます。
Len() int: コレクションの要素数を返します。Swap(i, j int): インデックスiとjの要素を入れ替えます。Less(i, j int) bool: インデックスiの要素がインデックスjの要素よりも小さい(ソート順で前にある)場合にtrueを返します。
このコミットでは、[]*doc.Example 型を Order フィールドに基づいてソートするために、この sort.Interface を実装した新しい型が定義されています。
4. ast パッケージ (Abstract Syntax Tree)
go/doc パッケージは、Goのソースコードを解析するために go/ast パッケージを利用します。go/ast は、Goのソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。go/doc.Examples 関数は、このASTを走査してExample関数を識別し、その情報を抽出します。
技術的詳細
このコミットの技術的詳細は、Example関数の検出、情報の格納、そしてソートという3つの主要なステップに分けられます。
1. doc.Example 構造体への Order フィールドの追加
まず、src/pkg/go/doc/example.go ファイルにおいて、Example 構造体に Order という新しい int 型のフィールドが追加されました。
// An Example represents an example function found in a source files.
type Example struct {
Name string // name of the item being exemplified
Doc string // example function doc string
Code *ast.BlockStmt
Comments []*ast.CommentGroup
Output string // expected output
EmptyOutput bool // expect empty output
Order int // original source code order
}
この Order フィールドは、Example関数がソースファイル内で見つかった順序を数値として記録するために使用されます。これにより、Example関数の物理的な出現順序をプログラム的に追跡することが可能になります。
2. doc.Examples 関数での Order フィールドの設定
次に、src/pkg/go/doc/example.go 内の Examples 関数が修正され、Example関数を解析して Example 構造体を生成する際に、この新しい Order フィールドに値を設定するようになりました。
// Examples returns the examples found in the files, sorted by Name field.
// The Order fields record the order in which the examples were encountered.
func Examples(files ...*ast.File) []*Example {
var list []*Example
for _, file := range files {
// ... (既存のExample検出ロジック) ...
if isExample(decl.Name.Name) {
// ... (既存のExample情報抽出ロジック) ...
list = append(list, &Example{
Name: decl.Name.Name,
Doc: doc,
Code: body,
Comments: file.Comments,
Output: output,
EmptyOutput: output == "" && hasOutput,
Order: len(list), // ここでOrderフィールドを設定
})
}
// ...
}
// ... (既存のソートロジック - このコミットで削除される) ...
return list
}
Order: len(list) の行が追加されています。これは、list スライスにExampleが追加されるたびに、その時点でのスライスの長さ(つまり、これまでに検出されたExampleの数)を Order フィールドに割り当てることを意味します。これにより、Exampleがソースコード内で検出された順序が 0 から始まる連番として Order フィールドに記録されます。
また、doc.Examples 関数のコメントが更新され、「Examples returns the examples found in the files, sorted by Name field.」という記述が「The Order fields record the order in which the examples were encountered.」という記述に変わっています。これは、doc.Examples 関数自体がExampleを名前順でソートする責任を持たなくなり、Order フィールドがその順序を記録する役割を担うことを示唆しています。実際、このコミットでは doc.Examples 内の最終的な名前順ソートロジックが削除されています。
3. cmd/go/test.go でのExampleのソート
最後に、src/cmd/go/test.go ファイルにおいて、go test コマンドがExample関数をロードした後に、この Order フィールドに基づいてソートするロジックが追加されました。
func (t *testFuncs) load(filename, pkg string, seen *bool) error {
// ...
ex := doc.Examples(f) // Example関数をロード
sort.Sort(byOrder(ex)) // Orderフィールドに基づいてソート
for _, e := range ex {
// ... (Exampleの実行ロジック) ...
}
return nil
}
type byOrder []*doc.Example
func (x byOrder) Len() int { return len(x) }
func (x byOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byOrder) Less(i, j int) bool { return x[i].Order < x[j].Order }
ここで重要なのは以下の点です。
doc.Examples(f)から返されたExampleのスライスexが、sort.Sort(byOrder(ex))によってソートされています。byOrderという新しい型が定義されており、これは[]*doc.Exampleのエイリアスです。byOrder型はsort.Interfaceインターフェース(Len,Swap,Lessメソッド)を実装しています。Less(i, j int) boolメソッドの実装がreturn x[i].Order < x[j].Orderとなっており、これによりExampleはOrderフィールドの昇順でソートされます。つまり、ソースコード上で先に現れたExampleが先に実行されるようになります。
この一連の変更により、go test はExample関数をソースコード上の出現順に実行するようになり、Exampleテストの信頼性と予測可能性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/pkg/go/doc/example.go:doc.Example構造体にOrder intフィールドを追加。doc.Examples関数内で、Example関数を検出した際にOrderフィールドにlen(list)を設定するロジックを追加。doc.Examples関数のコメントを更新し、Orderフィールドの役割を明記。
-
src/cmd/go/test.go:testFuncs.loadメソッド内で、doc.Examplesから取得したExampleのスライスをOrderフィールドに基づいてソートするsort.Sort(byOrder(ex))を追加。byOrderという新しい型を定義し、sort.Interfaceを実装(Len,Swap,Lessメソッド)。LessメソッドはOrderフィールドを比較する。
-
src/cmd/go/test.bash:- 新しいテストケース
testdata/example[12]_test.goを実行する行を追加。このテストは、Exampleがソースオーダーで実行されることを検証します。
- 新しいテストケース
-
src/cmd/go/testdata/example1_test.go(新規ファイル):Example_ZとExample_AというExample関数を含むテストファイル。名前順ではExample_Aが先だが、ソースオーダーではExample_Zが先になるように記述されており、go testがソースオーダーを尊重することを確認する。
-
src/cmd/go/testdata/example2_test.go(新規ファイル):Example_YとExample_BというExample関数を含むテストファイル。example1_test.goと同様に、ソースオーダーが尊重されることを確認する。
コアとなるコードの解説
src/pkg/go/doc/example.go の変更
// An Example represents an example function found in a source files.
type Example struct {
// ... 既存のフィールド ...
Order int // original source code order
}
// Examples returns the examples found in the files, sorted by Name field.
// The Order fields record the order in which the examples were encountered.
func Examples(files ...*ast.File) []*Example {
var list []*Example
for _, file := range files {
// ...
if isExample(decl.Name.Name) {
// ...
list = append(list, &Example{
// ... 既存のフィールドの設定 ...
Order: len(list), // Exampleがリストに追加される際の現在の長さを順序として記録
})
}
// ...
}
return list
}
Order intフィールド:Example構造体に追加されたこのフィールドは、Example関数がソースファイル内で解析され、listスライスに追加された順序を整数値で保持します。len(list)を使用することで、0から始まる連番が割り当てられ、これがExampleのソースコード上の相対的な位置を示します。doc.Examplesの役割: この関数は、Goのソースファイル(AST形式)を走査し、Example関数を識別してExample構造体のスライスとして収集します。このコミット以前は、この関数がExampleを名前順でソートして返していましたが、この変更により、ソートの責任はgo testコマンド側に移り、doc.Examplesは単に検出順にOrderフィールドを設定してExampleを返すようになりました。
src/cmd/go/test.go の変更
func (t *testFuncs) load(filename, pkg string, seen *bool) error {
// ...
ex := doc.Examples(f) // doc.ExamplesからExample関数を取得
sort.Sort(byOrder(ex)) // 取得したExampleをOrderフィールドに基づいてソート
for _, e := range ex {
if e.Output == "" && !e.EmptyOutput {
// 出力がないExampleは実行しない
continue
}
// Exampleの実行ロジック
t.add(e.Name, e.Doc, e.Code, e.Output, e.EmptyOutput)
}
return nil
}
// byOrder は []*doc.Example を Order フィールドでソートするための sort.Interface 実装
type byOrder []*doc.Example
func (x byOrder) Len() int { return len(x) }
func (x byOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byOrder) Less(i, j int) bool { return x[i].Order < x[j].Order }
ex := doc.Examples(f):go/docパッケージのExamples関数を呼び出し、現在のテストファイルからExample関数を抽出します。この時点では、ExampleはOrderフィールドが設定された状態で、検出された順序でスライスに格納されています。sort.Sort(byOrder(ex)): ここがこのコミットの核心部分です。sort.Sort関数は、sort.Interfaceを実装した任意のデータ構造をソートするために使用されます。ここでは、byOrder型にキャストされたexスライスが渡されます。これにより、byOrder型に実装されたLessメソッド(Orderフィールドを比較する)に基づいてExampleがソートされます。結果として、exスライス内のExampleは、ソースコード上での出現順に並べ替えられます。byOrder型: このカスタム型は、[]*doc.Exampleのエイリアスであり、Goの標準ライブラリsortパッケージが提供するsort.Interfaceを実装しています。Len(): スライスの長さを返します。Swap(i, j int): スライス内の2つのExampleの順序を入れ替えます。Less(i, j int) bool:i番目のExampleのOrderフィールドがj番目のExampleのOrderフィールドよりも小さい場合にtrueを返します。これにより、Orderフィールドが小さい(つまり、ソースコード上で先に現れる)Exampleがソート後のスライスで前に来るようになります。
この変更により、go test はExample関数を常にソースコード上の記述順に実行するようになり、Exampleテストの出力が予測可能で安定したものになります。
新規テストファイル (example1_test.go, example2_test.go) の解説
// src/cmd/go/testdata/example1_test.go
package p
import "fmt"
var n int
func Example_Z() {
n++
fmt.Println(n)
// Output: 1
}
func Example_A() {
n++
fmt.Println(n)
// Output: 2
}
このテストファイルでは、Example_Z と Example_A という2つのExample関数が定義されています。
- 関数名のアルファベット順では
Example_AがExample_Zよりも先に実行されるはずです。 - しかし、ソースコード上では
Example_ZがExample_Aよりも先に記述されています。 - 両方の関数はグローバル変数
nをインクリメントし、その値を出力します。 Example_Zの期待出力は1、Example_Aの期待出力は2です。- もし
go testが名前順で実行されると、Example_Aが先に実行されnは1となり、Example_Zが次に実行されnは2となるため、期待される出力と一致せずテストは失敗します。 - このコミットの変更が適用されると、
Example_Zがソースオーダーで先に実行されnは1となり、次にExample_Aが実行されnは2となるため、期待される出力と一致しテストは成功します。これにより、ソースオーダーでの実行が検証されます。
example2_test.go も同様のロジックで、Example_Y と Example_B を使用してソースオーダーでの実行を検証します。
関連リンク
- Go Issue #4662: https://github.com/golang/go/issues/4662
- Gerrit Change-Id: https://golang.org/cl/7229071
参考にした情報源リンク
- Go言語の公式ドキュメント:
go testコマンド、go/docパッケージ、sortパッケージに関する情報。 - Go言語のソースコード: 特に
src/cmd/go/test.goとsrc/pkg/go/doc/example.goの変更履歴。 - Go Issue #4662 の議論: 問題の背景と解決策に関するコミュニティの議論。
- Go言語のExampleテストに関するブログ記事やチュートリアル。
- Go言語のソ