[インデックス 19263] ファイルの概要
このコミットは、Go言語のテスト実行時に作成される一時ディレクトリのクリーンアップに関するバグ修正です。go test
コマンドがbuilder.init()
を複数回呼び出す可能性があり、そのたびに新しい作業ディレクトリが作成される問題に対応しています。これにより、以前の作業ディレクトリが適切にクリーンアップされない可能性がありました。この変更は、atexit
ハンドラに渡す作業ディレクトリのスコープを修正することで、すべての作業ディレクトリが確実にクリーンアップされるようにします。
コミット
commit 61fac6845a6c35253397c66bcc71a309f59d8c70
Author: Michael Fraenkel <michael.fraenkel@gmail.com>
Date: Wed Apr 30 13:03:38 2014 -0400
cmd/go: test: clean up all temporary directories
go test may call builder.init() multiple times which will create a new work directory. The cleanup needs to hoist the current work directory.
Fixes #7904.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/95900044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/61fac6845a6c35253397c66bcc71a309f59d8c70
元コミット内容
cmd/go: test: clean up all temporary directories
go test may call builder.init() multiple times which will create a new work directory. The cleanup needs to hoist the current work directory.
Fixes #7904.
変更の背景
このコミットの背景には、go test
コマンドが一時ファイルを生成する際のクリーンアップ処理に関する問題がありました。Goのテストフレームワークは、テスト実行のために一時的な作業ディレクトリを作成することがあります。しかし、特定のシナリオ、特にbuilder.init()
関数が複数回呼び出されるような場合、新しい作業ディレクトリが作成されるたびに、以前に作成された作業ディレクトリが適切に追跡されず、クリーンアップされないという問題が発生していました。
具体的には、go test
の実行中にbuilder.init()
が複数回呼び出されると、b.work
フィールドが新しい作業ディレクトリのパスで上書きされていました。しかし、クリーンアップのために登録されるatexit
ハンドラは、その登録時点でのb.work
の値をキャプチャしていませんでした。その結果、atexit
ハンドラが実行される際には、最後に設定されたb.work
の値(つまり、最後に作成された作業ディレクトリ)しか参照できず、それ以前に作成された作業ディレクトリがシステム上に残ってしまうというメモリリークならぬディスクスペースリークが発生していました。
この問題は、Fixes #7904
として参照されているGoのイシュートラッカーで報告されたバグに対応するものです。一時ディレクトリが残存することは、ディスクスペースの消費だけでなく、テストの再現性や環境のクリーンさを損なう可能性がありました。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連システムの概念を理解しておく必要があります。
go test
コマンド: Go言語のテストを実行するための標準コマンドです。テストのビルド、実行、結果の表示を行います。テスト実行時には、一時的なビルド成果物や作業ファイルが生成されることがあります。cmd/go
パッケージ: Goコマンドラインツール(go build
,go run
,go test
など)の実装が含まれるパッケージです。Goのビルドシステムの中核を担っています。builder
構造体:cmd/go
パッケージ内で、ビルドプロセスを管理するための内部的な構造体です。一時ディレクトリの作成や管理もこの構造体が行います。b.work
フィールド:builder
構造体内のフィールドで、現在のビルドまたはテスト実行のための一時作業ディレクトリのパスを保持します。builder.init()
関数:builder
構造体の初期化を行う関数です。この関数が呼び出されると、必要に応じて新しい一時作業ディレクトリが作成され、b.work
にそのパスが設定されます。atexit
関数: Go言語の標準ライブラリには直接的なatexit
関数は存在しませんが、この文脈では、プログラムの終了時に実行されるクリーンアップ処理を登録するためのメカニズムを指しています。C言語のatexit
関数と同様に、プログラムが正常終了する際に登録された関数が呼び出されます。Goのcmd/go
内部では、defer
やシグナルハンドラ、あるいは独自の終了フックメカニズムを用いて同様の機能を実現していると考えられます。ここでは、os.RemoveAll(b.work)
をプログラム終了時に実行するように登録していることを意味します。- クロージャと変数のキャプチャ: Go言語(および他の多くの言語)におけるクロージャは、それが定義された環境の変数を「キャプチャ」します。変数が値渡しでキャプチャされるか、参照渡しでキャプチャされるかは言語のセマンティクスによります。このバグは、
atexit
ハンドラに渡される無名関数(クロージャ)が、b.work
変数を参照渡しでキャプチャしていたために発生しました。つまり、atexit
ハンドラが実行される時点でのb.work
の「現在の」値が使用されてしまい、登録時点のb.work
の値が保持されていなかったのです。
技術的詳細
このコミットは、src/cmd/go/build.go
ファイル内のbuilder.init()
関数における一時ディレクトリのクリーンアップロジックを修正しています。
元のコードでは、builder.init()
が呼び出された際に、buildWork
フラグがfalse
の場合(つまり、一時作業ディレクトリがユーザーによって指定されていない場合)、atexit
ハンドラが登録されていました。このハンドラは、プログラム終了時にos.RemoveAll(b.work)
を呼び出して、作成された一時ディレクトリを削除することを目的としていました。
問題は、atexit(func() { os.RemoveAll(b.work) })
という行にありました。ここで定義されている無名関数(クロージャ)は、b.work
変数を「参照」としてキャプチャしていました。これは、Goのクロージャが外部変数をキャプチャする際の一般的な動作です。したがって、builder.init()
が複数回呼び出され、そのたびにb.work
の値が新しい一時ディレクトリのパスに更新されると、以前に登録されたatexit
ハンドラも、その「参照」を通じて常にb.work
の「最新の」値を参照するようになっていました。
結果として、プログラム終了時にatexit
ハンドラが実行される際には、b.work
は最後に作成された一時ディレクトリのパスを指しており、それ以前に作成されたすべての一時ディレクトリはクリーンアップの対象から外れてしまっていたのです。
このコミットによる修正は、このクロージャのキャプチャ動作を明示的に変更することで問題を解決しています。具体的には、atexit
ハンドラを登録する直前に、b.work
の現在の値を新しいローカル変数workdir
にコピーしています。
workdir := b.work
atexit(func() { os.RemoveAll(workdir) })
このようにすることで、atexit
ハンドラに渡される無名関数は、b.work
の参照ではなく、その時点でのb.work
の「値」がコピーされたローカル変数workdir
をキャプチャするようになります。これにより、builder.init()
が後で再度呼び出されてb.work
が更新されたとしても、すでに登録されたatexit
ハンドラは、それぞれが登録された時点での正しい一時ディレクトリのパスを保持し続けることができます。
この修正により、go test
の実行中に複数の一時作業ディレクトリが作成された場合でも、プログラム終了時にはそれらすべてが適切にクリーンアップされることが保証されます。
コアとなるコードの変更箇所
変更はsrc/cmd/go/build.go
ファイル内のbuilder.init()
関数にあります。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -447,7 +447,8 @@ func (b *builder) init() {
fmt.Fprintf(os.Stderr, "WORK=%s\\n", b.work)
}
if !buildWork {
- atexit(func() { os.RemoveAll(b.work) })
+ workdir := b.work
+ atexit(func() { os.RemoveAll(workdir) })
}
}
}
コアとなるコードの解説
変更されたのは以下の部分です。
変更前:
atexit(func() { os.RemoveAll(b.work) })
この行では、atexit
関数に渡される無名関数が、builder
構造体のフィールドであるb.work
を直接参照していました。Goのクロージャのセマンティクスにより、このb.work
は参照渡しでキャプチャされます。つまり、無名関数が実行される時点でのb.work
の「現在の」値が使用されます。builder.init()
が複数回呼び出されるとb.work
は更新されるため、古いb.work
の値が指す一時ディレクトリはクリーンアップされませんでした。
変更後:
workdir := b.work
atexit(func() { os.RemoveAll(workdir) })
この修正では、atexit
に登録する前に、b.work
の現在の値を新しいローカル変数workdir
に代入しています。Goでは、変数の代入は値渡しで行われます(ポインタ型でない限り)。したがって、workdir
はb.work
のその時点での文字列値のコピーを受け取ります。
そして、atexit
に渡される無名関数は、このローカル変数workdir
をキャプチャします。このworkdir
は、無名関数が定義された時点でのb.work
の値を保持しており、その後のb.work
の変更には影響されません。これにより、atexit
ハンドラが実行される際には、それぞれが登録された時点での正しい一時ディレクトリのパスがworkdir
を通じて参照され、確実にクリーンアップされるようになります。
この変更は、Goのクロージャが外部変数をキャプチャする際のセマンティクスを正確に理解し、意図した動作を実現するための典型的なパターンを示しています。
関連リンク
- Go Issue #7904 (参照されているが、正確な公式イシュートラッカーのリンクは特定できなかったため、一般的なGoのイシュートラッカーを参照)
- Goのイシュートラッカー: https://github.com/golang/go/issues
- Gerrit Code Review (Goプロジェクトのコードレビューシステム):
- このコミットのGerritリンク: https://golang.org/cl/95900044
参考にした情報源リンク
- Go言語の公式ドキュメント (クロージャ、
os
パッケージなど) - Go言語のソースコード (
src/cmd/go/build.go
) - Goのイシュートラッカー (一般的な情報源として)
- Gerrit Code Review (Goプロジェクトのコードレビューシステム)
- Go言語におけるクロージャの変数キャプチャに関する一般的な知識```markdown
[インデックス 19263] ファイルの概要
このコミットは、Go言語のテスト実行時に作成される一時ディレクトリのクリーンアップに関するバグ修正です。go test
コマンドがbuilder.init()
を複数回呼び出す可能性があり、そのたびに新しい作業ディレクトリが作成される問題に対応しています。これにより、以前の作業ディレクトリが適切にクリーンアップされない可能性がありました。この変更は、atexit
ハンドラに渡す作業ディレクトリのスコープを修正することで、すべての作業ディレクトリが確実にクリーンアップされるようにします。
コミット
commit 61fac6845a6c35253397c66bcc71a309f59d8c70
Author: Michael Fraenkel <michael.fraenkel@gmail.com>
Date: Wed Apr 30 13:03:38 2014 -0400
cmd/go: test: clean up all temporary directories
go test may call builder.init() multiple times which will create a new work directory. The cleanup needs to hoist the current work directory.
Fixes #7904.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/95900044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/61fac6845a6c35253397c66bcc71a309f59d8c70
元コミット内容
cmd/go: test: clean up all temporary directories
go test may call builder.init() multiple times which will create a new work directory. The cleanup needs to hoist the current work directory.
Fixes #7904.
変更の背景
このコミットの背景には、go test
コマンドが一時ファイルを生成する際のクリーンアップ処理に関する問題がありました。Goのテストフレームワークは、テスト実行のために一時的な作業ディレクトリを作成することがあります。しかし、特定のシナリオ、特にbuilder.init()
関数が複数回呼び出されるような場合、新しい作業ディレクトリが作成されるたびに、以前に作成された作業ディレクトリが適切に追跡されず、クリーンアップされないという問題が発生していました。
具体的には、go test
の実行中にbuilder.init()
が複数回呼び出されると、b.work
フィールドが新しい作業ディレクトリのパスで上書きされていました。しかし、クリーンアップのために登録されるatexit
ハンドラは、その登録時点でのb.work
の値をキャプチャしていませんでした。その結果、atexit
ハンドラが実行される際には、最後に設定されたb.work
の値(つまり、最後に作成された作業ディレクトリ)しか参照できず、それ以前に作成された作業ディレクトリがシステム上に残ってしまうというメモリリークならぬディスクスペースリークが発生していました。
この問題は、Fixes #7904
として参照されているGoのイシュートラッカーで報告されたバグに対応するものです。一時ディレクトリが残存することは、ディスクスペースの消費だけでなく、テストの再現性や環境のクリーンさを損なう可能性がありました。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連システムの概念を理解しておく必要があります。
go test
コマンド: Go言語のテストを実行するための標準コマンドです。テストのビルド、実行、結果の表示を行います。テスト実行時には、一時的なビルド成果物や作業ファイルが生成されることがあります。cmd/go
パッケージ: Goコマンドラインツール(go build
,go run
,go test
など)の実装が含まれるパッケージです。Goのビルドシステムの中核を担っています。builder
構造体:cmd/go
パッケージ内で、ビルドプロセスを管理するための内部的な構造体です。一時ディレクトリの作成や管理もこの構造体が行います。b.work
フィールド:builder
構造体内のフィールドで、現在のビルドまたはテスト実行のための一時作業ディレクトリのパスを保持します。builder.init()
関数:builder
構造体の初期化を行う関数です。この関数が呼び出されると、必要に応じて新しい一時作業ディレクトリが作成され、b.work
にそのパスが設定されます。atexit
関数: Go言語の標準ライブラリには直接的なatexit
関数は存在しませんが、この文脈では、プログラムの終了時に実行されるクリーンアップ処理を登録するためのメカニズムを指しています。C言語のatexit
関数と同様に、プログラムが正常終了する際に登録された関数が呼び出されます。Goのcmd/go
内部では、defer
やシグナルハンドラ、あるいは独自の終了フックメカニズムを用いて同様の機能を実現していると考えられます。ここでは、os.RemoveAll(b.work)
をプログラム終了時に実行するように登録していることを意味します。- クロージャと変数のキャプチャ: Go言語(および他の多くの言語)におけるクロージャは、それが定義された環境の変数を「キャプチャ」します。変数が値渡しでキャプチャされるか、参照渡しでキャプチャされるかは言語のセマンティクスによります。このバグは、
atexit
ハンドラに渡される無名関数(クロージャ)が、b.work
変数を参照渡しでキャプチャしていたために発生しました。つまり、atexit
ハンドラが実行される時点でのb.work
の「現在の」値が使用されてしまい、登録時点のb.work
の値が保持されていなかったのです。
技術的詳細
このコミットは、src/cmd/go/build.go
ファイル内のbuilder.init()
関数における一時ディレクトリのクリーンアップロジックを修正しています。
元のコードでは、builder.init()
が呼び出された際に、buildWork
フラグがfalse
の場合(つまり、一時作業ディレクトリがユーザーによって指定されていない場合)、atexit
ハンドラが登録されていました。このハンドラは、プログラム終了時にos.RemoveAll(b.work)
を呼び出して、作成された一時ディレクトリを削除することを目的としていました。
問題は、atexit(func() { os.RemoveAll(b.work) })
という行にありました。ここで定義されている無名関数(クロージャ)は、b.work
変数を「参照」としてキャプチャしていました。これは、Goのクロージャが外部変数をキャプチャする際の一般的な動作です。したがって、builder.init()
が複数回呼び出され、そのたびにb.work
の値が新しい一時ディレクトリのパスに更新されると、以前に登録されたatexit
ハンドラも、その「参照」を通じて常にb.work
の「最新の」値を参照するようになっていました。
結果として、プログラム終了時にatexit
ハンドラが実行される際には、b.work
は最後に作成された一時ディレクトリのパスを指しており、それ以前に作成されたすべての一時ディレクトリはクリーンアップの対象から外れてしまっていたのです。
このコミットによる修正は、このクロージャのキャプチャ動作を明示的に変更することで問題を解決しています。具体的には、atexit
ハンドラを登録する直前に、b.work
の現在の値を新しいローカル変数workdir
にコピーしています。
workdir := b.work
atexit(func() { os.RemoveAll(workdir) })
このようにすることで、atexit
ハンドラに渡される無名関数は、b.work
の参照ではなく、その時点でのb.work
の「値」がコピーされたローカル変数workdir
をキャプチャするようになります。これにより、builder.init()
が後で再度呼び出されてb.work
が更新されたとしても、すでに登録されたatexit
ハンドラは、それぞれが登録された時点での正しい一時ディレクトリのパスを保持し続けることができます。
この修正により、go test
の実行中に複数の一時作業ディレクトリが作成された場合でも、プログラム終了時にはそれらすべてが適切にクリーンアップされることが保証されます。
コアとなるコードの変更箇所
変更はsrc/cmd/go/build.go
ファイル内のbuilder.init()
関数にあります。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -447,7 +447,8 @@ func (b *builder) init() {
fmt.Fprintf(os.Stderr, "WORK=%s\\n", b.work)
}
if !buildWork {
- atexit(func() { os.RemoveAll(b.work) })
+ workdir := b.work
+ atexit(func() { os.RemoveAll(workdir) })
}
}
}
コアとなるコードの解説
変更されたのは以下の部分です。
変更前:
atexit(func() { os.RemoveAll(b.work) })
この行では、atexit
関数に渡される無名関数が、builder
構造体のフィールドであるb.work
を直接参照していました。Goのクロージャのセマンティクスにより、このb.work
は参照渡しでキャプチャされます。つまり、無名関数が実行される時点でのb.work
の「現在の」値が使用されます。builder.init()
が複数回呼び出されるとb.work
は更新されるため、古いb.work
の値が指す一時ディレクトリはクリーンアップされませんでした。
変更後:
workdir := b.work
atexit(func() { os.RemoveAll(workdir) })
この修正では、atexit
に登録する前に、b.work
の現在の値を新しいローカル変数workdir
に代入しています。Goでは、変数の代入は値渡しで行われます(ポインタ型でない限り)。したがって、workdir
はb.work
のその時点での文字列値のコピーを受け取ります。
そして、atexit
に渡される無名関数は、このローカル変数workdir
をキャプチャします。このworkdir
は、無名関数が定義された時点でのb.work
の値を保持しており、その後のb.work
の変更には影響されません。これにより、atexit
ハンドラが実行される際には、それぞれが登録された時点での正しい一時ディレクトリのパスがworkdir
を通じて参照され、確実にクリーンアップされるようになります。
この変更は、Goのクロージャが外部変数をキャプチャする際のセマンティクスを正確に理解し、意図した動作を実現するための典型的なパターンを示しています。
関連リンク
- Go Issue #7904 (参照されているが、正確な公式イシュートラッカーのリンクは特定できなかったため、一般的なGoのイシュートラッカーを参照)
- Goのイシュートラッカー: https://github.com/golang/go/issues
- Gerrit Code Review (Goプロジェクトのコードレビューシステム):
- このコミットのGerritリンク: https://golang.org/cl/95900044
参考にした情報源リンク
- Go言語の公式ドキュメント (クロージャ、
os
パッケージなど) - Go言語のソースコード (
src/cmd/go/build.go
) - Goのイシュートラッカー (一般的な情報源として)
- Gerrit Code Review (Goプロジェクトのコードレビューシステム)
- Go言語におけるクロージャの変数キャプチャに関する一般的な知識