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

[インデックス 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関数を実行する際の順序を、これまでの「名前順」から「ソースコード上の出現順(ソースオーダー)」に変更することを目的としています。

変更の主な内容は以下の通りです。

  1. go/doc パッケージ内の Example 構造体に Order という新しいフィールドを追加しました。このフィールドは、Example関数がソースファイル内で見つかった順序を記録するために使用されます。
  2. go/doc.Examples 関数がExample関数を解析する際に、この Order フィールドに適切な値を設定するように変更されました。
  3. cmd/go パッケージの test.go ファイルにおいて、Example関数をロードした後、この新しい Order フィールドに基づいてソートするように修正されました。これにより、go test がExampleを実行する際に、ソースコードに記述された順序が尊重されるようになります。
  4. この変更が正しく機能することを確認するための新しいテストケースが testdata/example1_test.gotestdata/example2_test.go として追加されました。これらのテストは、Example関数の名前がアルファベット順ではないにもかかわらず、ソースコード上の順序で実行されることを検証します。
  5. この変更は、Go issue #4662 を解決します。

変更の背景

この変更の背景には、Go言語のExampleテストが持つ特定の課題がありました。GoのExampleテストは、コードの動作例を示すだけでなく、その出力が期待される出力と一致するかどうかを検証する機能も持っています。これは、ドキュメントとテストを兼ねる非常に強力な機能です。

しかし、このコミット以前は、go test コマンドがExample関数を検出して実行する際に、それらを関数名のアルファベット順にソートして実行していました。この「名前順」での実行には、以下のような問題がありました。

  1. 非決定的な出力: 複数のExample関数が同じグローバル変数や共有リソースを操作する場合、それらの実行順序が名前によって決定されるため、開発者が意図しない順序で実行される可能性がありました。特に、Example関数が何らかの状態を変更し、その変更が後続のExample関数の出力に影響を与える場合、名前順では予期せぬ出力やテストの失敗につながることがありました。例えば、Example_AExample_Z という関数があった場合、名前順では Example_A が先に実行されますが、開発者が Example_Z が先に実行されることを期待してコードを記述していると、テストが失敗する原因となります。
  2. ドキュメントとしての整合性の欠如: Example関数は、コードの利用方法を順序立てて説明するドキュメントとしての側面も持ちます。しかし、名前順で実行されると、ドキュメントとしての論理的な流れが崩れる可能性がありました。開発者がExampleを記述した意図は、通常、ソースコード上での出現順に沿ったものです。
  3. Issue #4662 の解決: この問題は、GoのIssueトラッカーで #4662 として報告されていました。このIssueでは、Example関数の実行順序が非決定的な問題を引き起こすことが指摘されており、ソースコード上の順序を尊重するよう求める声がありました。このコミットは、この長年の問題を解決するためのものです。

この変更により、Example関数はソースコードに記述された通りの順序で実行されるようになり、Exampleテストの信頼性とドキュメントとしての正確性が向上しました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とメカニズムについて理解しておく必要があります。

1. go test コマンドとExampleテスト

go test はGo言語の標準テストツールです。Goのテストは、_test.go で終わるファイルに記述され、TestBenchmarkExample の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): インデックス ij の要素を入れ替えます。
  • 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テストの信頼性と予測可能性が大幅に向上しました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/go/doc/example.go:

    • doc.Example 構造体に Order int フィールドを追加。
    • doc.Examples 関数内で、Example関数を検出した際に Order フィールドに len(list) を設定するロジックを追加。
    • doc.Examples 関数のコメントを更新し、Order フィールドの役割を明記。
  2. src/cmd/go/test.go:

    • testFuncs.load メソッド内で、doc.Examples から取得したExampleのスライスを Order フィールドに基づいてソートする sort.Sort(byOrder(ex)) を追加。
    • byOrder という新しい型を定義し、sort.Interface を実装(Len, Swap, Less メソッド)。Less メソッドは Order フィールドを比較する。
  3. src/cmd/go/test.bash:

    • 新しいテストケース testdata/example[12]_test.go を実行する行を追加。このテストは、Exampleがソースオーダーで実行されることを検証します。
  4. src/cmd/go/testdata/example1_test.go (新規ファイル):

    • Example_ZExample_A というExample関数を含むテストファイル。名前順では Example_A が先だが、ソースオーダーでは Example_Z が先になるように記述されており、go test がソースオーダーを尊重することを確認する。
  5. src/cmd/go/testdata/example2_test.go (新規ファイル):

    • Example_YExample_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_ZExample_A という2つのExample関数が定義されています。

  • 関数名のアルファベット順では Example_AExample_Z よりも先に実行されるはずです。
  • しかし、ソースコード上では Example_ZExample_A よりも先に記述されています。
  • 両方の関数はグローバル変数 n をインクリメントし、その値を出力します。
  • Example_Z の期待出力は 1Example_A の期待出力は 2 です。
  • もし go test が名前順で実行されると、Example_A が先に実行され n1 となり、Example_Z が次に実行され n2 となるため、期待される出力と一致せずテストは失敗します。
  • このコミットの変更が適用されると、Example_Z がソースオーダーで先に実行され n1 となり、次に Example_A が実行され n2 となるため、期待される出力と一致しテストは成功します。これにより、ソースオーダーでの実行が検証されます。

example2_test.go も同様のロジックで、Example_YExample_B を使用してソースオーダーでの実行を検証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: go test コマンド、go/doc パッケージ、sort パッケージに関する情報。
  • Go言語のソースコード: 特に src/cmd/go/test.gosrc/pkg/go/doc/example.go の変更履歴。
  • Go Issue #4662 の議論: 問題の背景と解決策に関するコミュニティの議論。
  • Go言語のExampleテストに関するブログ記事やチュートリアル。
  • Go言語のソ