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

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

このコミットは、Go言語のコマンドラインツールであるgoコマンドの一部、具体的にはテスト関連のフラグ処理を司るsrc/cmd/go/testflag.goファイルに対する修正です。このファイルは、go testコマンドが受け取る引数を解析し、テストバイナリに渡すべき引数とそうでないものを区別する役割を担っています。

コミット

commit 16a2d2617fa96dc85359d9919e6dceff1413feab
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Wed Dec 21 21:20:17 2011 +0900

    cmd/go: avoid infinite loop with package specific flags

    R=rsc
    CC=golang-dev
    https://golang.org/cl/5505053

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

https://github.com/golang/go/commit/16a2d2617fa96dc85359d9919e6dceff1413feab

元コミット内容

cmd/go: avoid infinite loop with package specific flags

R=rsc
CC=golang-dev
https://golang.org/cl/5505053

変更の背景

このコミットは、go testコマンドがパッケージ固有のフラグ(テストバイナリ自体に渡されるべきフラグ)を処理する際に発生していた無限ループのバグを修正するために行われました。

go testコマンドは、Goのテストフレームワークを実行するための主要なツールです。このコマンドは、自身のフラグ(例: -v-run)と、テストバイナリに直接渡されるべきフラグ(例: -test.v-test.run、またはユーザー定義のフラグ)の両方を処理する必要があります。testflag.go内のtestFlags関数は、この引数の振り分けを担当していました。

元の実装では、go testコマンドが認識しない、しかしテストバイナリに渡されるべき引数(パッケージ固有のフラグ)が与えられた場合、その引数をtestFlags関数が処理中のargsスライスに再度追加していました。これは、forループがargsスライスの長さに依存しているため、ループ内でスライスが無限に拡張され、結果として無限ループに陥る原因となっていました。

前提知識の解説

go testコマンドとフラグ処理

go testコマンドは、Goプロジェクトのテストを実行するためのコマンドです。このコマンドは、大きく分けて2種類のフラグを扱います。

  1. go testコマンド自身のフラグ: 例えば、-v (詳細出力)、-run <regexp> (特定のテストの実行)、-count <n> (テストの実行回数) などがあります。これらはgo testコマンド自体が解釈し、テストの実行方法を制御します。
  2. テストバイナリに渡されるフラグ: go testコマンドは、テストコードをコンパイルしてテストバイナリを生成し、それを実行します。このテストバイナリも独自のフラグを持つことができます。Goの標準テストパッケージtestingが提供するフラグ(例: -test.v-test.run)や、ユーザーがテストコード内でflagパッケージなどを使って定義したカスタムフラグがこれに該当します。

go testコマンドは、引数を解析する際に、どちらの種類のフラグであるかを判断し、適切なものだけをテストバイナリに渡す必要があります。この振り分け処理がsrc/cmd/go/testflag.gotestFlags関数で行われています。

Go言語のスライスとappend関数

Go言語のスライスは、可変長シーケンスを表現するためのデータ構造です。スライスは内部的に配列を参照しており、その長さと容量を持っています。

append関数は、スライスに要素を追加するために使用されます。appendは新しいスライスを返すことがあり、これは元のスライスの容量が不足した場合に、より大きな新しい基底配列が割り当てられ、そこに要素がコピーされるためです。

このコミットの文脈では、args = append(args, arg)という操作が重要です。これは、argsスライスにargを追加し、その結果を再びargsに代入しています。もしargsがループのイテレーション対象である場合、この操作はループの終了条件に影響を与え、無限ループを引き起こす可能性があります。

技術的詳細

src/cmd/go/testflag.go内のtestFlags関数は、go testコマンドに与えられた引数argsをループで処理します。この関数は、各引数がgo testコマンド自身のフラグであるか、それともテストバイナリに渡すべきフラグであるかをtestFlag関数を使って判定します。

元のコードの関連部分:

func testFlags(args []string) (passToTest []string) {
	for i := 0; i < len(args); i++ {
		arg := args[i] // 現在の引数を取得
		f, value, extraWord := testFlag(args, i)
		if f == nil { // go testコマンドのフラグではない場合
			args = append(args, arg) // !!! 問題の箇所 !!!
			continue
		}
		// ... フラグの処理 ...
	}
	return
}

問題はif f == nilのブロック内にありました。testFlag関数がnilを返した場合、それは現在の引数arggo testコマンド自身のフラグではないことを意味します。この場合、その引数はテストバイナリに渡されるべきものと判断されます。しかし、元のコードではargs = append(args, arg)として、この引数を現在処理中のargsスライスの末尾に再追加していました。

forループはlen(args)を終了条件としています。ループ内でargsに要素が追加されると、len(args)が増加し、ループの終了条件が後退します。もしtestFlagが常にnilを返すような引数(例えば、go testが認識しないがテストバイナリが認識するカスタムフラグ)が与えられた場合、その引数は無限にargsスライスに追加され続け、結果としてループが終了しなくなる、つまり無限ループが発生していました。

このコミットは、この問題を解決するために、テストバイナリに渡すべき引数をargsスライスではなく、関数の戻り値として定義されている別のスライスpassToTestに追加するように修正しました。

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

--- a/src/cmd/go/testflag.go
+++ b/src/cmd/go/testflag.go
@@ -80,10 +80,9 @@ var testFlagDefn = []*testFlagSpec{
 // test.out's arguments.
 func testFlags(args []string) (passToTest []string) {
 	for i := 0; i < len(args); i++ {
-\t\targ := args[i]
 \t\tf, value, extraWord := testFlag(args, i)
 \t\tif f == nil {
-\t\t\targs = append(args, arg)
+\t\t\tpassToTest = append(passToTest, args[i])
 \t\t\tcontinue
 \t\t}
 \t\tswitch f.name {

コアとなるコードの解説

変更はtestFlags関数内の以下の1行です。

  • 変更前: args = append(args, arg)
  • 変更後: passToTest = append(passToTest, args[i])

この修正により、testFlag関数がnilを返した場合(つまり、引数がgo testコマンド自身のフラグではない場合)、その引数args[i]は、testFlags関数の戻り値として定義されているpassToTestスライスに追加されるようになりました。

passToTestスライスは、testFlags関数のローカル変数であり、forループのイテレーション対象であるargsスライスとは独立しています。したがって、passToTestに要素を追加してもargsスライスの長さは変化せず、forループの終了条件に影響を与えることはありません。これにより、無限ループが回避され、go testコマンドがパッケージ固有のフラグを正しく処理できるようになりました。

また、arg := args[i]という行が削除されていますが、これはargs[i]を直接passToTestに渡すことで、一時変数argが不要になったためです。これはコードの簡潔化にも貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(go testコマンド、スライス、append関数に関する一般的な情報)
  • Go言語のソースコード(src/cmd/go/testflag.goのコンテキスト理解のため)
  • Web検索("golang cmd/go testflag.go infinite loop")