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

[インデックス 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.

このコミットメッセージは、StdoutPipeStderrPipeのドキュメントにさらなる注意書きを追加したことを示しています。また、StdinPipeについては、以前の変更リスト(CL 13329043)で既に対応済みであること、そしてこの変更がIssue #6008を修正するものであることが明記されています。

変更の背景

この変更の背景には、Goのos/execパッケージにおける外部コマンドの標準入出力パイプの扱いに関するユーザーの混乱や誤用があったと考えられます。特に、StdoutPipeStderrPipeを使用して外部コマンドの出力を読み取る際に、Cmd.Wait()メソッドを呼び出すタイミングが重要であるにもかかわらず、その点がドキュメントで十分に強調されていなかったことが問題でした。

Issue #6008("os/exec: StdoutPipe and StderrPipe documentation is misleading")は、まさにこの問題点を指摘しています。ユーザーは、StdoutPipeStderrPipeから読み取りを完了する前にWaitを呼び出すと、デッドロックや予期せぬ動作が発生する可能性があることを理解していませんでした。これは、Waitがパイプを自動的に閉じるため、読み取りが完了する前にパイプが閉じられてしまうと、読み取り操作がブロックされるか、エラーになるためです。また、Runメソッドは内部でStartWaitを呼び出すため、StdoutPipeStderrPipeRunを併用することも同様の問題を引き起こします。

このコミットは、これらの潜在的な問題を回避するための重要なガイダンスをドキュメントに追加することで、ユーザーがos/execパッケージをより安全かつ効果的に使用できるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とos/execパッケージの基本的な知識が必要です。

  1. os/execパッケージ: Go言語で外部コマンドを実行するためのパッケージです。exec.Command関数でコマンドを構築し、Cmd構造体を通じてその実行を制御します。
  2. Cmd構造体: 実行する外部コマンドとその設定(引数、環境変数、標準入出力など)をカプセル化する構造体です。
  3. 標準入出力 (Standard I/O):
    • 標準入力 (Stdin): プログラムがデータを読み取るための入力ストリーム。
    • 標準出力 (Stdout): プログラムが通常の結果を出力するためのストリーム。
    • 標準エラー出力 (Stderr): プログラムがエラーメッセージを出力するためのストリーム。
  4. パイプ (Pipe): プロセス間通信の一種で、一方のプロセスの出力がもう一方のプロセスの入力に直接接続されるメカニズムです。os/execパッケージでは、Goプログラムと外部コマンドの間でデータをやり取りするためにパイプが使用されます。
  5. Cmd.Start(): 外部コマンドを非同期で開始します。コマンドが開始された後、すぐに制御が呼び出し元に戻ります。
  6. Cmd.Wait(): Startで開始された外部コマンドの終了を待ち、その終了ステータスを返します。このメソッドは、コマンドが終了するまでブロックします。
  7. Cmd.Run(): StartWaitを連続して呼び出すコンビニエンスメソッドです。コマンドを開始し、その終了を待ちます。
  8. io.ReadCloser / io.WriteCloser: Goの標準ライブラリで定義されているインターフェースです。ReadCloserReadCloseメソッドを持ち、WriteCloserWriteCloseメソッドを持ちます。パイプはこれらのインターフェースを実装しており、データの読み書きと、使用後のリソース解放(クローズ)が可能です。
  9. デッドロック (Deadlock): 複数のプロセスやスレッドが互いに相手が保持しているリソースの解放を待ち、結果としてどのプロセスも処理を進められなくなる状態です。このコミットの文脈では、パイプからの読み取りが完了する前にWaitがパイプを閉じてしまい、読み取り側がブロックされることで発生します。

技術的詳細

このコミットは、os/execパッケージのCmd構造体におけるStdinPipe, StdoutPipe, StderrPipeメソッドのドキュメントを修正・拡充しています。

変更の核心: 以前のドキュメントでは、StdoutPipeStderrPipeが「コマンドが終了すると自動的に閉じられる」とだけ述べられていました。しかし、この記述だけでは、ユーザーがこれらのパイプを扱う上で陥りやすいデッドロックのシナリオを十分に警告していませんでした。

具体的には、以下の重要な注意点が追加されました。

  • StdoutPipe / StderrPipeWait の関係:
    • Waitはコマンドの終了を確認した後、パイプを閉じます。
    • このため、パイプからのすべての読み取りが完了する前にWaitを呼び出すのは誤りです。読み取りが完了する前にパイプが閉じられると、読み取り操作がブロックされるか、エラーになります。
    • これは、外部コマンドが大量の出力を生成し、Goプログラムがそれをすべて読み取る前にWaitが呼び出された場合に特に問題となります。パイプのバッファが満杯になり、外部コマンドが書き込みをブロックし、Goプログラムが読み取りをブロックし、Waitがコマンドの終了を待つというデッドロック状態に陥る可能性があります。
  • StdoutPipe / StderrPipeRun の関係:
    • Runメソッドは内部でStartWaitを呼び出すため、StdoutPipeStderrPipeを使用する際には**Runを呼び出すのは誤り**です。
    • これは、RunWaitを自動的に呼び出すため、パイプからの読み取りが完了する前にWaitが実行されてしまうためです。
  • 慣用的な使用法への誘導:
    • ドキュメントは、これらのパイプを正しく使用するための「慣用的な使用法(idiomatic usage)」として、StdoutPipeの例を参照するように促しています。この例では、Cmd.Start()でコマンドを開始し、別のゴルーチンでパイプからの読み取りを行い、その後にCmd.Wait()を呼び出すというパターンが示されています。これにより、読み取りとWaitが並行して実行され、デッドロックが回避されます。

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構造体のメソッドであるStdinPipeStdoutPipeStderrPipeのドキュメント文字列(コメント)を修正しています。実際のコードのロジック自体には変更はありません。

  1. 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を呼び出すべきであることを理解できます。
  2. 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を呼び出すのも誤りである**ことを明確に警告しています。これは、WaitRunがパイプを閉じてしまうため、読み取りが完了する前にパイプが閉じられ、デッドロックやデータ損失につながる可能性があるためです。最後に、「慣用的な使用法」として例を参照するように促しています。
  3. 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の動作に関する潜在的な誤解を解消し、ユーザーがより堅牢で正しいコードを書くための重要なガイドラインを提供します。

関連リンク

参考にした情報源リンク

  • 上記のGitHub IssueおよびGo Change Listのページ
  • Go言語の公式ドキュメント (os/execパッケージ)
  • Go言語におけるパイプと外部コマンド実行に関する一般的な知識