[インデックス 17597] ファイルの概要
このコミットは、Go言語のos/exec
パッケージにおけるCmd
構造体のStdoutPipe
およびStderrPipe
メソッドのドキュメントに、重要な注意書きを追加するものです。特に、これらのパイプのライフサイクルと、Wait
メソッドやRun
メソッドとの相互作用に関する潜在的な落とし穴について、より明確な説明が加えられています。
コミット
commit 22e8f82e8d4b62a4c96a82d4f731606a87db8d09
Author: Russ Cox <rsc@golang.org>
Date: Fri Sep 13 15:43:54 2013 -0400
os/exec: add more caveats to StdoutPipe, StderrPipe
(StdinPipe was taken care of by CL 13329043.)
Fixes #6008.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13606046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/22e8f82e8d4b62a4c96a82d4f731606a87db8d09
元コミット内容
os/exec: add more caveats to StdoutPipe, StderrPipe
(StdinPipe was taken care of by CL 13329043.)
Fixes #6008.
このコミットメッセージは、StdoutPipe
とStderrPipe
のドキュメントにさらなる注意書きを追加したことを示しています。また、StdinPipe
については、以前の変更リスト(CL 13329043)で既に対応済みであること、そしてこの変更がIssue #6008を修正するものであることが明記されています。
変更の背景
この変更の背景には、Goのos/exec
パッケージにおける外部コマンドの標準入出力パイプの扱いに関するユーザーの混乱や誤用があったと考えられます。特に、StdoutPipe
やStderrPipe
を使用して外部コマンドの出力を読み取る際に、Cmd.Wait()
メソッドを呼び出すタイミングが重要であるにもかかわらず、その点がドキュメントで十分に強調されていなかったことが問題でした。
Issue #6008("os/exec: StdoutPipe and StderrPipe documentation is misleading")は、まさにこの問題点を指摘しています。ユーザーは、StdoutPipe
やStderrPipe
から読み取りを完了する前にWait
を呼び出すと、デッドロックや予期せぬ動作が発生する可能性があることを理解していませんでした。これは、Wait
がパイプを自動的に閉じるため、読み取りが完了する前にパイプが閉じられてしまうと、読み取り操作がブロックされるか、エラーになるためです。また、Run
メソッドは内部でStart
とWait
を呼び出すため、StdoutPipe
やStderrPipe
とRun
を併用することも同様の問題を引き起こします。
このコミットは、これらの潜在的な問題を回避するための重要なガイダンスをドキュメントに追加することで、ユーザーがos/exec
パッケージをより安全かつ効果的に使用できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とos/exec
パッケージの基本的な知識が必要です。
os/exec
パッケージ: Go言語で外部コマンドを実行するためのパッケージです。exec.Command
関数でコマンドを構築し、Cmd
構造体を通じてその実行を制御します。Cmd
構造体: 実行する外部コマンドとその設定(引数、環境変数、標準入出力など)をカプセル化する構造体です。- 標準入出力 (Standard I/O):
- 標準入力 (Stdin): プログラムがデータを読み取るための入力ストリーム。
- 標準出力 (Stdout): プログラムが通常の結果を出力するためのストリーム。
- 標準エラー出力 (Stderr): プログラムがエラーメッセージを出力するためのストリーム。
- パイプ (Pipe): プロセス間通信の一種で、一方のプロセスの出力がもう一方のプロセスの入力に直接接続されるメカニズムです。
os/exec
パッケージでは、Goプログラムと外部コマンドの間でデータをやり取りするためにパイプが使用されます。 Cmd.Start()
: 外部コマンドを非同期で開始します。コマンドが開始された後、すぐに制御が呼び出し元に戻ります。Cmd.Wait()
:Start
で開始された外部コマンドの終了を待ち、その終了ステータスを返します。このメソッドは、コマンドが終了するまでブロックします。Cmd.Run()
:Start
とWait
を連続して呼び出すコンビニエンスメソッドです。コマンドを開始し、その終了を待ちます。io.ReadCloser
/io.WriteCloser
: Goの標準ライブラリで定義されているインターフェースです。ReadCloser
はRead
とClose
メソッドを持ち、WriteCloser
はWrite
とClose
メソッドを持ちます。パイプはこれらのインターフェースを実装しており、データの読み書きと、使用後のリソース解放(クローズ)が可能です。- デッドロック (Deadlock): 複数のプロセスやスレッドが互いに相手が保持しているリソースの解放を待ち、結果としてどのプロセスも処理を進められなくなる状態です。このコミットの文脈では、パイプからの読み取りが完了する前に
Wait
がパイプを閉じてしまい、読み取り側がブロックされることで発生します。
技術的詳細
このコミットは、os/exec
パッケージのCmd
構造体におけるStdinPipe
, StdoutPipe
, StderrPipe
メソッドのドキュメントを修正・拡充しています。
変更の核心:
以前のドキュメントでは、StdoutPipe
とStderrPipe
が「コマンドが終了すると自動的に閉じられる」とだけ述べられていました。しかし、この記述だけでは、ユーザーがこれらのパイプを扱う上で陥りやすいデッドロックのシナリオを十分に警告していませんでした。
具体的には、以下の重要な注意点が追加されました。
StdoutPipe
/StderrPipe
とWait
の関係:Wait
はコマンドの終了を確認した後、パイプを閉じます。- このため、パイプからのすべての読み取りが完了する前に
Wait
を呼び出すのは誤りです。読み取りが完了する前にパイプが閉じられると、読み取り操作がブロックされるか、エラーになります。 - これは、外部コマンドが大量の出力を生成し、Goプログラムがそれをすべて読み取る前に
Wait
が呼び出された場合に特に問題となります。パイプのバッファが満杯になり、外部コマンドが書き込みをブロックし、Goプログラムが読み取りをブロックし、Wait
がコマンドの終了を待つというデッドロック状態に陥る可能性があります。
StdoutPipe
/StderrPipe
とRun
の関係:Run
メソッドは内部でStart
とWait
を呼び出すため、StdoutPipe
やStderrPipe
を使用する際には**Run
を呼び出すのは誤り**です。- これは、
Run
がWait
を自動的に呼び出すため、パイプからの読み取りが完了する前にWait
が実行されてしまうためです。
- 慣用的な使用法への誘導:
- ドキュメントは、これらのパイプを正しく使用するための「慣用的な使用法(idiomatic usage)」として、
StdoutPipe
の例を参照するように促しています。この例では、Cmd.Start()
でコマンドを開始し、別のゴルーチンでパイプからの読み取りを行い、その後にCmd.Wait()
を呼び出すというパターンが示されています。これにより、読み取りとWait
が並行して実行され、デッドロックが回避されます。
- ドキュメントは、これらのパイプを正しく使用するための「慣用的な使用法(idiomatic usage)」として、
StdinPipe
に関する補足:
コミットメッセージには「(StdinPipe was taken care of by CL 13329043.)
」とあります。これは、StdinPipe
についても同様の注意が必要であり、以前の変更リスト(CL 13329043)で既に対応済みであることを示しています。StdinPipe
の場合、外部コマンドが標準入力が閉じられるまで終了しない場合、呼び出し元が明示的にパイプを閉じる必要があるという注意が追加されています。
これらの変更は、os/exec
パッケージのAPIが提供する柔軟性と、それに伴う複雑さをユーザーがより深く理解し、一般的な落とし穴を避けるための重要なドキュメント改善です。
コアとなるコードの変更箇所
変更はsrc/pkg/os/exec/exec.go
ファイルに集中しています。
--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -358,8 +358,10 @@ func (c *Cmd) CombinedOutput() ([]byte, error) {
// StdinPipe returns a pipe that will be connected to the command's
// standard input when the command starts.
-// If the returned WriteCloser is not closed before Wait is called,
-// Wait will close it.
+// The pipe will be closed automatically after Wait sees the command exit.
+// A caller need only call Close to force the pipe to close sooner.
+// For example, if the command being run will not exit until standard input
+// is closed, the caller must close the pipe.
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
if c.Stdin != nil {
return nil, errors.New("exec: Stdin already set")
@@ -394,7 +396,12 @@ func (c *closeOnce) Close() error {
// StdoutPipe returns a pipe that will be connected to the command's
// standard output when the command starts.
-// The pipe will be closed automatically after Wait sees the command exit.
+//
+// Wait will close the pipe after seeing the command exit, so most callers
+// need not close the pipe themselves; however, an implication is that
+// it is incorrect to call Wait before all reads from the pipe have completed.
+// For the same reason, it is incorrect to call Run when using StdoutPipe.
+// See the example for idiomatic usage.
func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
@@ -414,7 +421,12 @@ func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
// StderrPipe returns a pipe that will be connected to the command's
// standard error when the command starts.
-// The pipe will be closed automatically after Wait sees the command exit.
+//
+// Wait will close the pipe after seeing the command exit, so most callers
+// need not close the pipe themselves; however, an implication is that
+// it is incorrect to call Wait before all reads from the pipe have completed.
+// For the same reason, it is incorrect to use Run when using StderrPipe.
+// See the StdoutPipe example for idiomatic usage.
func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
if c.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
コアとなるコードの解説
このコミットは、Goのos/exec
パッケージ内のCmd
構造体のメソッドであるStdinPipe
、StdoutPipe
、StderrPipe
のドキュメント文字列(コメント)を修正しています。実際のコードのロジック自体には変更はありません。
-
StdinPipe
の変更点:- 変更前:
// If the returned WriteCloser is not closed before Wait is called, // Wait will close it.
- 変更後:
// The pipe will be closed automatically after Wait sees the command exit. // A caller need only call Close to force the pipe to close sooner. // For example, if the command being run will not exit until standard input // is closed, the caller must close the pipe.
- 解説:
StdinPipe
は、以前のCLで既に修正済みとされていますが、このコミットでもドキュメントが更新されています。新しいドキュメントは、Wait
がパイプを自動的に閉じることを再確認しつつ、呼び出し元がパイプをより早く閉じる必要があるシナリオ(例:コマンドが入力の終了を待つ場合)を明確にしています。これにより、ユーザーはデッドロックを避けるために、必要に応じて明示的にClose
を呼び出すべきであることを理解できます。
- 変更前:
-
StdoutPipe
の変更点:- 変更前:
// The pipe will be closed automatically after Wait sees the command exit.
- 変更後:
// Wait will close the pipe after seeing the command exit, so most callers // need not close the pipe themselves; however, an implication is that // it is incorrect to call Wait before all reads from the pipe have completed. // For the same reason, it is incorrect to call Run when using StdoutPipe. // See the example for idiomatic usage.
- 解説: ここがこのコミットの主要な変更点です。以前の記述は、パイプが自動的に閉じられるという事実のみを述べていましたが、新しい記述では、その「含意(implication)」として、パイプからのすべての読み取りが完了する前に
Wait
を呼び出すのは誤りであること、そして**StdoutPipe
を使用する際にRun
を呼び出すのも誤りである**ことを明確に警告しています。これは、Wait
やRun
がパイプを閉じてしまうため、読み取りが完了する前にパイプが閉じられ、デッドロックやデータ損失につながる可能性があるためです。最後に、「慣用的な使用法」として例を参照するように促しています。
- 変更前:
-
StderrPipe
の変更点:- 変更前:
// The pipe will be closed automatically after Wait sees the command exit.
- 変更後:
// Wait will close the pipe after seeing the command exit, so most callers // need not close the pipe themselves; however, an implication is that // it is incorrect to call Wait before all reads from the pipe have completed. // For the same reason, it is incorrect to use Run when using StderrPipe. // See the StdoutPipe example for idiomatic usage.
- 解説:
StdoutPipe
と全く同じ変更がStderrPipe
にも適用されています。標準エラー出力も標準出力と同様に扱われるため、同じ注意が必要です。
- 変更前:
これらのドキュメントの追加は、APIの動作に関する潜在的な誤解を解消し、ユーザーがより堅牢で正しいコードを書くための重要なガイドラインを提供します。
関連リンク
- Go Issue #6008: os/exec: StdoutPipe and StderrPipe documentation is misleading
- Go Change List 13606046: os/exec: add more caveats to StdoutPipe, StderrPipe
- Go Change List 13329043: os/exec: document StdinPipe's Close behavior (StdinPipeに関する以前の修正)
- Go
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec
参考にした情報源リンク
- 上記のGitHub IssueおよびGo Change Listのページ
- Go言語の公式ドキュメント (
os/exec
パッケージ) - Go言語におけるパイプと外部コマンド実行に関する一般的な知識