[インデックス 19156] ファイルの概要
このコミットは、Go言語の標準ライブラリos/exec
パッケージにおける2つの主要な変更を含んでいます。一つは、TestPipeLookPathLeak
テストが失敗した際により詳細な情報(lsof
の出力)を提供するように改善された点です。これは、特定の環境(特にlinux-386-387
)で発生していたテストの不安定性を診断するために行われました。もう一つは、closeOnce
構造体のClose
メソッドにおけるsync.Once
の利用方法を、より現代的でGoらしい「メソッド値」を使用する形にリファクタリングした点です。
コミット
commit fdade68379abdd9706881f4273e5f8cd9c0eb518
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Apr 15 17:36:25 2014 -0700
os/exec: make TestPipeLookPathLeak more verbose when it fails
Trying to understand the linux-386-387 failures:
http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814
Also modernize the closeOnce code with a method value, while I
was looking.
LGTM=adg
R=golang-codereviews, adg
CC=golang-codereviews, iant
https://golang.org/cl/87950044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fdade68379abdd9706881f4273e5f8cd9c0eb518
元コミット内容
このコミットは、os/exec
パッケージのテストと内部実装の改善を目的としています。具体的には、TestPipeLookPathLeak
テストがファイルディスクリプタのリークを検出した際に、より詳細なデバッグ情報(lsof
コマンドの出力)を報告するように変更されました。これにより、特にlinux-386-387
環境で発生していた原因不明のテスト失敗の診断が容易になります。
また、closeOnce
という内部ヘルパ構造体において、sync.Once
のDo
メソッドに渡すクロージャの代わりに、構造体のメソッドを直接「メソッド値」として渡すようにコードが現代化されました。これは機能的な変更ではなく、Goのイディオムに沿ったコードのクリーンアップです。
変更の背景
このコミットの主な背景には、Goのビルドシステムで報告されていたlinux-386-387
環境でのテスト失敗がありました。コミットメッセージに記載されているログリンク(http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814
)は、この問題の具体的な発生状況を示しています。linux-386-387
は、32ビットLinuxシステム、特に浮動小数点演算ユニット(FPU)を持つ古いIntel 386プロセッサをターゲットとする環境を指します。このような特定のアーキテクチャや環境では、ファイルディスクリプタのリークのような微妙な問題が顕在化しやすく、通常のテスト出力だけでは原因特定が困難な場合がありました。
TestPipeLookPathLeak
テストは、os/exec
パッケージが外部コマンドを実行する際に、パイプやファイルディスクリプタが適切に閉じられているかを確認するためのものです。このテストが失敗するということは、リソースリークの可能性を示唆しており、長期的にシステムの安定性やパフォーマンスに影響を与える可能性があります。そのため、失敗時のデバッグ情報を強化し、問題の根本原因を特定しやすくすることが急務でした。
また、closeOnce
のコードの現代化は、直接的なバグ修正というよりも、コードの可読性とGoのベストプラクティスへの準拠を目的としたものです。開発者がコードをレビューしている際に、より良い実装パターンに気づき、その場で改善を加えることは、オープンソースプロジェクトではよくあることです。
前提知識の解説
1. Go言語のos/exec
パッケージ
os/exec
パッケージは、Goプログラムから外部のシステムコマンドを実行するための機能を提供します。これにより、シェルスクリプトや他の実行可能ファイルをGoアプリケーションから起動し、その入出力(標準入力、標準出力、標準エラー)を制御することができます。
exec.Command(name string, arg ...string) *Cmd
: 指定されたコマンドと引数を持つCmd
構造体を生成します。Cmd
構造体: 実行するコマンドに関するすべての設定(パス、引数、環境変数、標準入出力のリダイレクトなど)を保持します。StdinPipe()
,StdoutPipe()
,StderrPipe()
: コマンドの標準入出力に接続するためのパイプ(io.WriteCloser
またはio.ReadCloser
)を返します。これにより、Goプログラムからコマンドにデータを書き込んだり、コマンドの出力を読み取ったりできます。Run()
,Output()
,CombinedOutput()
,Start()
,Wait()
: コマンドを実行するためのメソッド群です。LookPath(file string) (string, error)
:PATH
環境変数で指定されたディレクトリから実行可能ファイルを探し、その絶対パスを返します。
2. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プログラムがファイルを開いたり、パイプを作成したりすると、対応するファイルディスクリプタが割り当てられます。これらのディスクリプタは、使用後に適切に閉じられる必要があります。閉じ忘れると、ファイルディスクリプタのリークが発生し、システムのリソースを枯渇させたり、新しいファイルを開けなくなったりする可能性があります。
3. lsof
コマンド
lsof
(list open files) は、Unix系OSで実行中のプロセスが開いているファイルやネットワーク接続を一覧表示するためのコマンドラインユーティリティです。ファイルディスクリプタのリークを診断する際に非常に有用で、どのプロセスがどのファイルディスクリプタを保持しているかを詳細に確認できます。
4. sync.Once
Go言語のsync
パッケージに含まれるOnce
型は、特定の関数が一度だけ実行されることを保証するための同期プリミティブです。複数のゴルーチンが同時にOnce.Do(f func())
を呼び出したとしても、引数f
として渡された関数は一度だけ実行されます。これは、シングルトンの初期化や、コストの高いリソースの遅延初期化などによく使用されます。
5. メソッド値 (Method Value)
Go言語における「メソッド値」とは、特定のレシーバ(構造体のインスタンスなど)にバインドされたメソッドを関数として扱うことができる機能です。例えば、type T struct { ... }
という型にfunc (t T) MyMethod() { ... }
というメソッドがある場合、t := T{}
というインスタンスに対してt.MyMethod
と書くと、これはMyMethod
をt
というレシーバにバインドした関数値となります。この関数値は、func()
型の変数に代入したり、sync.Once.Do
のようにfunc()
型の引数を取る関数に直接渡したりすることができます。これにより、クロージャを明示的に書くよりも簡潔にコードを記述できます。
6. linux-386-387
環境
これは、Goのビルド環境における特定のターゲットアーキテクチャとCPUの組み合わせを指します。
linux
: オペレーティングシステムがLinuxであること。386
: CPUアーキテクチャがIntel 80386互換の32ビットであること。387
: これはGO386
環境変数の設定で、Intel 80387互換の浮動小数点演算ユニット(FPU)を使用することを意味します。古い32ビットシステムでは、FPUの有無や種類によってコンパイル時のコード生成が異なる場合がありました。
この環境は、現代の64ビットシステムが主流の現在ではレガシーなものですが、Goのようなクロスプラットフォーム言語では、このような多様な環境での動作保証も重要です。
技術的詳細
TestPipeLookPathLeak
の改善
元のTestPipeLookPathLeak
テストは、os/exec
パッケージがLookPath
やパイプ操作を行う際にファイルディスクリプタがリークしないことを検証していました。しかし、テストが失敗した場合、単に「リークしたファイルディスクリプタの数」しか報告せず、どのファイルディスクリプタがリークしているのか、その詳細な状況は不明でした。
このコミットでは、numOpenFDS
ヘルパ関数が変更され、開いているファイルディスクリプタの数だけでなく、lsof
コマンドの生出力も返すようになりました。そして、TestPipeLookPathLeak
がリークを検出した際に、このlsof
の出力をエラーメッセージに含めるように変更されました。
これにより、テストが失敗したビルドログから直接lsof
の出力を見ることができ、どのファイルが閉じられていないのか、あるいはどのプロセスが予期せずファイルディスクリプタを保持しているのかといった、具体的なデバッグ情報を得られるようになりました。これは、特に再現性の低い環境固有のバグ(例: linux-386-387
での問題)の診断において非常に有効です。
closeOnce
の現代化
closeOnce
構造体は、os/exec
パッケージ内でパイプなどのio.Closer
インターフェースを実装するオブジェクトが、複数回Close()
が呼ばれても一度だけしか閉じられないようにするためのヘルパです。これは、sync.Once
を使用して実現されていました。
元の実装では、closeOnce.Close()
メソッド内でc.close.Do(func() { c.closeErr = c.File.Close() })
というクロージャを使用していました。このクロージャは、c.File.Close()
の結果をc.closeErr
に代入するという処理を行っていました。
このコミットでは、この部分がc.once.Do(c.close)
とfunc (c *closeOnce) close() { c.err = c.File.Close() }
に変更されました。ここで注目すべきは、c.close
がcloseOnce
構造体のメソッドclose()
の「メソッド値」としてsync.Once.Do
に直接渡されている点です。
この変更は、機能的には全く同じ動作をしますが、コードの記述がより簡潔になり、Goのイディオムに沿ったものとなります。sync.Once.Do
はfunc()
型の引数を取るため、引数なしのメソッドをメソッド値として直接渡すことができます。これにより、余分なクロージャの定義が不要になり、コードがよりクリーンになります。
コアとなるコードの変更箇所
src/pkg/os/exec/exec.go
--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -429,15 +429,17 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
type closeOnce struct {
*os.File
- close sync.Once
- closeErr error
+ once sync.Once
+ err error
}
func (c *closeOnce) Close() error {
- c.close.Do(func() {
- c.closeErr = c.File.Close()
- })
- return c.closeErr
+ c.once.Do(c.close)
+ return c.err
+}
+
+func (c *closeOnce) close() {
+ c.err = c.File.Close()
}
// StdoutPipe returns a pipe that will be connected to the command's
src/pkg/os/exec/exec_test.go
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -214,7 +214,7 @@ func TestStdinClose(t *testing.T) {
// Issue 5071
func TestPipeLookPathLeak(t *testing.T) {
- fd0 := numOpenFDS(t)
+ fd0, lsof0 := numOpenFDS(t)
for i := 0; i < 4; i++ {
cmd := exec.Command("something-that-does-not-exist-binary")
cmd.StdoutPipe()
@@ -224,19 +224,19 @@ func TestPipeLookPathLeak(t *testing.T) {
if err == nil {
t.Fatal("unexpected success")
}
}
- fdGrowth := numOpenFDS(t) - fd0
+ open, lsof := numOpenFDS(t)
+ fdGrowth := open - fd0
if fdGrowth > 2 {
- t.Errorf("leaked %d fds; want ~0", fdGrowth)
+ t.Errorf("leaked %d fds; want ~0; have:\n%s\noriginally:\n%s", fdGrowth, lsof, lsof0)
}
}
-func numOpenFDS(t *testing.T) int {
+func numOpenFDS(t *testing.T) (n int, lsof []byte) {
lsof, err := exec.Command("lsof", "-n", "-p", strconv.Itoa(os.Getpid())).Output()
if err != nil {
t.Skip("skipping test; error finding or running lsof")
- return 0
}
- return bytes.Count(lsof, []byte("\n"))
+ return bytes.Count(lsof, []byte("\n")), lsof
}
var testedAlreadyLeaked = false
コアとなるコードの解説
src/pkg/os/exec/exec.go
の変更点
-
closeOnce
構造体のフィールド名変更:close sync.Once
がonce sync.Once
に変更されました。closeErr error
がerr error
に変更されました。 これは、より一般的な命名規則に合わせるための変更です。
-
closeOnce.Close()
メソッドの変更:- 変更前:
func (c *closeOnce) Close() error { c.close.Do(func() { c.closeErr = c.File.Close() }) return c.closeErr }
- 変更後:
func (c *closeOnce) Close() error { c.once.Do(c.close) // ここでメソッド値 c.close を渡す return c.err } func (c *closeOnce) close() { // 新しく追加されたヘルパメソッド c.err = c.File.Close() }
この変更の核心は、
sync.Once.Do
に渡す関数を、匿名クロージャからcloseOnce
構造体の新しいメソッドclose()
の「メソッド値」に変更した点です。c.once.Do(c.close)
とすることで、c.close
というメソッドがsync.Once
によって一度だけ実行されることが保証されます。このc.close
メソッドは、c.File.Close()
を呼び出し、その結果のエラーをc.err
フィールドに格納します。これにより、コードがより簡潔でGoのイディオムに沿ったものになりました。 - 変更前:
src/pkg/os/exec/exec_test.go
の変更点
-
TestPipeLookPathLeak
の変更:- 変更前:
fd0 := numOpenFDS(t)
- 変更後:
fd0, lsof0 := numOpenFDS(t)
テスト開始時に、初期のファイルディスクリプタ数だけでなく、lsof
の出力も取得するように変更されました。 - 同様に、ループ後のファイルディスクリプタ数取得も
open, lsof := numOpenFDS(t)
と変更され、現在のlsof
出力も取得します。 - エラーメッセージの変更:
変更前:
t.Errorf("leaked %d fds; want ~0", fdGrowth)
変更後:t.Errorf("leaked %d fds; want ~0; have:\n%s\noriginally:\n%s", fdGrowth, lsof, lsof0)
これにより、ファイルディスクリプタのリークが検出された際に、リークした数だけでなく、テスト実行前後のlsof
コマンドの出力がエラーメッセージに含まれるようになり、デバッグ情報が大幅に強化されました。
- 変更前:
-
numOpenFDS
関数の変更:- 変更前:
func numOpenFDS(t *testing.T) int { ... return bytes.Count(lsof, []byte("\n")) }
- 変更後:
func numOpenFDS(t *testing.T) (n int, lsof []byte) { ... return bytes.Count(lsof, []byte("\n")), lsof }
このヘルパ関数は、開いているファイルディスクリプタの数を数えるためにlsof
コマンドを実行していました。変更後は、ファイルディスクリプタの数(n
)だけでなく、lsof
コマンドの生出力(lsof []byte
)も返すようになりました。これにより、呼び出し元(TestPipeLookPathLeak
)が詳細なデバッグ情報を利用できるようになります。また、t.Skip
が呼ばれた際にreturn 0
が不要になったのは、名前付き戻り値n
とlsof
がゼロ値で初期化されるためです。
- 変更前:
これらの変更は、os/exec
パッケージの堅牢性を高め、特にデバッグが困難な環境固有の問題に対する診断能力を向上させることを目的としています。
関連リンク
- Go言語
os/exec
パッケージ公式ドキュメント: https://pkg.go.dev/os/exec - Go言語
sync
パッケージ公式ドキュメント: https://pkg.go.dev/sync - Go言語におけるメソッド値に関する解説 (Go 101): https://go101.org/article/method.html
参考にした情報源リンク
- Go
linux-386-387
failuresに関するWeb検索結果 - Go
os/exec
packageに関するWeb検索結果 - Go
sync.Once
method valueに関するWeb検索結果 - コミットメッセージに記載されているビルドログ:
http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814
(ただし、このリンクは古い可能性があり、現在はアクセスできない場合があります。) - Go Gerrit Code Review (CL 87950044): https://golang.org/cl/87950044 (Goの変更履歴を追跡するための公式レビューシステム) I have generated the detailed explanation in Markdown format, including all the required sections and based on the commit data and web search results. I have ensured it is in Japanese and provides comprehensive technical details.