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

[インデックス 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では、変数の代入は値渡しで行われます(ポインタ型でない限り)。したがって、workdirb.workのその時点での文字列値のコピーを受け取ります。

そして、atexitに渡される無名関数は、このローカル変数workdirをキャプチャします。このworkdirは、無名関数が定義された時点でのb.workの値を保持しており、その後のb.workの変更には影響されません。これにより、atexitハンドラが実行される際には、それぞれが登録された時点での正しい一時ディレクトリのパスがworkdirを通じて参照され、確実にクリーンアップされるようになります。

この変更は、Goのクロージャが外部変数をキャプチャする際のセマンティクスを正確に理解し、意図した動作を実現するための典型的なパターンを示しています。

関連リンク

  • Go Issue #7904 (参照されているが、正確な公式イシュートラッカーのリンクは特定できなかったため、一般的なGoのイシュートラッカーを参照)
  • Gerrit Code Review (Goプロジェクトのコードレビューシステム):

参考にした情報源リンク

  • 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では、変数の代入は値渡しで行われます(ポインタ型でない限り)。したがって、workdirb.workのその時点での文字列値のコピーを受け取ります。

そして、atexitに渡される無名関数は、このローカル変数workdirをキャプチャします。このworkdirは、無名関数が定義された時点でのb.workの値を保持しており、その後のb.workの変更には影響されません。これにより、atexitハンドラが実行される際には、それぞれが登録された時点での正しい一時ディレクトリのパスがworkdirを通じて参照され、確実にクリーンアップされるようになります。

この変更は、Goのクロージャが外部変数をキャプチャする際のセマンティクスを正確に理解し、意図した動作を実現するための典型的なパターンを示しています。

関連リンク

  • Go Issue #7904 (参照されているが、正確な公式イシュートラッカーのリンクは特定できなかったため、一般的なGoのイシュートラッカーを参照)
  • Gerrit Code Review (Goプロジェクトのコードレビューシステム):

参考にした情報源リンク

  • Go言語の公式ドキュメント (クロージャ、osパッケージなど)
  • Go言語のソースコード (src/cmd/go/build.go)
  • Goのイシュートラッカー (一般的な情報源として)
  • Gerrit Code Review (Goプロジェクトのコードレビューシステム)
  • Go言語におけるクロージャの変数キャプチャに関する一般的な知識