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

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

このコミットは、Go言語のコマンドラインツール go clean におけるエラーハンドリングの改善に関するものです。具体的には、ファイルやディレクトリの削除時に発生する可能性のあるエラーが無視されないように修正されています。

コミット

commit bf7d229eb8256cbed54a136fbd1d255ac18a18d5
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Oct 11 01:34:26 2012 +0800

    cmd/go: don't ignore error when 'go clean'
            Fixes #4208.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/6635064

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

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

元コミット内容

cmd/go: don't ignore error when 'go clean' Fixes #4208.

R=golang-dev, iant CC=golang-dev https://golang.org/cl/6635064

変更の背景

go clean コマンドは、Goプロジェクトのビルドによって生成されたファイル(実行可能ファイル、アーカイブファイル、テストキャッシュなど)を削除するために使用されます。このコマンドは、クリーンなビルド環境を維持したり、ディスクスペースを解放したりするのに役立ちます。

このコミット以前の go clean コマンドの実装では、ファイルやディレクトリを削除する際に os.Removeos.RemoveAll 関数が使用されていましたが、これらの関数が返すエラーが適切に処理されていませんでした。これにより、例えば削除対象のファイルが存在しない場合や、アクセス権の問題で削除に失敗した場合でも、go clean コマンドはエラーを報告せずに正常終了してしまう可能性がありました。

このような挙動は、ユーザーが go clean コマンドの実行結果を誤解する原因となり、問題の特定を困難にする可能性があります。例えば、ユーザーが特定のファイルが削除されたと期待しているにもかかわらず、実際にはエラーで削除されていなかった場合、その後のビルドやテストで予期せぬ問題が発生する可能性があります。

このコミットは、この問題を解決し、go clean コマンドがファイル削除時のエラーを適切に報告するようにすることで、コマンドの信頼性とユーザーエクスペリエンスを向上させることを目的としています。コミットメッセージにある Fixes #4208 は、Goの内部イシュートラッカーにおける特定のバグ報告に対応するものであることを示しています。

前提知識の解説

Go言語におけるエラーハンドリング

Go言語では、エラーハンドリングは他の多くの言語とは異なるアプローチを取ります。Goには例外処理のメカニズム(try-catchブロックなど)は存在せず、関数は通常、最後の戻り値として error 型の値を返します。慣例として、errornil でない場合はエラーが発生したことを意味し、nil の場合は成功したことを意味します。

開発者は、関数の呼び出し後、返された error 値を明示的にチェックし、それに応じて処理を行う必要があります。これにより、エラーが無視されることを防ぎ、プログラムの堅牢性を高めることができます。

result, err := someFunction()
if err != nil {
    // エラー処理
    log.Fatalf("Error: %v", err)
}
// 正常処理

os.Removeos.RemoveAll

Go言語の標準ライブラリ os パッケージは、オペレーティングシステムとのインタラクションを提供します。ファイルシステム操作もその一部です。

  • os.Remove(name string) error: 指定された name のファイルまたは空のディレクトリを削除します。削除に失敗した場合、error を返します。
  • os.RemoveAll(path string) error: 指定された path のファイルまたはディレクトリツリー全体を削除します。ディレクトリが空でなくても、その中のすべてのファイルとサブディレクトリを再帰的に削除します。削除に失敗した場合、error を返します。

これらの関数は、削除対象が存在しない場合にもエラーを返します。しかし、多くの場合、ファイルが存在しないことによる削除エラーは、特に問題視されないことがあります。そのため、os.IsNotExist(err) 関数を使って、エラーが「ファイルが存在しない」ことによるものかどうかをチェックし、それに応じて処理を分岐させることが一般的です。

filepath.Join

filepath パッケージは、ファイルパスを操作するためのユーティリティを提供します。

  • filepath.Join(elem ...string) string: 1つ以上のパス要素を結合して、単一のパスを構築します。この関数は、オペレーティングシステム固有のパス区切り文字(Windowsでは \、Unix系では /)を適切に挿入し、余分な区切り文字を削除して正規化されたパスを返します。これにより、異なるOS間でのパスの互換性が保たれます。

errorf 関数

このコミットで導入された errorf 関数は、Goの標準的なエラー報告パターンに従っています。これは、フォーマットされた文字列を使用してエラーメッセージを生成し、それを標準エラー出力に書き込むか、プログラムを終了させるなどの処理を行うためのヘルパー関数であると推測されます。Goのコマンドラインツールでは、log.Fatalffmt.Errorf と組み合わせて使用されることが多いです。

技術的詳細

このコミットの主要な変更点は、go clean コマンド内でファイルやディレクトリを削除する際に os.Remove および os.RemoveAll が返すエラーを捕捉し、適切に報告するようにしたことです。

具体的には、以下の点が変更されています。

  1. os.RemoveAll のエラーハンドリングの追加: os.RemoveAll(filepath.Join(p.Dir, name)) の呼び出しに対して、返されるエラーを err 変数で受け取り、if err != nil でエラーが発生したかどうかをチェックしています。エラーが発生した場合は、errorf("go clean: %v", err) を呼び出してエラーメッセージを出力しています。これにより、ディレクトリツリーの削除に失敗した場合に、そのエラーがユーザーに通知されるようになります。

  2. removeFile ヘルパー関数の導入: 複数の場所で os.Remove を呼び出している箇所がありましたが、これらを removeFile という新しいヘルパー関数に置き換えています。この removeFile 関数は、ファイル削除時のエラーハンドリングロジックをカプセル化しています。 removeFile 関数は、os.Remove(f) を呼び出し、エラーが発生した場合に os.IsNotExist(err) を使って、そのエラーが「ファイルが存在しない」ことによるものかどうかをチェックします。もしエラーが nil でなく、かつファイルが存在しないことによるエラーでもない場合(つまり、本当に削除に失敗した場合)、errorf("go clean: %v", err) を呼び出してエラーを報告します。 このアプローチにより、ファイルが存在しないことによるエラーは無視しつつ、それ以外の重要なエラーは確実に報告されるようになります。これは、go clean のようなツールにおいて、削除対象が既に存在しないことは正常なケースとして扱われるべきであるという設計思想に基づいています。

これらの変更により、go clean コマンドはより堅牢になり、ファイルシステム操作における潜在的な問題をユーザーに明確に伝えることができるようになりました。

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

変更は src/cmd/go/clean.go ファイルに集中しています。

--- a/src/cmd/go/clean.go
+++ b/src/cmd/go/clean.go
@@ -170,7 +170,9 @@ func clean(p *Package) {
 						continue
 					}
 				}
-				os.RemoveAll(filepath.Join(p.Dir, name))
+				if err := os.RemoveAll(filepath.Join(p.Dir, name)); err != nil {
+					errorf("go clean: %v", err)
+				}
 			}
 			continue
 		}
@@ -180,7 +182,7 @@ func clean(p *Package) {
 		}
 
 		if cleanFile[name] || cleanExt[filepath.Ext(name)] || toRemove[name] {
-			os.Remove(filepath.Join(p.Dir, name))
+			removeFile(filepath.Join(p.Dir, name))
 		}
 	}
 
@@ -189,7 +191,7 @@ func clean(p *Package) {
 			b.showcmd("", "rm -f %s", p.target)
 		}
 		if !cleanN {
-			os.Remove(p.target)
+			removeFile(p.target)
 		}
 	}
 
@@ -202,7 +204,7 @@ func clean(p *Package) {
 			b.showcmd("", "rm -f %s", target)
 		}
 		if !cleanN {
-			os.Remove(target)
+			removeFile(target)
 		}
 	}
 
@@ -213,3 +215,11 @@ func clean(p *Package) {
 		}
 	}
 }
+
+// removeFile tries to remove file f, if error other than file doesn't exist
+// occurs, it will report the error.
+func removeFile(f string) {
+	if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
+		errorf("go clean: %v", err)
+	}
+}

コアとなるコードの解説

clean 関数内の変更

  • os.RemoveAll の変更: 元のコードでは os.RemoveAll(filepath.Join(p.Dir, name)) と直接呼び出されていましたが、変更後には以下のようにエラーチェックが追加されました。

    if err := os.RemoveAll(filepath.Join(p.Dir, name)); err != nil {
        errorf("go clean: %v", err)
    }
    

    これにより、os.RemoveAll がエラーを返した場合、そのエラーが errorf 関数を通じて報告されるようになりました。これは、ディレクトリツリーの削除が失敗した場合に、その情報がユーザーに伝わることを保証します。

  • os.RemoveremoveFile への置き換え: clean 関数内で直接 os.Remove を呼び出していた箇所が、新しく定義された removeFile 関数への呼び出しに置き換えられました。 例えば、以下の行が変更されています。

    -			os.Remove(filepath.Join(p.Dir, name))
    +			removeFile(filepath.Join(p.Dir, name))
    
    -			os.Remove(p.target)
    +			removeFile(p.target)
    
    -			os.Remove(target)
    +			removeFile(target)
    

    この変更により、ファイル削除に関するエラーハンドリングロジックが一箇所に集約され、コードの重複が排除され、保守性が向上しました。

removeFile 関数の追加

このコミットで新しく追加された removeFile 関数は以下の通りです。

// removeFile tries to remove file f, if error other than file doesn't exist
// occurs, it will report the error.
func removeFile(f string) {
	if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
		errorf("go clean: %v", err)
	}
}

この関数のロジックは以下のようになっています。

  1. os.Remove(f) を呼び出して、指定されたファイル f の削除を試みます。
  2. os.Remove が返したエラーを err 変数に格納します。
  3. if err != nil && !os.IsNotExist(err) という条件でエラーをチェックします。
    • err != nil: エラーが発生したことを意味します。
    • !os.IsNotExist(err): 発生したエラーが「ファイルが存在しない」ことによるものではないことを意味します。 この2つの条件が両方とも真である場合、つまり、エラーが発生し、かつそのエラーがファイルが存在しないことによるものではない場合にのみ、エラーを報告します。
  4. 条件が真の場合、errorf("go clean: %v", err) を呼び出して、エラーメッセージをフォーマットして出力します。

この removeFile 関数は、go clean コマンドの意図(存在しないファイルを削除しようとしてもエラーとしない)を正確に反映しつつ、それ以外の予期せぬ削除エラー(例: アクセス権限の問題、ディスクフルなど)を確実に捕捉して報告する役割を担っています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のエラーハンドリングに関する一般的なプラクティス
  • go clean コマンドの挙動に関する一般的な知識
  • GitHubのコミット履歴と差分表示
  • Go言語のソースコードリポジトリ (golang/go)
  • Goの内部イシュートラッカー (Go issue #4208) - ただし、このイシューの具体的な内容は公開されていないため、コミットメッセージからの推測に留まります。