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

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

このコミットは、Go言語の標準ライブラリosパッケージにおけるエラーハンドリング、特にos.IsExistおよびos.IsNotExist関数がPathErrorLinkErrorを適切に処理することを確認するためのテストを追加し、関連するエラー判定ロジックを修正するものです。これにより、ファイルシステム操作で発生する可能性のある様々なエラータイプに対して、より堅牢で一貫性のあるエラー判定が保証されます。

コミット

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関数が、PathErrorLinkErrorという特定のエラー型を適切に処理するかどうかをテストすることに焦点を当てていることを示しています。

変更の背景

Go言語のosパッケージは、ファイルシステム操作を行うための基本的な機能を提供します。これらの操作中にエラーが発生した場合、Goはエラーインターフェースを介してエラーを返します。ファイルやディレクトリの存在確認は一般的な操作であり、その際に「ファイルが存在する」「ファイルが存在しない」といった特定の状態を正確に判定できることは、アプリケーションのロバスト性を高める上で非常に重要です。

os.IsExistos.IsNotExistといった関数は、この目的のために提供されています。しかし、ファイルシステム操作で発生するエラーは、単なる「存在しない」といった単純なものではなく、PathError(パスに関連するエラー)やLinkError(シンボリックリンクに関連するエラー)のように、より詳細な情報を含むラッパーエラーとして返されることがあります。

このコミット以前は、これらのラッパーエラーがIsExistIsNotExistによって適切に「アンラップ」され、内部の根本的なエラー(例: syscall.ENOENTsyscall.EEXIST)が正確に評価されているかどうかのテストが不十分だった可能性があります。そのため、開発者は、PathErrorLinkErrorが返された場合に、IsExistIsNotExistが期待通りに動作しないという問題に直面する可能性がありました。

このコミットの背景には、osパッケージのエラーハンドリングの堅牢性を向上させ、開発者がファイルシステムエラーをより確実に判定できるようにするという目的があります。特に、PathErrorLinkErrorのような一般的なラッパーエラー型を考慮に入れることで、異なるオペレーティングシステム(Plan 9, POSIX, Windows)間でのエラー判定の一貫性を保つことが目指されています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とosパッケージのエラー型に関する知識が必要です。

  1. errorインターフェース: Go言語におけるエラーハンドリングの基本です。Goの関数は、エラーが発生した場合にerror型の値を返します。errorインターフェースはError() stringメソッドを定義しており、エラーの文字列表現を返します。

  2. os.PathError:

    • ファイルパスに関連する操作(例: ファイルのオープン、読み書き、ディレクトリの作成など)でエラーが発生した場合に返されるエラー型です。
    • PathError構造体は、エラーが発生した操作 (Op string)、関連するファイルパス (Path string)、そして根本的なエラー (Err error) の3つのフィールドを持ちます。
    • これにより、エラーが発生した具体的な操作とパス、そしてその根本原因を詳細に把握できます。
  3. os.LinkError:

    • シンボリックリンクに関連する操作(例: os.Link, os.Symlinkなど)でエラーが発生した場合に返されるエラー型です。
    • LinkError構造体は、操作 (Op string)、古いパス (Old string)、新しいパス (New string)、そして根本的なエラー (Err error) の4つのフィールドを持ちます。
    • PathErrorと同様に、リンク操作における詳細なエラー情報を提供します。
  4. os.IsExist(err error) bool:

    • 与えられたエラーerrが、ファイルやディレクトリが「既に存在すること」を示すエラーであるかどうかを判定する関数です。
    • 例えば、既に存在するファイルを作成しようとした場合などに返されるエラーを検出します。
  5. os.IsNotExist(err error) bool:

    • 与えられたエラーerrが、ファイルやディレクトリが「存在しないこと」を示すエラーであるかどうかを判定する関数です。
    • 存在しないファイルをオープンしようとした場合などに返されるエラーを検出します。
  6. syscallパッケージとエラーコード:

    • syscallパッケージは、オペレーティングシステム(OS)のシステムコールへの低レベルなインターフェースを提供します。
    • ファイルシステム操作のエラーは、OS固有のシステムコールエラーコード(例: syscall.ENOENT (No such file or directory), syscall.EEXIST (File exists), syscall.EACCES (Permission denied) など)として返されることがよくあります。
    • os.IsExistos.IsNotExistは、これらのシステムコールエラーコードを内部的にチェックして、エラーの性質を判定します。
  7. エラーのアンラッピング:

    • PathErrorLinkErrorのようなラッパーエラーは、内部に別のエラー(根本的なエラー)を含んでいます。
    • os.IsExistos.IsNotExistがこれらのラッパーエラーを正確に処理するためには、ラッパーから根本的なエラーを取り出し(アンラッピングし)、その根本的なエラーを評価する必要があります。このコミットでは、switch pe := err.(type)構文を使用して、エラーの型アサーションを行い、PathErrorLinkErrorの場合に内部の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.IsExistos.IsNotExistが、PathErrorLinkErrorによってラップされた根本的なエラーを正確に識別できるようになります。例えば、os.OpenPathErrorを返し、その内部のErrsyscall.ENOENTである場合、os.IsNotExistは正しくtrueを返すようになります。

テストコードの追加

src/pkg/os/error_test.goTestIsExistという新しいテスト関数と、そのテストケースを定義するisExistTestsというスライスが追加されました。

  • isExistTests:

    • このテストスライスは、様々なエラー型(*os.PathError, *os.LinkError, nil)と、それらがos.ErrInvalid, os.ErrPermission, os.ErrExist, os.ErrNotExistといった内部エラーをラップしている場合の組み合わせを網羅しています。
    • 各テストケースには、os.IsExistが期待する結果(isフィールド)と、os.IsNotExistが期待する結果(isnotフィールド)が定義されています。
    • これにより、PathErrorLinkErrorが内部に持つエラーの種類に応じて、IsExistIsNotExistが正しく動作するかどうかを検証します。
  • TestIsExist関数:

    • isExistTestsスライスをイテレートし、各テストケースに対してos.IsExistos.IsNotExistを呼び出します。
    • 関数の戻り値が期待される結果と異なる場合、t.Errorfを使用してテスト失敗を報告します。これにより、エラーの型、値、そして期待される結果と実際の結果が詳細にログに出力され、デバッグが容易になります。

このテストの追加は、単にバグを修正するだけでなく、将来的な回帰を防ぎ、osパッケージのエラーハンドリングの信頼性を高める上で非常に重要です。特に、異なるOS環境での挙動の違いを考慮したテストケースが含まれていることで、クロスプラットフォームでの一貫性が保証されます。

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

このコミットにおけるコアとなるコードの変更箇所は、主に以下の4つのファイルにわたります。

  1. src/pkg/os/error_plan9.go
  2. src/pkg/os/error_posix.go
  3. src/pkg/os/error_test.go
  4. 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)が、PathErrorLinkErrorというラッパーエラー型を適切に「アンラップ」して、その内部の根本的なエラーを評価するように修正された点です。

エラーアンラッピングの改善

以前のコードでは、エラーが*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など)や文字列をチェックする

この変更により、以下の点が改善されました。

  1. 網羅的なエラー型ハンドリング: PathErrorだけでなく、LinkErrorも明示的に処理対象に加えることで、シンボリックリンク操作で発生するエラーも正確に判定できるようになりました。
  2. nilエラーの明確な処理: nilエラーが渡された場合にfalseを返すという挙動が明示的に定義され、コードの意図がより明確になりました。
  3. 一貫性のあるエラー評価: PathErrorLinkErrorによってラップされた根本的なエラーが、各プラットフォーム固有のエラー判定ロジック(例: syscall.EEXISTとの比較や、エラー文字列のcontainsチェック)に渡されるため、より正確な判定が可能になります。

テストカバレッジの向上

src/pkg/os/error_test.goに追加されたisExistTestsスライスとTestIsExist関数は、この変更の正しさを検証するために不可欠です。

isExistTestsは、PathErrorLinkErrorがそれぞれos.ErrInvalid, os.ErrPermission, os.ErrExist, os.ErrNotExistといった様々な内部エラーをラップしているケースを網羅しています。また、nilエラーのケースも含まれています。

TestIsExist関数は、これらのテストケースを順に実行し、os.IsExistos.IsNotExistが期待通りのブール値を返すかどうかを確認します。これにより、エラーアンラッピングのロジックが正しく機能していること、そして異なる種類のエラーが適切に分類されていることが保証されます。

このテストの追加は、単に現在のバグを修正するだけでなく、将来的に同様の問題が再発するのを防ぐための重要な安全網となります。

関連リンク

参考にした情報源リンク