[インデックス 12614] ファイルの概要
このコミットは、Go言語の標準ライブラリである go/build
パッケージ内の match
関数のバグを修正するものです。具体的には、ビルドタグの否定 (!
) を処理する際に発生しうるパニック(プログラムの異常終了)を防ぐための変更と、ビルドタグのリストの区切り文字に関するコメントの修正が含まれています。
コミット
commit 5361712ab4f582fda6c098a45d270278b7907404
Author: Maxim Pimenov <mpimenov@google.com>
Date: Tue Mar 13 10:00:43 2012 -0400
go/build: fix match
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5801043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5361712ab4f582fda6c098a45d270278b7907404
元コミット内容
src/pkg/go/build/build.go | 6 +++---\n src/pkg/go/build/build_test.go | 1 +\n 2 files changed, 4 insertions(+), 3 deletions(-)\n\ndiff --git a/src/pkg/go/build/build.go b/src/pkg/go/build/build.go\nindex dc9dcd1d65..d2dbb58a1c 100644\n--- a/src/pkg/go/build/build.go\n+++ b/src/pkg/go/build/build.go\n@@ -874,7 +874,7 @@ func splitQuoted(s string) (r []string, err error) {\n //\t!cgo (if cgo is disabled)\n //\ttag (if tag is listed in ctxt.BuildTags)\n //\t!tag (if tag is not listed in ctxt.BuildTags)\n-//\ta slash-separated list of any of these\n+//\ta comma-separated list of any of these\n //\n func (ctxt *Context) match(name string) bool {\n \tif name == \"\" {\n@@ -888,11 +888,11 @@ func (ctxt *Context) match(name string) bool {\n \t\treturn false\n \t}\n \tif strings.HasPrefix(name, \"!\") { // negation\n-\t\treturn !ctxt.match(name[1:])\n+\t\treturn len(name) > 1 && !ctxt.match(name[1:])\n \t}\n \n \t// Tags must be letters, digits, underscores.\n-\t// Unlike in Go identifiers, all digits is fine (e.g., \"386\").\n+\t// Unlike in Go identifiers, all digits are fine (e.g., \"386\").\n \tfor _, c := range name {\n \t\tif !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != \'_\' {\n \t\t\treturn false\ndiff --git a/src/pkg/go/build/build_test.go b/src/pkg/go/build/build_test.go\nindex 06b8b0e94f..560ebad5c9 100644\n--- a/src/pkg/go/build/build_test.go\n+++ b/src/pkg/go/build/build_test.go\n@@ -36,6 +36,7 @@ func TestMatch(t *testing.T) {\n \tnomatch(runtime.GOOS + \",\" + runtime.GOARCH + \",!foo\")\n \tmatch(runtime.GOOS + \",\" + runtime.GOARCH + \",!bar\")\n \tnomatch(runtime.GOOS + \",\" + runtime.GOARCH + \",bar\")\n+\tnomatch(\"!\")\n }\n \n func TestDotSlashImport(t *testing.T) {\n```
## 変更の背景
このコミットは、Go言語のビルドシステムがGoソースファイルを解析する際に使用する `go/build` パッケージの `match` 関数における潜在的なランタイムパニック(panic)を修正するために行われました。
`match` 関数は、Goのビルド制約(build constraints)やビルドタグ(build tags)を評価する役割を担っています。ビルド制約は、特定の環境(OS、アーキテクチャなど)やカスタムタグに基づいて、どのファイルを含めるか、または除外するかを決定するためにGoソースファイルの先頭に記述される特殊なコメントです。例えば、`// +build linux,amd64` はLinuxかつAMD64アーキテクチャの場合にのみファイルをビルドすることを示し、`// +build !debug` は`debug`タグが有効でない場合にビルドすることを示します。
問題は、`match` 関数が否定 (`!`) プレフィックスを持つタグを処理する際に発生しました。もし `match` 関数に渡される `name` 引数が単一の感嘆符 `!` だけであった場合、`name[1:]` というスライス操作は空の文字列に対して行われ、これがランタイムパニックを引き起こす可能性がありました。これは、Goのスライス操作が範囲外アクセスに対して厳格であるためです。
また、関連するコメントの修正は、ビルドタグのリストがスラッシュ区切りではなく、カンマ区切りであることを明確にするためのものです。これは、ドキュメントと実際の動作の整合性を保つための修正と考えられます。
## 前提知識の解説
### Goのビルド制約とビルドタグ
Go言語では、ソースファイルのコンパイル時に特定の条件に基づいてファイルを含めたり除外したりするメカニズムとして「ビルド制約(build constraints)」または「ビルドタグ(build tags)」が提供されています。これらは、クロスプラットフォーム開発や特定の機能の有効/無効化に非常に役立ちます。
* **構文**: Goのソースファイルの先頭に `// +build tag1,tag2 !tag3` のような形式で記述します。
* `// +build` で始まり、その後にスペースが続く必要があります。
* タグはスペースで区切られたリストとして扱われます。
* 各リスト内のタグはカンマ (` , `) で区切られたAND条件として評価されます。
* 異なるリストはOR条件として評価されます。
* タグの前に `!` を付けると否定(NOT)条件になります。
* **例**:
* `// +build linux,amd64` : LinuxかつAMD64アーキテクチャの場合にのみビルド。
* `// +build debug` : `debug`タグが有効な場合にのみビルド。
* `// +build !windows` : Windows以外のOSの場合にのみビルド。
* `// +build go1.18` : Go 1.18以降のバージョンでビルド。
* **組み込みタグ**: Goには、`linux`, `windows`, `darwin` などのOS名、`amd64`, `arm`, `386` などのアーキテクチャ名、`cgo`(Cgoが有効な場合)、`go1.x`(Goのバージョン)といった組み込みのビルドタグがあります。
* **カスタムタグ**: `go build -tags "mytag"` のように `-tags` フラグを使用して独自のカスタムタグを指定することもできます。
### `go/build` パッケージ
`go/build` パッケージは、Goのソースコードを解析し、パッケージの依存関係を解決し、ビルド制約を評価するための機能を提供します。`go build` コマンドやGoのIDEツールなどが内部的にこのパッケージを利用しています。
* **`Context` 構造体**: ビルド環境に関する情報(OS、アーキテクチャ、ビルドタグなど)を保持します。
* **`match` メソッド**: `Context` 構造体のメソッドであり、特定のビルドタグ文字列が現在のビルドコンテキストに合致するかどうかを評価します。このコミットで修正されたのがこのメソッドです。
### Goにおけるパニック(Panic)
Go言語におけるパニックは、プログラムの実行中に回復不可能なエラーが発生した際に、通常の実行フローを停止させるメカニズムです。これは、C++の例外やJavaの未捕捉例外に似ています。パニックが発生すると、defer関数が実行された後、プログラムは終了します。このコミットで修正された問題は、`name[1:]` のようなスライス操作が、空の文字列に対して行われた場合に発生する「インデックス範囲外」のパニックでした。
## 技術的詳細
このコミットの技術的な核心は、`go/build` パッケージの `Context.match` メソッドにおける文字列スライスの安全性の確保と、ドキュメンテーションの正確性の向上です。
### `Context.match` メソッドの修正
元のコードでは、ビルドタグが否定 (`!`) で始まる場合、`name[1:]` を使用して感嘆符の後の文字列を取得し、それを再帰的に `match` 関数に渡していました。
```go
// 変更前
if strings.HasPrefix(name, "!") { // negation
return !ctxt.match(name[1:])
}
このコードの問題は、もし name
が単に !
という文字列であった場合、name[1:]
は空の文字列を生成します。Goのスライス操作は、s[low:high]
の形式で low <= high
かつ high <= len(s)
である必要があります。name
が !
の場合、len(name)
は1です。name[1:]
は name[1:len(name)]
と等価であり、name[1:1]
となります。これは有効なスライス操作であり、結果は空の文字列になります。
しかし、このコミットの修正は、len(name) > 1
という条件を追加しています。
// 変更後
if strings.HasPrefix(name, "!") { // negation
return len(name) > 1 && !ctxt.match(name[1:])
}
この変更の意図は、name
が !
の場合に ctxt.match(name[1:])
が呼び出されるのを防ぐことです。なぜなら、name[1:]
が空文字列を生成し、その空文字列が ctxt.match
に渡されると、match
関数内の他のロジックで問題が発生する可能性があったためです。特に、match
関数は空文字列を特別に処理し、false
を返すようになっています。
この len(name) > 1
の追加により、name
が !
の場合、len(name) > 1
は false
となり、!ctxt.match(name[1:])
の評価はスキップされ、結果として false
が返されます。これは、単一の !
は有効なビルドタグではないため、正しく「マッチしない」と判断されるべきというロジックに合致します。
コメントの修正
src/pkg/go/build/build.go
の874行目付近のコメントが修正されました。
- // a slash-separated list of any of these
+ // a comma-separated list of any of these
これは、ビルドタグのリストがスラッシュ (/
) ではなく、カンマ (,
) で区切られることを明確にするためのドキュメンテーションの修正です。Goのビルド制約の構文では、複数のタグをAND条件で結合する際にカンマを使用します(例: linux,amd64
)。この修正は、コードの動作とドキュメントの整合性を高めるものです。
また、891行目付近のコメントも修正されています。
- // Unlike in Go identifiers, all digits is fine (e.g., "386").
+ // Unlike in Go identifiers, all digits are fine (e.g., "386").
これは "is fine" から "are fine" への単純な文法的な修正であり、機能的な変更はありません。
テストケースの追加
src/pkg/go/build/build_test.go
に nomatch("!")
という新しいテストケースが追加されました。
// 変更後
nomatch("!")
このテストケースは、match
関数に単一の !
が渡された場合に、それがマッチしない(false
を返す)ことを検証します。これは、前述の len(name) > 1
の修正が意図通りに機能し、!
という不正な入力に対してパニックを起こさずに false
を返すことを保証するためのものです。
コアとなるコードの変更箇所
src/pkg/go/build/build.go
// ... (省略) ...
@@ -874,7 +874,7 @@ func splitQuoted(s string) (r []string, err error) {\n //\t!cgo (if cgo is disabled)\n //\ttag (if tag is listed in ctxt.BuildTags)\n //\t!tag (if tag is not listed in ctxt.BuildTags)\n-//\ta slash-separated list of any of these\n+//\ta comma-separated list of any of these\n //\n func (ctxt *Context) match(name string) bool {\n \tif name == "" {\n@@ -888,11 +888,11 @@ func (ctxt *Context) match(name string) bool {\n \t\treturn false\n \t}\n \tif strings.HasPrefix(name, "!") { // negation\n-\t\treturn !ctxt.match(name[1:])\n+\t\treturn len(name) > 1 && !ctxt.match(name[1:])\n \t}\n \n \t// Tags must be letters, digits, underscores.\n-\t// Unlike in Go identifiers, all digits is fine (e.g., "386").\n+\t// Unlike in Go identifiers, all digits are fine (e.g., "386").\n \tfor _, c := range name {\n \t\tif !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_'\n \t\t\treturn false
// ... (省略) ...
src/pkg/go/build/build_test.go
// ... (省略) ...
@@ -36,6 +36,7 @@ func TestMatch(t *testing.T) {\n \tnomatch(runtime.GOOS + "," + runtime.GOARCH + ",!foo")\n \tmatch(runtime.GOOS + "," + runtime.GOARCH + ",!bar")\n \tnomatch(runtime.GOOS + "," + runtime.GOARCH + ",bar")\n+\tnomatch("!")\n }\n \n func TestDotSlashImport(t *testing.T) {\n// ... (省略) ...
コアとなるコードの解説
src/pkg/go/build/build.go
の match
関数
この関数の目的は、与えられた name
(ビルドタグ文字列)が現在のビルドコンテキスト(ctxt
)に合致するかどうかをブール値で返すことです。
変更前のコードでは、name
が !
で始まる場合、name[1:]
を使って否定されるタグ名を取得し、そのタグ名がマッチしない場合に true
を返していました。しかし、name
が単に !
だった場合、name[1:]
は空文字列を返します。この空文字列が ctxt.match
に渡されると、match
関数内の他のロジック(例えば、空文字列に対する特別な処理がない場合)で予期せぬ動作やパニックを引き起こす可能性がありました。
変更後のコード return len(name) > 1 && !ctxt.match(name[1:])
は、この問題を解決します。
len(name) > 1
という条件が追加されました。- Goの論理AND演算子
&&
はショートサーキット評価を行います。つまり、左側のオペランドlen(name) > 1
がfalse
であれば、右側のオペランド!ctxt.match(name[1:])
は評価されません。 - したがって、
name
が!
の場合(len(name)
が1)、len(name) > 1
はfalse
となり、式全体がfalse
を返します。これにより、!
という不正な入力に対してmatch
関数が安全にfalse
を返すようになります。これは、単一の!
は有効なビルドタグではないため、正しい挙動です。
src/pkg/go/build/build_test.go
のテストケース
TestMatch
関数は、match
関数の様々な入力に対する挙動をテストします。
追加された nomatch("!")
は、match
関数に !
という文字列が渡されたときに、期待される結果が false
であることを検証します。これは、前述の build.go
の修正が正しく機能していることを確認するための回帰テストとして機能します。
関連リンク
- Go Modules and build constraints: https://go.dev/doc/go-build-constraints
go/build
package documentation: https://pkg.go.dev/go/build
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のビルド制約に関する一般的な情報源
- Go言語におけるパニックとエラーハンドリングに関する情報源