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

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

このコミットで変更されたファイルは src/cmd/go/test.go です。このファイルはGo言語の公式ツールチェインの一部であり、go test コマンドのテスト実行ロジックを実装しています。具体的には、テストプロセスの起動、実行、タイムアウト処理、結果の収集といった、テスト実行の核心部分を担っています。

コミット

このコミットは、テストプロセスが起動に失敗した場合にタイマーがリークする問題を修正します。

  • コミットハッシュ: b006cd9bb097c6b3a8cf7cebdb8067eef34957b1
  • Author: Dave Cheney dave@cheney.net
  • Date: Sat Jan 5 21:15:51 2013 +1100
  • コミットメッセージ:
    cmd/go: avoid leaking timer if test process failed to start
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7034047
    

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

https://github.com/golang/go/commit/b006cd9bb097c6b3a8cf7cebdb8067eef34957b1

元コミット内容

commit b006cd9bb097c6b3a8cf7cebdb8067eef34957b1
Author: Dave Cheney <dave@cheney.net>
Date:   Sat Jan 5 21:15:51 2013 +1100

    cmd/go: avoid leaking timer if test process failed to start
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7034047
---
 src/cmd/go/test.go | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n
diff --git a/src/cmd/go/test.go b/src/cmd/go/test.go
index 87ae571bd3..5d3f21e5e9 100644
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -642,8 +642,8 @@ func (b *builder) runTest(a *action) error {\n 	// This is a last-ditch deadline to detect and\n 	// stop wedged test binaries, to keep the builders\n 	// running.\n-\ttick := time.NewTimer(testKillTimeout)\n \tif err == nil {\n+\t\ttick := time.NewTimer(testKillTimeout)\n \t\tstartSigHandlers()\n \t\tdone := make(chan error)\n \t\tgo func() {\n@@ -660,8 +660,7 @@ func (b *builder) runTest(a *action) error {\n \t\ttick.Stop()\n \t}\n \tout := buf.Bytes()\n-\tt1 := time.Now()\n-\tt := fmt.Sprintf(\"%.3fs\", t1.Sub(t0).Seconds())\n+\tt := fmt.Sprintf(\"%.3fs\", time.Since(t0).Seconds())\n \tif err == nil {\n \t\tif testShowPass {\n \t\t\ta.testOutput.Write(out)\n```

## 変更の背景

このコミットの主な目的は、`go test` コマンドがテストバイナリの起動に失敗した場合に発生する可能性のあるリソースリーク(具体的には `time.Timer` のリーク)を回避することです。

`go test` コマンドは、テストの実行中にテストプロセスがハングアップするのを防ぐために、`testKillTimeout` というタイムアウトを設定し、`time.NewTimer` を使用してこのタイムアウトを監視していました。しかし、元のコードでは、テストプロセスを起動する前に `time.NewTimer` が無条件に呼び出されていました。

もしテストプロセスの起動自体が失敗した場合(例えば、テストバイナリが見つからない、実行権限がない、またはその他のシステムエラー)、`runTest` 関数はエラーを返して終了します。このとき、`time.NewTimer` によって作成されたタイマーは、`tick.Stop()` が呼び出されることなく、そのまま残されてしまいます。`time.NewTimer` は内部的に新しいゴルーチンを起動するため、`Stop()` が呼ばれない限り、このゴルーチンはタイマーのチャネルにイベントを送信し続けるか、ガベージコレクションの対象とならずにメモリ上に残り続ける可能性があります。これは、特にビルドシステムやCI/CD環境のように `go test` が頻繁に実行される状況で、時間の経過とともに不要なゴルーチンが蓄積され、リソースを消費し続ける「ゴルーチンリーク」を引き起こす原因となります。

このコミットは、タイマーの作成を、テストプロセスが正常に起動した場合のみに限定することで、このリーク問題を解決しています。

## 前提知識の解説

### Go言語の `time.Timer`

Go言語の `time` パッケージには、指定された時間が経過した後に単一のイベントを送信する `Timer` 型が提供されています。
- `time.NewTimer(d time.Duration)`: 指定された期間 `d` が経過した後に、タイマーのチャネル(`Timer.C`)に現在の時刻を送信する新しい `Timer` を作成し、返します。`NewTimer` を呼び出すと、内部的に新しいゴルーチンが起動され、タイマーのカウントダウンとチャネルへの送信を処理します。
- `Timer.Stop()`: タイマーが期限切れになるのを防ぎます。タイマーがまだ期限切れになっていない場合、`Stop` は `true` を返します。タイマーが既に期限切れになっているか、既に停止している場合、`Stop` は `false` を返します。`Stop()` を呼び出すことで、タイマーに関連付けられた内部ゴルーチンが終了し、リソースが解放されます。`Stop()` を呼び出さないと、タイマーのゴルーチンがリークする可能性があります。

### ゴルーチンとリソースリーク

Go言語のゴルーチン(goroutine)は、軽量な並行実行単位です。Goランタイムによって管理され、OSのスレッドよりもはるかに少ないメモリで起動できます。しかし、ゴルーチンが適切に終了しない場合、そのゴルーチンが使用しているメモリやその他のリソースが解放されずに残り続けることがあります。これを「ゴルーチンリーク」と呼びます。ゴルーチンリークは、アプリケーションのメモリ使用量を徐々に増加させ、最終的にはメモリ不足やパフォーマンス低下を引き起こす可能性があります。`time.Timer` のように内部的にゴルーチンを使用する機能では、`Stop()` メソッドなどを適切に呼び出してゴルーチンを終了させることが重要です。

### `go test` コマンド

`go test` は、Go言語の標準的なテスト実行ツールです。ソースコード内のテスト関数(`TestXxx`、`BenchmarkXxx`、`ExampleXxx` など)を自動的に検出し、テストバイナリをコンパイルして実行します。このコマンドは、開発者がコードの品質を維持し、リグレッションを防ぐために不可欠なツールです。`src/cmd/go/test.go` は、この `go test` コマンドの内部実装の一部です。

### `cmd/go` パッケージ

`cmd/go` パッケージは、Go言語のコマンドラインツール `go` の主要な実装を含んでいます。`go build`、`go run`、`go install`、`go test` など、Go開発者が日常的に使用する様々なサブコマンドのロジックがここに集約されています。

## 技術的詳細

このコミットの技術的な核心は、`time.NewTimer` の呼び出し位置の変更と、`time.Since` の利用による時間の計測方法の改善です。

1.  **`time.NewTimer` の呼び出し位置の変更**:
    *   元のコードでは、`tick := time.NewTimer(testKillTimeout)` は `if err == nil` ブロックの外にありました。これは、テストプロセスの起動結果(`err` の値)に関わらず、常にタイマーが作成されることを意味します。
    *   `testKillTimeout` は、テストバイナリがハングアップした場合に強制終了するための「最終手段のデッドライン」として設定されていました。
    *   変更後、`tick := time.NewTimer(testKillTimeout)` は `if err == nil` ブロックの内部に移動されました。これにより、タイマーは「テストプロセスが正常に起動した場合(`err` が `nil` の場合)」にのみ作成されるようになります。
    *   もし `err` が `nil` でない場合(テストプロセスの起動に失敗した場合)、タイマーは作成されず、したがって `tick.Stop()` を呼び出す必要もなくなります。これにより、不要なタイマーゴルーチンのリークが防止されます。

2.  **時間の計測方法の改善**:
    *   元のコードでは、テスト実行時間の計測に `t1 := time.Now()` と `t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())` を使用していました。これは、`t0`(テスト開始時刻)から `t1`(テスト終了時刻)までの差分を計算しています。
    *   変更後、`t := fmt.Sprintf("%.3fs", time.Since(t0).Seconds())` に置き換えられました。`time.Since(t)` は `time.Now().Sub(t)` の糖衣構文(シンタックスシュガー)であり、より簡潔でGoらしい書き方です。この変更はリーク修正とは直接関係ありませんが、コードの可読性とGoのイディオムへの準拠を向上させます。

この修正は、Goの標準ライブラリやツールにおけるリソース管理の重要性を示しています。特に、並行処理を多用するGoでは、ゴルーチンのライフサイクル管理が適切に行われないと、予期せぬリソース消費やパフォーマンス問題につながる可能性があるため、このような細かな修正がシステムの安定性に寄与します。

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

```diff
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -642,8 +642,8 @@ func (b *builder) runTest(a *action) error {\n 	// This is a last-ditch deadline to detect and\n 	// stop wedged test binaries, to keep the builders\n 	// running.\n-\ttick := time.NewTimer(testKillTimeout)\n \tif err == nil {\n+\t\ttick := time.NewTimer(testKillTimeout)\n \t\tstartSigHandlers()\n \t\tdone := make(chan error)\n \t\tgo func() {\n@@ -660,8 +660,7 @@ func (b *builder) runTest(a *action) error {\n \t\ttick.Stop()\n \t}\n \tout := buf.Bytes()\n-\tt1 := time.Now()\n-\tt := fmt.Sprintf(\"%.3fs\", t1.Sub(t0).Seconds())\n+\tt := fmt.Sprintf(\"%.3fs\", time.Since(t0).Seconds())\n \tif err == nil {\n \t\tif testShowPass {\n \t\t\ta.testOutput.Write(out)\n```

## コアとなるコードの解説

### 1. `time.NewTimer` の移動

```diff
-	tick := time.NewTimer(testKillTimeout)
 	if err == nil {
+		tick := time.NewTimer(testKillTimeout)
  • 変更前: tick := time.NewTimer(testKillTimeout)if err == nil のブロックの外にありました。これは、runTest 関数が呼び出されると、テストプロセスの起動が成功したかどうかにかかわらず、常に新しいタイマーが作成されることを意味します。もし errnil でなく(テストプロセスの起動に失敗し)、if err == nil ブロックがスキップされた場合、タイマーは作成されたまま tick.Stop() が呼び出されず、関連するゴルーチンがリークする可能性がありました。
  • 変更後: tick := time.NewTimer(testKillTimeout)if err == nil ブロックの内部に移動されました。これにより、タイマーはテストプロセスが正常に起動し、実際に監視が必要な場合にのみ作成されるようになります。テストプロセスの起動に失敗した場合は、タイマー自体が作成されないため、リークの心配がなくなります。これは、リソースを必要なときにだけ割り当てるという良いプラクティスに沿っています。

2. 時間計測の改善

-	t1 := time.Now()
-	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())
+	t := fmt.Sprintf("%.3fs", time.Since(t0).Seconds())
  • 変更前: t1 := time.Now() で現在の時刻を再度取得し、t1.Sub(t0)t0(テスト開始時刻)からの経過時間を計算していました。これは機能的には正しいですが、少し冗長です。
  • 変更後: time.Since(t0) を使用するように変更されました。time.Since(t)time.Now().Sub(t) と同等であり、より簡潔でGoのイディオムに沿った書き方です。この変更は、タイマーリークの修正とは直接関係ありませんが、コードの可読性と保守性を向上させます。

これらの変更により、go test コマンドの堅牢性が向上し、特に自動ビルド環境などでの長期的な安定性が確保されます。

関連リンク

参考にした情報源リンク

  • Go言語の time パッケージに関する公式ドキュメント: https://pkg.go.dev/time
  • Go言語のゴルーチンと並行処理に関する一般的な情報源(例: Go公式ブログ、Effective Goなど)
  • go test コマンドに関する公式ドキュメント: https://pkg.go.dev/cmd/go#hdr-Test_packages
  • Go言語におけるリソースリーク、特にゴルーチンリークに関する一般的な解説記事。I have provided the detailed explanation as requested.