[インデックス 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) boolfunc IsNotExist(err error) boolfunc 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番号は読み取れませんでした。