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

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

このコミットは、Go言語のコマンドラインツールであるgoコマンドの一部であるsrc/cmd/go/vcs.goファイルに対する変更です。このファイルは、go getなどのコマンドがGitやMercurialといったバージョン管理システム(VCS)と連携する際に、外部のVCSコマンドを実行し、その出力を処理するためのロジックを含んでいます。具体的には、VCSコマンドの実行、標準出力および標準エラー出力のキャプチャ、そしてエラー発生時の出力表示を担当しています。

コミット

このコミットは、goコマンドがVCSコマンドを実行する際に、そのコマンドの出力を正しく収集できるようにするための修正です。以前のコードでは、VCSコマンドの実行前に出力バッファの内容を取得しようとしていたため、実際には何も出力がキャプチャされていませんでした。この修正により、VCSコマンドの実行後にバッファから出力を取得するようになり、エラー発生時にVCSコマンドからの有用な診断メッセージが正しく表示されるようになりました。

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

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

元コミット内容

cmd/go: make vcs command actually gather output

R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5577062

変更の背景

この変更の背景には、go getなどのコマンドが外部のVCSツール(例: git, hg)を呼び出す際に、それらのツールがエラーを返した場合に、そのエラーメッセージや診断情報を正しくユーザーに伝えることができていなかったという問題があります。

具体的には、VCSコマンドの実行結果をキャプチャするためにbytes.Bufferを使用し、そのバッファをcmd.Stdoutcmd.Stderrに割り当てていました。しかし、バッファの内容をout変数に代入する処理が、VCSコマンドの実行(cmd.Run())よりも前に行われていました。このため、cmd.Run()が実行される時点ではbufはまだ空であり、結果としてoutには常に空のバイトスライスが代入されていました。

VCSコマンドがエラーを返した場合、そのエラーメッセージはbufに書き込まれるものの、out変数が空であるため、エラー処理ブロック内でos.Stderr.Write(out)が呼び出されても、何も出力されませんでした。これにより、ユーザーはVCS操作が失敗したことはわかるものの、その具体的な原因(例: リポジトリが見つからない、認証エラーなど)を把握することが困難でした。

このコミットは、このバグを修正し、VCSコマンドの出力を確実にキャプチャして、エラー発生時にその出力をユーザーに表示できるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と一般的なプログラミングの知識が必要です。

  1. cmd/go: Go言語の公式ツールチェーンの一部であるgoコマンド自体を指します。go build, go run, go getなど、Go開発者が日常的に使用する様々なサブコマンドを提供します。
  2. バージョン管理システム (VCS): ソースコードの変更履歴を管理するためのシステムです。Git, Mercurial (hg), Subversion (svn) などが代表的です。go getコマンドは、指定されたパッケージのソースコードをVCSから取得するために、内部的にこれらのVCSツールを呼び出します。
  3. os/execパッケージ: Go言語の標準ライブラリの一部で、外部コマンドを実行するための機能を提供します。
    • exec.Command(name string, arg ...string): 実行するコマンドと引数を指定してCmd構造体を作成します。
    • Cmd.Stdout / Cmd.Stderr: 実行されるコマンドの標準出力および標準エラー出力のリダイレクト先を設定するためのフィールドです。通常、io.Writerインターフェースを実装するオブジェクト(例: bytes.Buffer, os.Stdout)を割り当てます。
    • Cmd.Run(): コマンドを実行し、完了するまで待機します。コマンドが正常に終了した場合はnilを返し、エラーが発生した場合は*ExitErrorなどのエラーを返します。
  4. bytes.Buffer: bytesパッケージに含まれる型で、可変長のバイトバッファを実装します。io.Writerインターフェースを実装しているため、Cmd.StdoutCmd.Stderrに割り当てることで、外部コマンドの出力をメモリ上にキャプチャすることができます。
    • buf.Bytes(): bytes.Bufferの内容をバイトスライスとして返します。このメソッドはバッファの内容のコピーではなく、バッファが保持する内部スライスへの参照を返します。そのため、Bytes()呼び出し後にバッファの内容が変更されると、返されたスライスの内容も変更されます。

技術的詳細

このコミットの技術的な核心は、bytes.Bufferos/exec.Cmdの連携における出力キャプチャのタイミングの問題を解決することにあります。

元のコードでは、VCSコマンドを実行するrun1関数内で、以下のような順序で処理が行われていました。

var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
out := buf.Bytes() // ここでバッファの内容を取得
err := cmd.Run()   // ここでコマンドが実行され、出力がbufに書き込まれる
// ...
os.Stderr.Write(out) // outは空のまま

このコードの問題点は、out := buf.Bytes()cmd.Run()前に実行されていることです。cmd.Run()が呼び出されるまで、外部コマンドは実行されず、その出力はbufに書き込まれません。したがって、out変数には、cmd.Run()が実行される前の空のbufの内容が代入されてしまいます。

cmd.Run()がエラーを返した場合、そのエラーメッセージはbufに書き込まれますが、すでにoutは空のバイトスライスを参照しているため、エラー処理ブロック内でos.Stderr.Write(out)を呼び出しても、何も出力されません。

このコミットでは、out := buf.Bytes()の行をcmd.Run()後に移動させることで、この問題を解決しています。

var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
err := cmd.Run()   // コマンドが実行され、出力がbufに書き込まれる
out := buf.Bytes() // コマンド実行後、bufに書き込まれた内容をoutに取得
// ...
os.Stderr.Write(out) // outにはVCSコマンドの出力が含まれる

この変更により、cmd.Run()が完了し、VCSコマンドからのすべての出力がbufに書き込まれた後に、その内容がout変数に正しく代入されるようになります。これにより、VCSコマンドがエラーを返した場合でも、そのコマンドの標準出力および標準エラー出力に書き込まれた診断情報がoutに含まれるため、os.Stderr.Write(out)によってユーザーに表示され、問題の特定に役立つようになります。

これは、外部プロセスとの連携において、出力のキャプチャとエラーハンドリングを正しく行うための基本的なパターンを示しています。

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

変更はsrc/cmd/go/vcs.goファイル内のvcsCmd.run1関数にあります。

--- a/src/cmd/go/vcs.go
+++ b/src/cmd/go/vcs.go
@@ -157,8 +157,8 @@ func (v *vcsCmd) run1(dir string, output bool, cmdline string, keyval []string)\
 	var buf bytes.Buffer
 	cmd.Stdout = &buf
 	cmd.Stderr = &buf
-	out := buf.Bytes() // 変更前: ここでoutが初期化されていた
 	err := cmd.Run()
-	out := buf.Bytes() // 変更後: ここに移動
+	out := buf.Bytes() // 変更後: ここに移動
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "# cd %s; %s %s\\n", dir, v.cmd, strings.Join(args, " "))
 		os.Stderr.Write(out)

コアとなるコードの解説

このコミットの核心は、たった1行のコードの移動です。

  • 変更前:

    out := buf.Bytes()
    err := cmd.Run()
    

    この順序では、cmd.Run()が実行される前にbufの内容(この時点では空)がoutに代入されていました。そのため、cmd.Run()bufに何かを書き込んでも、outは古い(空の)内容のままでした。

  • 変更後:

    err := cmd.Run()
    out := buf.Bytes()
    

    この順序では、まずcmd.Run()が実行され、外部VCSコマンドからの出力がbufに完全に書き込まれます。その後、out := buf.Bytes()が実行されることで、bufに書き込まれた最新の、そして完全な出力内容がoutに代入されます。

このシンプルな変更により、if err != nilブロック内でos.Stderr.Write(out)が呼び出された際に、VCSコマンドが生成した実際のエラーメッセージや診断情報がユーザーに表示されるようになり、デバッグや問題解決が格段に容易になりました。これは、プログラムのロジックにおける処理順序の重要性を示す典型的な例です。

関連リンク

  • Go Code Review (CL) リンク: https://golang.org/cl/5577062 これはGoプロジェクトが使用しているコードレビューシステムへのリンクです。このコミットがマージされる前の議論やレビューコメントを辿ることができます。

参考にした情報源リンク