[インデックス 13560] ファイルの概要
このコミットは、Go言語の標準ライブラリosパッケージにおけるエラーハンドリング、特にos.IsExistおよびos.IsNotExist関数がPathErrorとLinkErrorを適切に処理することを確認するためのテストを追加し、関連するエラー判定ロジックを修正するものです。これにより、ファイルシステム操作で発生する可能性のある様々なエラータイプに対して、より堅牢で一貫性のあるエラー判定が保証されます。
コミット
commit b9b29ce2ba9e45de8372d81292b52f8623237220
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Aug 3 14:25:35 2012 +1000
os: test that IsExist and IsNotExist handle PathError and LinkError
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6442080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b9b29ce2ba9e45de8372d81292b52f8623237220
元コミット内容
os: test that IsExist and IsNotExist handle PathError and LinkError
このコミットメッセージは、osパッケージのIsExistおよびIsNotExist関数が、PathErrorとLinkErrorという特定のエラー型を適切に処理するかどうかをテストすることに焦点を当てていることを示しています。
変更の背景
Go言語のosパッケージは、ファイルシステム操作を行うための基本的な機能を提供します。これらの操作中にエラーが発生した場合、Goはエラーインターフェースを介してエラーを返します。ファイルやディレクトリの存在確認は一般的な操作であり、その際に「ファイルが存在する」「ファイルが存在しない」といった特定の状態を正確に判定できることは、アプリケーションのロバスト性を高める上で非常に重要です。
os.IsExistやos.IsNotExistといった関数は、この目的のために提供されています。しかし、ファイルシステム操作で発生するエラーは、単なる「存在しない」といった単純なものではなく、PathError(パスに関連するエラー)やLinkError(シンボリックリンクに関連するエラー)のように、より詳細な情報を含むラッパーエラーとして返されることがあります。
このコミット以前は、これらのラッパーエラーがIsExistやIsNotExistによって適切に「アンラップ」され、内部の根本的なエラー(例: syscall.ENOENTやsyscall.EEXIST)が正確に評価されているかどうかのテストが不十分だった可能性があります。そのため、開発者は、PathErrorやLinkErrorが返された場合に、IsExistやIsNotExistが期待通りに動作しないという問題に直面する可能性がありました。
このコミットの背景には、osパッケージのエラーハンドリングの堅牢性を向上させ、開発者がファイルシステムエラーをより確実に判定できるようにするという目的があります。特に、PathErrorやLinkErrorのような一般的なラッパーエラー型を考慮に入れることで、異なるオペレーティングシステム(Plan 9, POSIX, Windows)間でのエラー判定の一貫性を保つことが目指されています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とosパッケージのエラー型に関する知識が必要です。
-
errorインターフェース: Go言語におけるエラーハンドリングの基本です。Goの関数は、エラーが発生した場合にerror型の値を返します。errorインターフェースはError() stringメソッドを定義しており、エラーの文字列表現を返します。 -
os.PathError:- ファイルパスに関連する操作(例: ファイルのオープン、読み書き、ディレクトリの作成など)でエラーが発生した場合に返されるエラー型です。
PathError構造体は、エラーが発生した操作 (Opstring)、関連するファイルパス (Pathstring)、そして根本的なエラー (Errerror) の3つのフィールドを持ちます。- これにより、エラーが発生した具体的な操作とパス、そしてその根本原因を詳細に把握できます。
-
os.LinkError:- シンボリックリンクに関連する操作(例:
os.Link,os.Symlinkなど)でエラーが発生した場合に返されるエラー型です。 LinkError構造体は、操作 (Opstring)、古いパス (Oldstring)、新しいパス (Newstring)、そして根本的なエラー (Errerror) の4つのフィールドを持ちます。PathErrorと同様に、リンク操作における詳細なエラー情報を提供します。
- シンボリックリンクに関連する操作(例:
-
os.IsExist(err error) bool:- 与えられたエラー
errが、ファイルやディレクトリが「既に存在すること」を示すエラーであるかどうかを判定する関数です。 - 例えば、既に存在するファイルを作成しようとした場合などに返されるエラーを検出します。
- 与えられたエラー
-
os.IsNotExist(err error) bool:- 与えられたエラー
errが、ファイルやディレクトリが「存在しないこと」を示すエラーであるかどうかを判定する関数です。 - 存在しないファイルをオープンしようとした場合などに返されるエラーを検出します。
- 与えられたエラー
-
syscallパッケージとエラーコード:syscallパッケージは、オペレーティングシステム(OS)のシステムコールへの低レベルなインターフェースを提供します。- ファイルシステム操作のエラーは、OS固有のシステムコールエラーコード(例:
syscall.ENOENT(No such file or directory),syscall.EEXIST(File exists),syscall.EACCES(Permission denied) など)として返されることがよくあります。 os.IsExistやos.IsNotExistは、これらのシステムコールエラーコードを内部的にチェックして、エラーの性質を判定します。
-
エラーのアンラッピング:
PathErrorやLinkErrorのようなラッパーエラーは、内部に別のエラー(根本的なエラー)を含んでいます。os.IsExistやos.IsNotExistがこれらのラッパーエラーを正確に処理するためには、ラッパーから根本的なエラーを取り出し(アンラッピングし)、その根本的なエラーを評価する必要があります。このコミットでは、switch pe := err.(type)構文を使用して、エラーの型アサーションを行い、PathErrorやLinkErrorの場合に内部のErrフィールドを取り出すように変更されています。
これらの概念を理解することで、コミットがなぜ重要であり、どのようにosパッケージのエラーハンドリングを改善しているのかが明確になります。
技術的詳細
このコミットの技術的詳細は、主にosパッケージ内のプラットフォーム固有のエラー判定ロジック(error_plan9.go, error_posix.go, error_windows.go)と、それらを検証するためのテストコード(error_test.go)の変更に集約されます。
エラー判定ロジックの変更点
各プラットフォーム固有のisExist, isNotExist, isPermission関数において、エラーの型チェックとアンラッピングのロジックが改善されています。
変更前:
多くの場合、エラーが*PathError型であるかどうかをif pe, ok := err.(*PathError); okのような形でチェックし、その場合にpe.Errを評価していました。しかし、LinkErrorの考慮が不足していたり、nilエラーの初期チェックが不十分だったりするケースがありました。
変更後:
switch pe := err.(type)構文が導入され、より網羅的かつ明確なエラー型のハンドリングが行われるようになりました。
case nil:: エラーがnilの場合、falseを返すように明示的に処理されます。これは、エラーが存在しない場合は当然「存在エラー」や「非存在エラー」ではないという論理を強化します。case *PathError:: エラーが*PathError型の場合、その内部のpe.Errを実際の評価対象のエラーとして使用します。case *LinkError:: エラーが*LinkError型の場合も同様に、その内部のpe.Errを評価対象のエラーとして使用します。
この変更により、os.IsExistやos.IsNotExistが、PathErrorやLinkErrorによってラップされた根本的なエラーを正確に識別できるようになります。例えば、os.OpenがPathErrorを返し、その内部のErrがsyscall.ENOENTである場合、os.IsNotExistは正しくtrueを返すようになります。
テストコードの追加
src/pkg/os/error_test.goにTestIsExistという新しいテスト関数と、そのテストケースを定義するisExistTestsというスライスが追加されました。
-
isExistTests:- このテストスライスは、様々なエラー型(
*os.PathError,*os.LinkError,nil)と、それらがos.ErrInvalid,os.ErrPermission,os.ErrExist,os.ErrNotExistといった内部エラーをラップしている場合の組み合わせを網羅しています。 - 各テストケースには、
os.IsExistが期待する結果(isフィールド)と、os.IsNotExistが期待する結果(isnotフィールド)が定義されています。 - これにより、
PathErrorやLinkErrorが内部に持つエラーの種類に応じて、IsExistとIsNotExistが正しく動作するかどうかを検証します。
- このテストスライスは、様々なエラー型(
-
TestIsExist関数:isExistTestsスライスをイテレートし、各テストケースに対してos.IsExistとos.IsNotExistを呼び出します。- 関数の戻り値が期待される結果と異なる場合、
t.Errorfを使用してテスト失敗を報告します。これにより、エラーの型、値、そして期待される結果と実際の結果が詳細にログに出力され、デバッグが容易になります。
このテストの追加は、単にバグを修正するだけでなく、将来的な回帰を防ぎ、osパッケージのエラーハンドリングの信頼性を高める上で非常に重要です。特に、異なるOS環境での挙動の違いを考慮したテストケースが含まれていることで、クロスプラットフォームでの一貫性が保証されます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下の4つのファイルにわたります。
src/pkg/os/error_plan9.gosrc/pkg/os/error_posix.gosrc/pkg/os/error_test.gosrc/pkg/os/error_windows.go
それぞれのファイルでの具体的な変更は以下の通りです。
src/pkg/os/error_plan9.go
isExist, isNotExist, isPermission関数の内部ロジックが変更されました。
if pe, ok := err.(*PathError); ok の形式から、switch pe := err.(type) 構文に置き換えられ、nil、*PathError、*LinkErrorの各ケースを明示的に処理するようになりました。
--- a/src/pkg/os/error_plan9.go
+++ b/src/pkg/os/error_plan9.go
@@ -5,30 +5,36 @@
package os
func isExist(err error) bool {
- if err == nil {
+ switch pe := err.(type) {
+ case nil:
return false
- }
- if pe, ok := err.(*PathError); ok {
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return contains(err.Error(), " exists")
}
func isNotExist(err error) bool {
- if err == nil {
+ switch pe := err.(type) {
+ case nil:
return false
- }
- if pe, ok := err.(*PathError); ok {
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return contains(err.Error(), "does not exist")
}
func isPermission(err error) bool {
- if err == nil {
+ switch pe := err.(type) {
+ case nil:
return false
- }
- if pe, ok := err.(*PathError); ok {
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return contains(err.Error(), "permission denied")
src/pkg/os/error_posix.go
isExist, isNotExist, isPermission関数の内部ロジックが変更されました。
Plan 9版と同様に、switch pe := err.(type) 構文が導入され、nil、*PathError、*LinkErrorの各ケースを処理するようになりました。
--- a/src/pkg/os/error_posix.go
+++ b/src/pkg/os/error_posix.go
@@ -9,21 +9,36 @@ package os
import "syscall"
func isExist(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return err == syscall.EEXIST || err == ErrExist
}
func isNotExist(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
func isPermission(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
src/pkg/os/error_test.go
isExistTestsという新しいテストケースのスライスと、TestIsExistというテスト関数が追加されました。
--- a/src/pkg/os/error_test.go
+++ b/src/pkg/os/error_test.go
@@ -79,3 +79,30 @@ func checkErrorPredicate(predName string, pred func(error) bool, err error) stri
}
return ""
}
+
+var isExistTests = []struct {
+ err error
+ is bool
+ isnot bool
+}{
+ {&os.PathError{Err: os.ErrInvalid}, false, false},
+ {&os.PathError{Err: os.ErrPermission}, false, false},
+ {&os.PathError{Err: os.ErrExist}, true, false},
+ {&os.PathError{Err: os.ErrNotExist}, false, true},
+ {&os.LinkError{Err: os.ErrInvalid}, false, false},
+ {&os.LinkError{Err: os.ErrPermission}, false, false},
+ {&os.LinkError{Err: os.ErrExist}, true, false},
+ {&os.LinkError{Err: os.ErrNotExist}, false, true},
+ {nil, false, false},
+}
+
+func TestIsExist(t *testing.T) {
+ for _, tt := range isExistTests {
+ if is := os.IsExist(tt.err); is != tt.is {
+ t.Errorf("os.IsExist(%T %v) = %v, want %v", tt.err, tt.err, is, tt.is)
+ }
+ if isnot := os.IsNotExist(tt.err); isnot != tt.isnot {
+ t.Errorf("os.IsNotExist(%T %v) = %v, want %v", tt.err, tt.err, isnot, tt.isnot)
+ }
+ }
+}
src/pkg/os/error_windows.go
isExist, isNotExist, isPermission関数の内部ロジックが変更されました。
Plan 9版やPOSIX版と同様に、switch pe := err.(type) 構文が導入され、nil、*PathError、*LinkErrorの各ケースを処理するようになりました。
--- a/src/pkg/os/error_windows.go
+++ b/src/pkg/os/error_windows.go
@@ -7,10 +7,12 @@ package os
import "syscall"
func isExist(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
err = pe.Err
- }\n-\tif pe, ok := err.(*LinkError); ok {
+ case *LinkError:
err = pe.Err
}
return err == syscall.ERROR_ALREADY_EXISTS ||
@@ -18,7 +20,12 @@ func isExist(err error) bool {
}
func isNotExist(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return err == syscall.ERROR_FILE_NOT_FOUND ||
@@ -26,7 +33,12 @@ func isNotExist(err error) bool {
}
func isPermission(err error) bool {
- if pe, ok := err.(*PathError); ok {
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *PathError:
+ err = pe.Err
+ case *LinkError:
err = pe.Err
}
return err == syscall.ERROR_ACCESS_DENIED || err == ErrPermission
コアとなるコードの解説
このコミットのコアとなる変更は、osパッケージのエラー判定関数(isExist, isNotExist, isPermission)が、PathErrorとLinkErrorというラッパーエラー型を適切に「アンラップ」して、その内部の根本的なエラーを評価するように修正された点です。
エラーアンラッピングの改善
以前のコードでは、エラーが*PathError型であるかどうかをif pe, ok := err.(*PathError); okのような形でチェックしていました。しかし、このアプローチでは、LinkErrorのような他のラッパーエラー型が考慮されていなかったり、nilエラーの初期チェックが不足していたりする可能性がありました。
新しいコードでは、switch pe := err.(type)構文が導入されています。これはGo言語の型スイッチと呼ばれる機能で、変数の動的な型に基づいて異なる処理を実行できます。
switch pe := err.(type) {
case nil:
// エラーがnilの場合の処理
return false
case *PathError:
// エラーがPathError型の場合、内部のErrフィールドを評価対象とする
err = pe.Err
case *LinkError:
// エラーがLinkError型の場合も同様に、内部のErrフィールドを評価対象とする
err = pe.Err
}
// ここで、errは元のエラーか、PathError/LinkErrorの内部エラーのいずれかになる
// その後、プラットフォーム固有の具体的なエラーコード(syscall.EEXISTなど)や文字列をチェックする
この変更により、以下の点が改善されました。
- 網羅的なエラー型ハンドリング:
PathErrorだけでなく、LinkErrorも明示的に処理対象に加えることで、シンボリックリンク操作で発生するエラーも正確に判定できるようになりました。 nilエラーの明確な処理:nilエラーが渡された場合にfalseを返すという挙動が明示的に定義され、コードの意図がより明確になりました。- 一貫性のあるエラー評価:
PathErrorやLinkErrorによってラップされた根本的なエラーが、各プラットフォーム固有のエラー判定ロジック(例:syscall.EEXISTとの比較や、エラー文字列のcontainsチェック)に渡されるため、より正確な判定が可能になります。
テストカバレッジの向上
src/pkg/os/error_test.goに追加されたisExistTestsスライスとTestIsExist関数は、この変更の正しさを検証するために不可欠です。
isExistTestsは、PathErrorとLinkErrorがそれぞれos.ErrInvalid, os.ErrPermission, os.ErrExist, os.ErrNotExistといった様々な内部エラーをラップしているケースを網羅しています。また、nilエラーのケースも含まれています。
TestIsExist関数は、これらのテストケースを順に実行し、os.IsExistとos.IsNotExistが期待通りのブール値を返すかどうかを確認します。これにより、エラーアンラッピングのロジックが正しく機能していること、そして異なる種類のエラーが適切に分類されていることが保証されます。
このテストの追加は、単に現在のバグを修正するだけでなく、将来的に同様の問題が再発するのを防ぐための重要な安全網となります。
関連リンク
- Go言語の
osパッケージドキュメント: https://pkg.go.dev/os - Go言語のエラーハンドリングに関する公式ブログ記事(古いものですが、基本的な概念は共通): https://go.dev/blog/error-handling-and-go
- Go言語の
syscallパッケージドキュメント: https://pkg.go.dev/syscall
参考にした情報源リンク
- コミットハッシュ:
b9b29ce2ba9e45de8372d81292b52f8623237220 - GitHub上のコミットページ: https://github.com/golang/go/commit/b9b29ce2ba9e45de8372d81292b52f8623237220
- Gerrit Code Review (Goプロジェクトのコードレビューシステム): https://golang.org/cl/6442080 (コミットメッセージに記載されているCLリンク)
- Go言語の公式ドキュメントおよびソースコード
- Go言語のエラーハンドリングに関する一般的な知識