[インデックス 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
構造体は、エラーが発生した操作 (Op
string)、関連するファイルパス (Path
string)、そして根本的なエラー (Err
error) の3つのフィールドを持ちます。- これにより、エラーが発生した具体的な操作とパス、そしてその根本原因を詳細に把握できます。
-
os.LinkError
:- シンボリックリンクに関連する操作(例:
os.Link
,os.Symlink
など)でエラーが発生した場合に返されるエラー型です。 LinkError
構造体は、操作 (Op
string)、古いパス (Old
string)、新しいパス (New
string)、そして根本的なエラー (Err
error) の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.go
src/pkg/os/error_posix.go
src/pkg/os/error_test.go
src/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言語のエラーハンドリングに関する一般的な知識