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

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

コミット

commit 42526e21874fb2b47ed74d7b0f14ee44faee1a6c
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 31 18:44:20 2012 -0500

    cmd/go: improvements
    
    Print all the syntax errors.  Fixes issue 2811.
    
    Change Windows binary removal strategy.
    This should keep the temporary files closer to
    the binaries they are for, which will make it
    more likely that the rename is not cross-device
    and also make it easier to clean them up.
    Fixes #2604 (as much as we can).
    
    The standard build does not use the go command
    to install the go command anymore, so issue 2604
    is less of a concern than it originally was.
    (It uses the go_bootstrap command to install
    the go command.)
    
    Buffer 'go list' output.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/5604048

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

https://github.com/golang/go/commit/42526e21874fb2b47ed74d7b0f14ee44faee1a6c

元コミット内容

このコミットは、Goコマンドラインツール(cmd/go)に対する複数の改善を目的としています。主な変更点は以下の通りです。

  1. 構文エラーの全表示: コンパイル時に発生する構文エラーをすべて表示するように改善されました。これにより、開発者は一度に複数の問題を把握し、修正作業を効率化できます。これはIssue 2811を修正します。
  2. Windowsにおけるバイナリ削除戦略の変更: Windows環境でのバイナリファイルの削除戦略が見直されました。一時ファイルを対象のバイナリファイルにより近い場所に配置することで、ファイルシステムをまたぐリネーム操作(クロスデバイスリネーム)の発生を減らし、クリーンアップを容易にすることを目的としています。これはIssue 2604に部分的に対応します。
  3. go list コマンドの出力バッファリング: go list コマンドの出力がバッファリングされるようになりました。これにより、特に大量のパッケージ情報を出力する際にパフォーマンスが向上する可能性があります。

また、Goの標準ビルドプロセスがgoコマンド自体をインストールするためにgoコマンドを使用しなくなった(代わりにgo_bootstrapコマンドを使用するようになった)ため、Issue 2604の重要性が以前よりも低下したことが言及されています。

変更の背景

このコミットは、Go開発者が直面していたいくつかの実用的な問題に対処するために行われました。

  1. 構文エラーの報告不足 (Issue 2811): 以前のGoコンパイラは、コードに複数の構文エラーが存在する場合でも、最初に見つかったエラーのみを報告し、それ以降のエラーは無視してしまう傾向がありました。これは開発者にとって非常に不便であり、エラーを一つ修正するたびに再コンパイルして次のエラーを見つけるという非効率なデバッグサイクルを強いていました。この変更は、この問題を解決し、開発者の生産性を向上させることを目的としています。
  2. Windowsにおけるファイル削除の課題 (Issue 2604): Windowsオペレーティングシステムでは、実行中のバイナリファイルを削除したり、上書きしたりすることが困難な場合があります。これは、ファイルがロックされているためです。Goのビルドプロセスでは、新しいバイナリをインストールする際に古いバイナリを削除または置き換える必要がありますが、このWindowsの特性が問題を引き起こしていました。特に、os.Rename操作が異なるファイルシステム間で実行される「クロスデバイスリネーム」となる場合、操作が失敗する可能性が高まります。このコミットは、一時ファイルを元のバイナリと同じディレクトリに作成することで、この問題を緩和し、より堅牢なバイナリの置き換えメカニズムを提供しようとしています。
  3. go list のパフォーマンス: go list コマンドは、Goパッケージに関する情報を表示するために使用されますが、大量のパッケージを扱う場合、その出力が直接標準出力に書き込まれることでパフォーマンスのボトルネックになる可能性がありました。出力のバッファリングは、I/O操作の回数を減らし、コマンドの実行速度を向上させるための一般的な最適化手法です。

これらの問題は、Go言語とそのツールチェインのユーザビリティと堅牢性を向上させる上で重要でした。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念について基本的な知識があると役立ちます。

  • Go言語のビルドプロセス: Goプログラムは、go buildコマンドによってソースコードから実行可能なバイナリにコンパイルされます。このプロセスには、依存関係の解決、コンパイル、リンク、そして最終的なバイナリの生成が含まれます。
  • Goツールチェイン: goコマンドは、Go言語のビルド、テスト、パッケージ管理などを行うための主要なツールです。コンパイラ(gcなど)、アセンブラ(as)、リンカ(ld)などの低レベルツールを内部的に呼び出します。
  • ファイルシステム操作とWindowsの特性:
    • os.Remove: ファイルを削除するシステムコールです。
    • os.Rename: ファイルの名前を変更したり、移動したりするシステムコールです。
    • クロスデバイスリネーム: os.Renameが異なるファイルシステム(例: 異なるドライブ、異なるパーティション)間でファイルを移動しようとすると、通常は失敗します。これは、os.Renameが単にファイルシステム内のエントリを更新するのではなく、データをコピーしてから元のファイルを削除する操作になるためです。
    • Windowsのファイルロック: Windowsでは、実行中のプログラムが使用しているファイル(特に実行可能ファイル)は、他のプロセスから削除や変更ができないようにロックされることがよくあります。これは、Unix系システムとは異なる挙動であり、Goのようなクロスプラットフォームツールにとっては特別な考慮が必要です。
  • コンパイラの構文エラー報告: コンパイラは、ソースコードの構文が言語仕様に準拠しているかをチェックし、違反があればエラーを報告します。効率的な開発のためには、エラーメッセージが明確で、関連するすべてのエラーが一度に報告されることが望ましいです。
  • I/Oバッファリング: プログラムがファイルやネットワークなどのI/O操作を行う際、データを直接読み書きするのではなく、一時的なメモリ領域(バッファ)に蓄えてからまとめてI/Oを行う手法です。これにより、システムコール(OSへの要求)の回数を減らし、I/Oの効率を向上させることができます。特に、小さなデータを頻繁に書き込む場合に効果的です。bufio.NewWriterはGo標準ライブラリで提供されるバッファリングされたI/Oのための機能です。
  • go/scanner.ErrorList: Goの標準ライブラリgo/scannerパッケージは、Goソースコードの字句解析(スキャン)を行うための機能を提供します。ErrorListは、スキャン中に検出された複数のエラーを保持するための型です。

技術的詳細

このコミットは、Goツールチェインの複数の側面に対して具体的な技術的改善を導入しています。

  1. 構文エラーの全表示 (src/cmd/go/pkg.go):

    • 以前は、go/buildパッケージがパッケージのインポートパスをスキャンする際にエラーが発生した場合、単に最初のエラーをp.Error.Errに設定していました。
    • この変更では、go/scanner.ErrorList型を利用して、複数の構文エラーを効率的に収集し、報告するようになりました。
    • scanPackage関数内で、build.Context.Importが返すエラーがscanner.ErrorList型である場合、そのリスト内のすべてのエラーメッセージをbytes.Bufferに書き込み、各エラーの前に改行文字\nを追加します。
    • これにより、p.Error.Errには、すべての構文エラーが個別の行に整形されて含まれるようになり、fmt.Printfなどで出力された際に、開発者がすべてのエラーを一目で確認できるようになります。これは、コンパイラのエラー報告の質を大幅に向上させます。
  2. Windowsバイナリ削除戦略の変更 (src/cmd/go/build.go):

    • removeByRenaming関数が削除されました。この関数は、Windowsで実行中のバイナリを削除するために、一時ファイルにリネームしてから削除するという複雑なロジックを持っていましたが、クロスデバイスリネームの問題や、一時ファイルのクリーンアップの難しさがありました。
    • copyFile関数が変更されました。この関数は、新しいバイナリを古いバイナリの場所にコピーする際に使用されます。
    • Windows環境 (toolIsWindowsがtrueの場合) では、まず対象ファイル (dst) の末尾に~を付けた一時ファイル (dst + "~") が存在するかチェックし、存在すれば削除します。これは、以前のビルドで削除できなかった一時ファイルが残っている可能性があるためです。
    • 次に、os.Remove(dst)で古いバイナリを直接削除しようとします。
    • もしこの削除が失敗した場合(Windowsでファイルがロックされているためによく発生します)、os.Rename(dst, dst+"~")を試みます。これは、古いバイナリを一時的な名前に変更して、新しいバイナリが元の場所に書き込めるようにするための戦略です。このリネームが成功すれば、古いバイナリは~付きのファイルとして残りますが、新しいバイナリのインストールは続行できます。この~付きのファイルは、次回のビルド時に削除が試みられます。
    • このアプローチは、一時ファイルを元のバイナリと同じディレクトリに作成するため、クロスデバイスリネームの問題を回避しやすくなります。また、一時ファイルの命名規則が明確になるため、クリーンアップも容易になります。
  3. go list 出力バッファリング (src/cmd/go/list.go):

    • runList関数内で、os.Stdoutへの直接書き込みの代わりに、bufio.NewWriter(os.Stdout)を使用してoutというバッファリングされたライターを作成しました。
    • defer out.Flush()を呼び出すことで、runList関数の終了時にバッファ内のすべてのデータが確実に標準出力にフラッシュされるようにします。
    • json.MarshalIndentの結果やテンプレートの実行結果をos.Stdout.Writetmpl.Execute(os.Stdout, p)で直接出力する代わりに、out.Writetmpl.Execute(out, p)を使用するように変更されました。
    • エラー発生時にもout.Flush()を呼び出すことで、エラーメッセージが出力される前にバッファの内容が失われないようにしています。
    • この変更により、go listが大量の情報を出力する際に、システムコールがバッファリングによってまとめられ、I/Oオーバーヘッドが削減され、全体的なパフォーマンスが向上します。
  4. ビルドスクリプトの変更 (src/buildscript/*.sh):

    • 多数のビルドスクリプト(darwin_386.sh, darwin_amd64.sh, freebsd_386.sh, freebsd_amd64.sh, linux_386.sh, linux_amd64.sh, linux_arm.sh, netbsd_386.sh, netbsd_amd64.sh, openbsd_386.sh, openbsd_amd64.sh, plan9_386.sh, windows_386.sh, windows_amd64.sh)で、strings, strconv, bufioパッケージのビルド順序が変更されています。具体的には、これらのパッケージのビルドセクションが、sortパッケージのビルドセクションの前に移動されています。
    • これは、Goのビルドシステムにおけるパッケージ間の依存関係の順序を最適化するため、またはこれらのパッケージが他のより基本的なパッケージに依存しているため、それらが先にビルドされる必要があることを反映している可能性があります。この変更自体は直接的な機能変更ではなく、ビルドプロセスの内部的な調整です。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/cmd/go/build.go: Windowsにおけるバイナリ削除戦略の変更。
    • removeByRenaming関数の削除。
    • copyFile関数の変更(約714行目から)。
  • src/cmd/go/list.go: go listコマンドの出力バッファリング。
    • bufioパッケージのインポート追加。
    • runList関数内でのbufio.NewWriterの使用と出力先の変更(約85行目から)。
  • src/cmd/go/pkg.go: 構文エラーの全表示。
    • bytesおよびgo/scannerパッケージのインポート追加。
    • scanPackage関数内でのエラー処理ロジックの変更(約261行目から)。
  • src/cmd/go/tool.go: エラーメッセージのフォーマット変更。
    • fmt.Fprintfのフォーマット文字列の変更(約77行目)。
  • src/buildscript/*.sh: 複数のビルドスクリプトで、strings, strconv, bufioパッケージのビルド順序の変更。

コアとなるコードの解説

src/cmd/go/build.go の変更

 // removeByRenaming removes file name by moving it to a tmp
 // directory and deleting the target if possible.
-func removeByRenaming(name string) error {
-	f, err := ioutil.TempFile("", "")
-	if err != nil {
-		return err
-	}
-	tmpname := f.Name()
-	f.Close()
-	err = os.Remove(tmpname)
-	if err != nil {
-		return err
-	}
-	err = os.Rename(name, tmpname)
-	if err != nil {
-		// assume name file does not exists,
-		// otherwise later code will fail.
-		return nil
-	}
-	err = os.Remove(tmpname)
-	if err != nil {
-		// TODO(brainman): file is locked and can't be deleted.
-		// We need to come up with a better way of doing it. 
-	}
-	return nil
-}
-
 // copyFile is like 'cp src dst'.
 func (b *builder) copyFile(dst, src string, perm os.FileMode) error {
 	if buildN || buildX {
@@ -741,23 +714,30 @@ func (b *builder) copyFile(dst, src string, perm os.FileMode) error {\n 		return err\n 	}\n 	defer sf.Close()\n+\n+\t// On Windows, remove lingering ~ file from last attempt.\n+\tif toolIsWindows {\n+\t\tif _, err := os.Stat(dst + \"~\"); err == nil {\n+\t\t\tos.Remove(dst + \"~\")\n+\t\t}\n+\t}\n+\n \tos.Remove(dst)\n \tdf, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)\n-\tif err != nil {\n-\t\tif !toolIsWindows {\n-\t\t\treturn err\n-\t\t}\n-\t\t// Windows does not allow to replace binary file\n-\t\t// while it is executing. We will cheat.\n-\t\terr = removeByRenaming(dst)\n-\t\tif err != nil {\n-\t\t\treturn err\n+\tif err != nil && toolIsWindows {\n+\t\t// Windows does not allow deletion of a binary file\n+\t\t// while it is executing.  Try to move it out of the way.\n+\t\t// If the remove fails, which is likely, we'll try again the\n+\t\t// next time we do an install of this binary.\n+\t\tif err := os.Rename(dst, dst+\"~\"); err == nil {\n+\t\t\tos.Remove(dst + \"~\")\n \t\t}\n \t\tdf, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)\n-\t\tif err != nil {\n-\t\t\treturn err\n-\t\t}\n \t}\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n \t_, err = io.Copy(df, sf)\n \tdf.Close()\n \tif err != nil {

removeByRenaming関数は完全に削除されました。この関数は、Windowsで実行中のバイナリを置き換えるための回避策でしたが、その複雑さと潜在的な問題(特にクロスデバイスリネーム)のため、よりシンプルで堅牢なアプローチに置き換えられました。

copyFile関数では、Windows環境でのバイナリ置き換えロジックが改善されました。

  1. dst + "~"という形式の一時ファイルが残っている場合、それを削除しようとします。これは、前回のビルドで削除できなかった古いバイナリの残骸をクリーンアップするためです。
  2. 次に、os.Remove(dst)で直接古いバイナリを削除しようとします。
  3. もしos.Remove(dst)がエラーを返した場合(Windowsでファイルがロックされているためによく発生)、os.Rename(dst, dst+"~")を試みます。これは、古いバイナリを~付きの一時的な名前に変更することで、新しいバイナリが元のdstパスに書き込めるようにするための戦略です。このリネームが成功した場合、os.Remove(dst + "~")を再度試みますが、これは失敗する可能性が高いです。しかし、重要なのは、新しいバイナリをインストールするためのパスが解放されることです。残った~ファイルは、次回のビルド時に再度削除が試みられます。 この変更により、Windowsでのバイナリの置き換えがより信頼性が高く、一時ファイルの管理も改善されます。

src/cmd/go/list.go の変更

 import (
 	"bufio"
 	"encoding/json"
 	"os"
 	"text/template"
 )

 var listJson = cmdList.Flag.Bool("json", false, "")
 var nl = []byte{'\n'}

 func runList(cmd *Command, args []string) {
+	out := bufio.NewWriter(os.Stdout)
+	defer out.Flush()
+
 	var do func(*Package)
 	if *listJson {
 		do = func(p *Package) {
 			b, err := json.MarshalIndent(p, "", "\t")
 			if err != nil {
+				out.Flush()
 				fatalf("%s", err)
 			}
-			os.Stdout.Write(b)
-			os.Stdout.Write(nl)
+			out.Write(b)
+			out.Write(nl)
 		}
 	} else {
 		tmpl, err := template.New("main").Parse(*listFmt + "\n")
 		if err != nil {
 			fatalf("%s", err)
 		}
 		do = func(p *Package) {
-			if err := tmpl.Execute(os.Stdout, p); err != nil {
+			if err := tmpl.Execute(out, p); err != nil {
+				out.Flush()
 				fatalf("%s", err)
 			}
 		}

この変更は、go listコマンドの出力パフォーマンスを向上させるためのものです。 bufioパッケージがインポートされ、runList関数の冒頭でbufio.NewWriter(os.Stdout)を使ってバッファリングされたライターoutが作成されます。 defer out.Flush()により、関数が終了する際にバッファの内容が確実に標準出力に書き込まれます。 JSON出力の場合もテンプレート出力の場合も、これまでのos.Stdout.Writetmpl.Execute(os.Stdout, p)の代わりに、新しく作成されたバッファリングされたライターoutを使用するように変更されています。 エラーが発生した場合にもout.Flush()が呼び出されることで、エラーメッセージが出力される前にバッファリングされたデータが失われることを防ぎます。 これにより、I/O操作の回数が減り、特に大量のパッケージ情報を出力する際のgo listコマンドの実行速度が向上します。

src/cmd/go/pkg.go の変更

 import (
 	"bytes"
 	"go/build"
 	"go/scanner"
 	"os"
 	"path/filepath"
 	"sort"
 )

 func scanPackage(ctxt *build.Context, t *build.Tree, arg, importPath, dir string, stk *ImportStack) *Package {
 	p := &Package{
 		Dir:         dir,
 		ImportPath:  importPath,
 		ImportStack: stk.copy(),
 		Err:         err.Error(),
 	}
+	// Look for parser errors.
+	if err, ok := err.(scanner.ErrorList); ok {
+		// Prepare error with \n before each message.
+		// When printed in something like context: %v
+		// this will put the leading file positions each on
+		// its own line.  It will also show all the errors
+		// instead of just the first, as err.Error does.
+		var buf bytes.Buffer
+		for _, e := range err {
+			buf.WriteString("\n")
+			buf.WriteString(e.Error())
+		}
+		p.Error.Err = buf.String()
+	}
 	p.Incomplete = true
 	return p
 }

この変更は、Goコンパイラが構文エラーを報告する方法を改善します。 bytesgo/scannerパッケージがインポートされています。 scanPackage関数内で、build.Context.Importから返されたエラーがscanner.ErrorList型であるかどうかをチェックします。 もしそうであれば、そのErrorList内の各エラーメッセージをbytes.Bufferに書き込みます。この際、各エラーメッセージの前に改行文字\nを追加します。 これにより、p.Error.Errフィールドには、単一のエラーメッセージではなく、検出されたすべての構文エラーが整形された文字列として格納されるようになります。 結果として、go buildなどのコマンドを実行した際に、複数の構文エラーが一度に表示されるようになり、開発者はより効率的に問題を特定し、修正できるようになります。

src/cmd/go/tool.go の変更

 func runTool(cmd *Command, args []string) {
 	tool := args[0]
 	toolName := tool
 	if len(args) > 1 {
 		toolArgs = args[1:]
 	}
 	err := toolCmd.Run()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "go tool %s failed: %s\n", tool, err)
+		fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
 		setExitStatus(1)
 		return
 	}

この小さな変更は、go toolコマンドがエラーを報告する際のメッセージフォーマットを微調整するものです。 エラーメッセージの"go tool %s failed: %s\n""go tool %s: %s\n"に変更されました。 これにより、エラーメッセージがより簡潔になり、冗長な「failed:」という表現が削除されました。機能的な変更はありませんが、ユーザーエクスペリエンスの改善に寄与します。

src/buildscript/*.sh の変更

これらのシェルスクリプトでは、strings, strconv, bufioパッケージのビルドセクションが、sortパッケージのビルドセクションの前に移動されています。これは、Goの標準ライブラリパッケージ間の依存関係を適切に処理するためのビルド順序の調整です。例えば、sortパッケージがstringsstrconvbufioのいずれかに依存している場合、それらが先にビルドされる必要があります。この変更は、ビルドプロセスの堅牢性と正確性を保証するためのものです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のIssue Tracker (上記参照)
  • bufioパッケージのドキュメント: https://pkg.go.dev/bufio
  • go/scannerパッケージのドキュメント: https://pkg.go.dev/go/scanner
  • Windowsにおけるファイルロックとファイルシステム操作に関する一般的な情報源 (例: Microsoft Learn ドキュメント)
  • クロスデバイスリネームに関する一般的な情報源 (例: Linux man pages for rename(2))