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

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

このコミットは、Go言語のコマンドラインツール cmd/go におけるパッケージリストの順序予測可能性を向上させるための変更と、cmd/go/test.bash スクリプトにクリーンアップフェーズを追加するものです。特に、Goのマップ(map)のイテレーション順序が不定であるという特性に起因する go list コマンドの出力の不安定性を解消することを目的としています。

コミット

commit 6a3ad481cd495bc22aa4f892ad8f0c225acac1f3
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Sat Oct 20 17:25:13 2012 +0800

    cmd/go: make package list order predicable
    also add a cleanup phase to cmd/go/test.bash.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6741050

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

https://github.com/golang/go/commit/6a3ad481cd495bc22aa4f892ad8f0c225acac1f3

元コミット内容

cmd/go: make package list order predicable
also add a cleanup phase to cmd/go/test.bash.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6741050

変更の背景

Go言語の map 型は、その設計上、要素のイテレーション(走査)順序が保証されていません。これは、パフォーマンス最適化のためにハッシュテーブルが内部的に使用されており、要素の追加や削除によってメモリ上の配置が変わり、結果としてイテレーション順序が実行ごとに、あるいは同じ実行内でも変化する可能性があるためです。Go 1以降では、この不定性を開発者が当てにしないように、意図的にイテレーション順序がランダム化されています。

cmd/go ツール、特に go list コマンドは、Goのパッケージ情報をリストアップする際に内部的にマップを使用している可能性がありました。このマップの不定なイテレーション順序が、go list コマンドの出力順序に影響を与え、テストの再現性やスクリプトでの利用において問題を引き起こす可能性がありました。例えば、go list std のようなコマンドを実行した際に、標準ライブラリのパッケージリストが実行ごとに異なる順序で出力されると、その出力を比較するテストが不安定になります。

このコミットは、このような go list コマンドの出力順序の不安定性を解消し、予測可能な順序でパッケージがリストされるようにすることで、テストの信頼性を向上させ、ツールの挙動をより安定させることを目的としています。また、テストスクリプト test.bash にクリーンアップ処理を追加することで、テスト実行後の環境をきれいに保ち、次のテスト実行に影響が出ないようにしています。

前提知識の解説

Go言語の map のイテレーション順序

Go言語の map はキーと値のペアを格納するデータ構造で、他の言語のハッシュマップや辞書に相当します。Goの仕様では、map のイテレーション順序は保証されていません。これは、map が内部的にハッシュテーブルとして実装されており、要素の追加、削除、リサイズなどによってメモリ上の配置が動的に変化するためです。Go 1からは、開発者がこの不定な順序に依存しないように、意図的にイテレーション順序がランダム化されるようになりました。これにより、もしコードが map のイテレーション順序に依存している場合、その問題が早期に発見されるようになっています。

もし特定の順序で map の要素を処理したい場合は、キーをスライスに抽出し、そのスライスをソートしてから、ソートされたキーを使って map から値を取り出すという方法が一般的です。

go list コマンド

go list コマンドは、Goのパッケージやモジュールに関する情報を表示するための強力なツールです。指定されたパッケージのインポートパス、ディレクトリ、依存関係、ビルド情報など、多岐にわたる詳細情報を取得できます。このコマンドは、Goプロジェクトの構造を理解したり、ビルドスクリプトや自動化ツールでパッケージ情報を利用したりする際によく使われます。

例えば、go list std はGoの標準ライブラリに含まれるすべてのパッケージのインポートパスをリストアップします。go list -f '{{.Dir}} {{.ImportPath}}' のように -f フラグとGoの text/template 構文を組み合わせることで、出力形式をカスタマイズし、より詳細な情報を抽出することも可能です。

test.bash スクリプト

test.bash は、Goプロジェクトのテストスイートを実行するためのシェルスクリプトです。Goの標準ライブラリやツールのテストは、このようなスクリプトを通じて自動化されており、継続的インテグレーション(CI)システムなどで利用されます。テストスクリプトは、テストの実行、結果の検証、そしてテストによって生成された一時ファイルのクリーンアップなど、一連の処理を自動で行います。

技術的詳細

このコミットの主要な変更点は、src/cmd/go/pkg.go ファイル内の packagesAndErrors 関数におけるパッケージのロードロジックです。

変更前は、args スライスから引数を受け取り、それを set という map[string]bool に格納していました。その後、この set マップをイテレーションして loadPackage を呼び出し、結果を pkgs スライスに追加していました。

// 変更前 (簡略化)
var set = make(map[string]bool)
for _, arg := range args {
    set[arg] = true // ここでマップに格納
}
for arg := range set { // マップのイテレーション順序は不定
    pkgs = append(pkgs, loadPackage(arg, &stk))
}

Goの map のイテレーション順序は不定であるため、for arg := range set のループは、set に格納されたキー(パッケージ名)を毎回異なる順序で返す可能性がありました。これが pkgs スライスにパッケージが追加される順序に影響を与え、結果として go list コマンドの出力順序が不安定になる原因となっていました。

変更後は、args スライスを直接イテレーションし、set マップは既に処理した引数を追跡するためにのみ使用されます。

// 変更後 (簡略化)
var set = make(map[string]bool)
for _, arg := range args { // args スライスのイテレーション順序は保証される
    if !set[arg] { // 既に処理済みでなければ
        pkgs = append(pkgs, loadPackage(arg, &stk))
        set[arg] = true // 処理済みとしてマーク
    }
}

この変更により、pkgs スライスにパッケージが追加される順序は、入力 args スライスの順序に厳密に従うようになります。set マップは、重複するパッケージ引数が複数回ロードされないようにするためのチェックとして機能し、map のイテレーション順序の不定性の影響を受けなくなります。これにより、go list コマンドの出力順序が予測可能かつ安定したものになります。

また、src/cmd/go/test.bash には、go list std の出力が実行間で一貫していることを確認するための新しいテストが追加されました。このテストは、go list std の出力をファイルに保存し、その後の実行で同じコマンドの出力と比較することで、順序の一貫性を検証します。さらに、テスト実行後に生成される一時ファイル (test_std.list, testdata/bin, testdata/bin1, testgo) を削除するクリーンアップフェーズが追加され、テスト環境の健全性が保たれるようになりました。

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

src/cmd/go/pkg.go

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -674,12 +674,11 @@ func packagesAndErrors(args []string) []*Package {
 	var set = make(map[string]bool)
 
 	for _, arg := range args {
-\t\tset[arg] = true
-\t}\n-\tfor arg := range set {\n-\t\tpkgs = append(pkgs, loadPackage(arg, &stk))\n+\t\tif !set[arg] {
+\t\t\tpkgs = append(pkgs, loadPackage(arg, &stk))
+\t\t\tset[arg] = true
+\t\t}
 	}\n-\n \tcomputeStale(pkgs...)\n \n \treturn pkgs

src/cmd/go/test.bash

--- a/src/cmd/go/test.bash
+++ b/src/cmd/go/test.bash
@@ -142,6 +142,18 @@ if [ $(./testgo test fmt fmt fmt fmt fmt | wc -l) -ne 1 ] ; then
     ok=false
 fi
 
+# ensure that output of 'go list' is consistent between runs
+./testgo list std > test_std.list
+if ! ./testgo list std | cmp -s test_std.list - ; then
+\techo "go list std ordering is inconsistent"
+\tok=false
+fi
+rm -f test_std.list
+
+# clean up
+rm -rf testdata/bin testdata/bin1
+rm -f testgo
+
 if $ok; then
 \techo PASS
 else

コアとなるコードの解説

src/cmd/go/pkg.go の変更

packagesAndErrors 関数は、go コマンドに渡されたパッケージ引数を処理し、対応する *Package オブジェクトのリストを返す役割を担っています。

変更前は、まずすべての引数を set マップのキーとして追加し、その後 set マップをイテレーションして loadPackage を呼び出していました。この for arg := range set の部分が、Goのマップのイテレーション順序が不定であるという特性により、pkgs スライスにパッケージが追加される順序を不安定にしていました。

変更後は、for _, arg := range args ループで元の args スライスを直接イテレーションします。args スライスは順序が保証されているため、このループの順序は常に一定です。 if !set[arg] の条件は、同じパッケージが複数回引数として渡された場合に、重複して loadPackage が呼び出されるのを防ぐためのものです。set[arg] = true で、そのパッケージが既に処理されたことを記録します。 この修正により、pkgs スライスにパッケージが追加される順序は、args スライスで指定された順序と一致するようになり、go list コマンドの出力が予測可能になります。

src/cmd/go/test.bash の変更

このシェルスクリプトの変更は、主に2つの部分からなります。

  1. go list std の出力順序の一貫性テスト:

    • ./testgo list std > test_std.list: go list std コマンドの出力を test_std.list というファイルにリダイレクトして保存します。
    • if ! ./testgo list std | cmp -s test_std.list - ; then ... fi: 再度 go list std を実行し、その出力を cmp -s コマンドを使って test_std.list の内容と比較します。
      • cmp -s は、2つのファイルが同じであれば何も出力せず、異なる場合に非ゼロの終了ステータスを返します。
      • | はパイプで、左側のコマンドの標準出力を右側のコマンドの標準入力に渡します。
      • -cmp コマンドにおいて標準入力を意味します。
    • もし比較結果が異なれば(つまり、go list std の出力順序が不安定であれば)、エラーメッセージを出力し、テスト全体の ok フラグを false に設定します。
    • rm -f test_std.list: テストで使用した一時ファイルを削除します。
  2. クリーンアップフェーズの追加:

    • rm -rf testdata/bin testdata/bin1: テスト中に生成される可能性のある testdata/bin および testdata/bin1 ディレクトリを再帰的に削除します。
    • rm -f testgo: テスト実行用のバイナリ testgo を削除します。

これらの変更により、go list コマンドの出力順序の安定性がテストによって保証されるようになり、またテスト実行後の環境が常にクリーンな状態に保たれることで、テストの信頼性と再現性が向上します。

関連リンク

  • Go言語の map のイテレーション順序に関する公式ドキュメントやブログ記事
  • go list コマンドの公式ドキュメント

参考にした情報源リンク