[インデックス 11983] ファイルの概要
このコミットは、Go言語の標準ライブラリであるos
パッケージにおけるエラーハンドリングのポータビリティを向上させるための重要な変更を導入しています。具体的には、オペレーティングシステム(OS)固有のPOSIXエラー定数(例: os.EINVAL
, os.ENOENT
など)をos
パッケージから削除し、代わりにOSに依存しない汎用的なエラーチェックヘルパー関数(os.IsExist
, os.IsNotExist
, os.IsPermission
)を導入しています。これにより、Goプログラムが異なるOS環境でより一貫したエラー処理を行えるようになります。
コミット
commit 56069f0333ea5464a5d6688c55a03b607b01ad11
Author: Rob Pike <r@golang.org>
Date: Fri Feb 17 10:04:29 2012 +1100
os: delete os.EINVAL and so on
The set of errors forwarded by the os package varied with system and
was therefore non-portable.
Three helpers added for portable error checking: IsExist, IsNotExist, and IsPermission.
One or two more may need to come, but let's keep the set very small to discourage
thinking about errors that way.
R=mikioh.mikioh, gustavo, r, rsc
CC=golang-dev
https://golang.org/cl/5672047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/56069f0333ea5464a5d6688c55a03b607b01ad11
元コミット内容
os: delete os.EINVAL and so on
このコミットの目的は、os
パッケージが提供していたos.EINVAL
などのOS固有のエラー定数を削除することです。これらのエラー定数はシステムによって異なり、そのためポータビリティがありませんでした。代わりに、ポータブルなエラーチェックのための3つのヘルパー関数、IsExist
、IsNotExist
、IsPermission
が追加されました。将来的にはさらに追加される可能性もありますが、エラーをそのように考えることを推奨しないために、セットは非常に小さく保たれています。
変更の背景
Go言語は、その設計思想として「ポータビリティ」を重視しています。しかし、初期のos
パッケージでは、ファイル操作やシステムコールに関連するエラーを、基盤となるオペレーティングシステム(OS)が返すPOSIXエラーコード(例: ENOENT
(No such entity), EINVAL
(Invalid argument), EPERM
(Operation not permitted) など)を直接os
パッケージの定数として公開していました。
このアプローチにはいくつかの問題がありました。
- OS間の非互換性: POSIXエラーコードはUNIX系システムでは共通していますが、Windowsのような非UNIX系システムでは異なるエラーコード体系を持っています。また、同じUNIX系システムであっても、特定のエラーコードが意味する内容が微妙に異なる場合や、特定のエラーコードが存在しない場合がありました。これにより、
os.EINVAL
のような定数に直接依存するコードは、異なるOSでコンパイルエラーになったり、予期せぬ動作を引き起こしたりする可能性がありました。 - 抽象化の欠如:
os
パッケージはOSの抽象化レイヤーを提供するべきですが、OS固有のエラーコードを直接公開することは、この抽象化を損なっていました。開発者はOSの詳細に踏み込むことなく、GoのAPIを通じてエラーを処理できるべきです。 - エラー処理の複雑化: 特定のOSエラーコードに依存するエラー処理ロジックは、コードの可読性を低下させ、メンテナンスを困難にしていました。より高レベルで意味のあるエラーカテゴリ(例: ファイルが存在しない、パーミッションがない)でエラーをチェックできる方が、より堅牢で理解しやすいコードになります。
これらの問題を解決するため、Goチームはos
パッケージからOS固有のエラー定数を削除し、より抽象的でポータブルなエラーチェックメカニズムを導入することを決定しました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびOSに関する基本的な知識が必要です。
1. Go言語のエラーハンドリング
Go言語では、エラーは組み込みのerror
インターフェースによって表現されます。このインターフェースは、Error() string
という単一のメソッドを持ち、エラーメッセージを文字列として返します。
type error interface {
Error() string
}
関数は通常、最後の戻り値としてerror
型を返します。エラーがない場合はnil
を返します。
func doSomething() (result string, err error) {
// ... 処理 ...
if somethingWentWrong {
return "", errors.New("something went wrong") // エラーを返す
}
return "success", nil // 成功を返す
}
エラーをチェックする際は、if err != nil
というイディオムが広く使われます。
2. syscall
パッケージ
syscall
パッケージは、Goプログラムから基盤となるOSのシステムコールに直接アクセスするための低レベルなインターフェースを提供します。これには、ファイル操作、ネットワーク通信、プロセス管理など、OSが提供する基本的な機能が含まれます。
syscall
パッケージはOS固有の定数や関数を多く含んでおり、例えばUNIX系システムではsyscall.ENOENT
、syscall.EINVAL
といったPOSIXエラーコードが定義されています。これらの定数は、システムコールが失敗した際に返されるエラーコードをGoのerror
型にラップしたものです。
3. os.PathError
os.PathError
は、ファイルパスに関連する操作で発生したエラーをラップするためにos
パッケージで定義されている構造体です。
type PathError struct {
Op string // 操作 (例: "open", "read", "write")
Path string // 操作対象のファイルパス
Err error // 元のエラー (通常はsyscall.Errnoなど)
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
os.PathError
は、どの操作がどのパスで失敗したかというコンテキストを提供し、エラーメッセージをより詳細にします。この構造体のErr
フィールドには、基盤となるOSが返した具体的なエラー(例えばsyscall.ENOENT
)が含まれることがあります。
4. POSIXエラーコード
POSIX (Portable Operating System Interface) は、UNIX系OSの標準化されたAPIセットです。これには、ファイルシステム、プロセス、スレッド、ネットワークなどに関するシステムコールと、それらが返すエラーコードが含まれます。
一般的なPOSIXエラーコードの例:
ENOENT
(Error No ENTry): ファイルやディレクトリが存在しない。EINVAL
(Error INVALid argument): 不正な引数。EACCES
(Error ACCESs denied): アクセス権がない。EPERM
(Error PERMission denied): 操作が許可されていない。EEXIST
(Error EXISTs): ファイルやディレクトリが既に存在する。
これらのエラーコードは数値で表現されますが、Goのsyscall
パッケージでは対応するerror
型の定数としてラップされています。
技術的詳細
このコミットの技術的な核心は、os
パッケージがOS固有のエラー定数を直接公開するのをやめ、代わりにエラーの「種類」を抽象化するヘルパー関数を提供する点にあります。
変更前のアプローチの問題点
変更前は、開発者は以下のようにOS固有のエラー定数を使ってエラーの種類を判別していました。
import "os"
func readFile(filename string) {
f, err := os.Open(filename)
if err != nil {
if pe, ok := err.(*os.PathError); ok {
if pe.Err == os.ENOENT { // OS固有のエラー定数に依存
fmt.Println("ファイルが見つかりません:", filename)
return
}
}
fmt.Println("ファイルを開く際にエラーが発生しました:", err)
}
// ...
}
このコードは、os.ENOENT
がすべてのOSで同じ意味を持つとは限らないため、ポータビリティの問題を抱えていました。特にWindowsでは、ENOENT
に相当するエラーコードが異なる場合があります。
新しいアプローチ:ヘルパー関数の導入
このコミットでは、os
パッケージに以下の3つのブール型ヘルパー関数が導入されました。
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
これらの関数は、与えられたerror
が、ファイルが存在しない、ファイルが既に存在する、またはパーミッションがないといった特定の条件を示すかどうかを、OSに依存しない形で判定します。
これらのヘルパー関数は、内部的にos.PathError
のErr
フィールドを調べたり、OS固有のエラーコード(syscall.ENOENT
など)や、Goのerrors.New
で作成された汎用的なエラー文字列をチェックしたりすることで、ポータビリティを確保しています。
例えば、os.IsNotExist
関数は、エラーがsyscall.ENOENT
であるか、またはos.ErrNotExist
(新しく導入された汎用エラー)であるかをチェックします。os.ErrNotExist
は、errors.New("file does not exit")
として定義されており、OS固有のエラーコードに直接依存しません。
os.EINVAL
からos.ErrInvalid
への変更
コミットメッセージにあるos.EINVAL
は、os.ErrInvalid
という新しい汎用エラーに置き換えられました。os.ErrInvalid
もまた、errors.New("invalid argument")
として定義されており、OS固有のsyscall.EINVAL
とは異なります。
これにより、os
パッケージのAPIは、OS固有のエラー定数から完全に切り離され、より抽象的でポータブルなエラー表現に移行しました。
影響範囲
この変更は、os
パッケージだけでなく、net
パッケージやio/ioutil
など、os
パッケージのエラー定数に依存していた多くの標準ライブラリのコードに影響を与えました。これらのコードは、新しいos.Is*
ヘルパー関数や、必要に応じてsyscall
パッケージの直接的なエラー定数を使用するように修正されました。
また、Go 1のリリースノート(doc/go1.html
)にもこの変更が明記され、古いPOSIXエラー値を使用しているコードはコンパイルに失敗し、手動で更新する必要があることが示されています。これは、Goの互換性保証(Go 1以降は後方互換性を維持する)の例外的なケースであり、APIのクリーンアップとポータビリティ向上のために行われた重要な変更でした。
コアとなるコードの変更箇所
このコミットの核心的な変更は、src/pkg/os/error.go
、src/pkg/os/error_plan9.go
、src/pkg/os/error_posix.go
の3つのファイルに集約されています。
src/pkg/os/error.go
このファイルでは、OSに依存しない汎用的なエラー定数と、PathError
構造体が定義されています。
--- a/src/pkg/os/error.go
+++ b/src/pkg/os/error.go
@@ -4,6 +4,18 @@
package os
+import (
+ "errors"
+)
+
+// Portable analogs of some common system call errors.
+var (
+ ErrInvalid = errors.New("invalid argument")
+ ErrPermission = errors.New("permission denied")
+ ErrExist = errors.New("file already exists")
+ ErrNotExist = errors.New("file does not exit")
+)
+
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
ErrInvalid
、ErrPermission
、ErrExist
、ErrNotExist
という新しいerror
型の変数が導入されました。これらはerrors.New
を使って、OS固有のエラーコードではなく、意味のある文字列で初期化されています。これにより、これらのエラーはOSに依存しない形で表現されます。
src/pkg/os/error_plan9.go
このファイルはPlan 9 OS向けのエラーハンドリングロジックを含んでいます。ここでは、新しいIsExist
、IsNotExist
、IsPermission
ヘルパー関数が定義されています。
--- a/src/pkg/os/error_plan9.go
+++ b/src/pkg/os/error_plan9.go
@@ -4,34 +4,38 @@
package os
-import (
- "errors"
- "syscall"
-)
+// IsExist returns whether the error is known to report that a file already exists.
+func IsExist(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return contains(err.Error(), " exists")
+}
-var (
- Eshortstat = errors.New("stat buffer too small")
- Ebadstat = errors.New("malformed stat buffer")
- Ebadfd = errors.New("fd out of range or not open")
- Ebadarg = errors.New("bad arg in system call")
- Enotdir = errors.New("not a directory")
- Enonexist = errors.New("file does not exist")
- Eexist = errors.New("file already exists")
- Eio = errors.New("i/o error")
- Eperm = errors.New("permission denied")
+// IsNotExist returns whether the error is known to report that a file does not exist.
+func IsNotExist(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return contains(err.Error(), "does not exist")
+}
- EINVAL = Ebadarg
- ENOTDIR = Enotdir
- ENOENT = Enonexist
- EEXIST = Eexist
- EIO = Eio
- EACCES = Eperm
- EPERM = Eperm
- EISDIR = syscall.EISDIR
+// IsPermission returns whether the error is known to report that permission is denied.
+func IsPermission(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return contains(err.Error(), "permission denied")
+}
- EBADF = errors.New("bad file descriptor")
- ENAMETOOLONG = errors.New("file name too long")
- ERANGE = errors.New("math result not representable")
- EPIPE = errors.New("Broken Pipe")
- EPLAN9 = errors.New("not supported by plan 9")
-)
+// contains is a local version of strings.Contains. It knows len(sep) > 1.
+func contains(s, sep string) bool {
+ n := len(sep)
+ c := sep[0]
+ for i := 0; i+n <= len(s); i++ {
+ if s[i] == c && s[i:i+n] == sep {
+ return true
+ }
+ }
+ return false
+}
- 以前定義されていた
Eshortstat
,Ebadstat
,Ebadfd
,Ebadarg
,Enotdir
,Enonexist
,Eexist
,Eio
,Eperm
などのOS固有のエラー定数、およびそれらのエイリアス(EINVAL
,ENOENT
など)が削除されました。 - 代わりに、
IsExist
、IsNotExist
、IsPermission
関数が追加されました。これらの関数は、エラーがPathError
型であればその内部のErr
を抽出し、エラーメッセージ文字列が特定のキーワード(" exists", "does not exist", "permission denied")を含むかどうかをチェックします。これはPlan 9特有の実装であり、エラーメッセージの文字列比較によってエラーの種類を判別しています。 contains
ヘルパー関数も追加され、文字列検索を効率的に行っています。
src/pkg/os/error_posix.go
このファイルはPOSIX準拠OS向けのエラーハンドリングロジックを含んでいます。ここでも、新しいIsExist
、IsNotExist
、IsPermission
ヘルパー関数が定義されています。
--- a/src/pkg/os/error_posix.go
+++ b/src/pkg/os/error_posix.go
@@ -8,44 +8,29 @@ package os
import "syscall"
-// Commonly known Unix errors.
-var (
- EPERM error = syscall.EPERM
- ENOENT error = syscall.ENOENT
- ESRCH error = syscall.ESRCH
- EINTR error = syscall.EINTR
- EIO error = syscall.EIO
- E2BIG error = syscall.E2BIG
- ENOEXEC error = syscall.ENOEXEC
- EBADF error = syscall.EBADF
- ECHILD error = syscall.ECHILD
- EDEADLK error = syscall.EDEADLK
- ENOMEM error = syscall.ENOMEM
- EACCES error = syscall.EACCES
- EFAULT error = syscall.EFAULT
- EBUSY error = syscall.EBUSY
- EEXIST error = syscall.EEXIST
- EXDEV error = syscall.EXDEV
- ENODEV error = syscall.ENODEV
- ENOTDIR error = syscall.ENOTDIR
- EISDIR error = syscall.EISDIR
- EINVAL error = syscall.EINVAL
- ENFILE error = syscall.ENFILE
- EMFILE error = syscall.EMFILE
- ENOTTY error = syscall.ENOTTY
- EFBIG error = syscall.EFBIG
- ENOSPC error = syscall.ENOSPC
- ESPIPE error = syscall.ESPIPE
- EROFS error = syscall.EROFS
- EMLINK error = syscall.EMLINK
- EPIPE error = syscall.EPIPE
- EAGAIN error = syscall.EAGAIN
- EDOM error = syscall.EDOM
- ERANGE error = syscall.ERANGE
- EADDRINUSE error = syscall.EADDRINUSE
- ECONNREFUSED error = syscall.ECONNREFUSED
- ENAMETOOLONG error = syscall.ENAMETOOLONG
- EAFNOSUPPORT error = syscall.EAFNOSUPPORT
- ETIMEDOUT error = syscall.ETIMEDOUT
- ENOTCONN error = syscall.ENOTCONN
-)
+// IsExist returns whether the error is known to report that a file already exists.
+// It is satisfied by ErrExist as well as some syscall errors.
+func IsExist(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return err == syscall.EEXIST || err == ErrExist
+}
+
+// IsNotExist returns whether the error is known to report that a file does not exist.
+// It is satisfied by ErrNotExist as well as some syscall errors.
+func IsNotExist(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return err == syscall.ENOENT || err == ErrNotExist
+}
+
+// IsPermission returns whether the error is known to report that permission is denied.
+// It is satisfied by ErrPermission as well as some syscall errors.
+func IsPermission(err error) bool {
+ if pe, ok := err.(*PathError); ok {
+ err = pe.Err
+ }
+ return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
+}
- 以前定義されていた多数の
syscall
パッケージのエラー定数へのエイリアスが削除されました。これにより、os
パッケージが直接これらのOS固有のエラー定数を公開しなくなりました。 IsExist
、IsNotExist
、IsPermission
関数が追加されました。これらの関数は、エラーがPathError
型であればその内部のErr
を抽出し、それがsyscall
パッケージの対応するエラー定数(例:syscall.EEXIST
)またはos
パッケージで新しく定義された汎用エラー(例:ErrExist
)のいずれかと一致するかどうかをチェックします。これにより、POSIX準拠システムにおけるポータブルなエラーチェックが実現されます。
これらの変更により、Goのコードは以下のようにポータブルなエラーチェックを行うことができるようになりました。
import "os"
func readFile(filename string) {
f, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) { // OSに依存しない形でエラーをチェック
fmt.Println("ファイルが見つかりません:", filename)
return
}
if os.IsPermission(err) {
fmt.Println("ファイルへのアクセス権がありません:", filename)
return
}
fmt.Println("ファイルを開く際にエラーが発生しました:", err)
}
// ...
}
コアとなるコードの解説
このコミットのコアとなるコードは、os
パッケージからOS固有のエラー定数を削除し、代わりにポータブルなエラーチェック関数を導入した点です。
1. 汎用エラー定数の導入 (src/pkg/os/error.go
)
var (
ErrInvalid = errors.New("invalid argument")
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exit")
)
os
パッケージは、OS固有のsyscall
エラー定数への直接的な依存をなくすために、これらの汎用的なerror
変数を導入しました。- これらは
errors.New
を使って、人間が読めるエラーメッセージを持つ新しいエラーインスタンスを作成します。これにより、これらのエラーは特定のOSのエラーコードに縛られず、Goのerror
インターフェースのセマンティクスに沿った形で表現されます。 - 例えば、ファイルが存在しないエラーは、Windowsでは異なる数値コードを持つかもしれませんが、Goの
os.ErrNotExist
は常に同じerror
インターフェースの実装として扱われます。
2. ポータブルなエラーチェック関数の実装 (src/pkg/os/error_plan9.go
および src/pkg/os/error_posix.go
)
これらのファイルは、各OS(Plan 9とPOSIX準拠システム)向けにIsExist
, IsNotExist
, IsPermission
関数の具体的な実装を提供します。
Plan 9向け (src/pkg/os/error_plan9.go
):
func IsExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return contains(err.Error(), " exists")
}
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return contains(err.Error(), "does not exist")
}
func IsPermission(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return contains(err.Error(), "permission denied")
}
func contains(s, sep string) bool {
n := len(sep)
c := sep[0]
for i := 0; i+n <= len(s); i++ {
if s[i] == c && s[i:i+n] == sep {
return true
}
}
return false
}
- Plan 9では、エラーメッセージの文字列を解析してエラーの種類を判別しています。これは、Plan 9のエラー報告メカニズムが他のOSと異なるためです。
contains
ヘルパー関数は、エラーメッセージ文字列内に特定の部分文字列(例: " exists")が含まれているかを効率的にチェックするために使用されます。PathError
にラップされている場合は、その内部のErr
フィールドを抽出してから文字列比較を行います。
POSIX準拠システム向け (src/pkg/os/error_posix.go
):
func IsExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.EEXIST || err == ErrExist
}
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
func IsPermission(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
}
- POSIX準拠システムでは、
syscall
パッケージが提供するOS固有のエラー定数(例:syscall.EEXIST
,syscall.ENOENT
,syscall.EACCES
,syscall.EPERM
)と、os
パッケージで新しく定義された汎用エラー(例:ErrExist
,ErrNotExist
,ErrPermission
)のいずれかと一致するかどうかをチェックします。 - ここでも、エラーが
PathError
型であれば、その内部のErr
フィールドを抽出してから比較を行います。
これらの実装により、GoのユーザーはOSの詳細を意識することなく、os.IsExist(err)
のようなシンプルな呼び出しでエラーの種類を判別できるようになりました。これにより、Goプログラムのポータビリティと堅牢性が大幅に向上しました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語のエラーハンドリングに関する公式ブログ記事 (Go 1.13以降の
errors.Is
とerrors.As
について): https://go.dev/blog/go1.13-errors- このコミットはGo 1.0以前の変更ですが、Goのエラーハンドリングの進化を理解する上で役立ちます。
- POSIX標準: https://pubs.opengroup.org/onlinepubs/9699919799/
参考にした情報源リンク
- Go言語のソースコード (特に
src/os
およびsrc/syscall
パッケージ) - Go 1 Release Notes: https://go.dev/doc/go1
- このコミットの変更は、Go 1のリリースノートの"The os package"セクションに記載されています。
- Go言語の
errors
パッケージのドキュメント: https://pkg.go.dev/errors - Go言語の
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go言語のエラーハンドリングに関する一般的な記事やチュートリアル。
- POSIXエラーコードに関する情報源。
- Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5672047
- コミットメッセージに記載されているGerritのリンクは、この変更に関する詳細な議論やレビューの履歴を提供します。
- GoのIssue Tracker: https://go.dev/issue
- 関連するIssueが存在する可能性がありますが、このコミットメッセージからは直接的なIssue番号は読み取れませんでした。