[インデックス 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言語のソ