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

[インデックス 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つのヘルパー関数、IsExistIsNotExistIsPermissionが追加されました。将来的にはさらに追加される可能性もありますが、エラーをそのように考えることを推奨しないために、セットは非常に小さく保たれています。

変更の背景

Go言語は、その設計思想として「ポータビリティ」を重視しています。しかし、初期のosパッケージでは、ファイル操作やシステムコールに関連するエラーを、基盤となるオペレーティングシステム(OS)が返すPOSIXエラーコード(例: ENOENT (No such entity), EINVAL (Invalid argument), EPERM (Operation not permitted) など)を直接osパッケージの定数として公開していました。

このアプローチにはいくつかの問題がありました。

  1. OS間の非互換性: POSIXエラーコードはUNIX系システムでは共通していますが、Windowsのような非UNIX系システムでは異なるエラーコード体系を持っています。また、同じUNIX系システムであっても、特定のエラーコードが意味する内容が微妙に異なる場合や、特定のエラーコードが存在しない場合がありました。これにより、os.EINVALのような定数に直接依存するコードは、異なるOSでコンパイルエラーになったり、予期せぬ動作を引き起こしたりする可能性がありました。
  2. 抽象化の欠如: osパッケージはOSの抽象化レイヤーを提供するべきですが、OS固有のエラーコードを直接公開することは、この抽象化を損なっていました。開発者はOSの詳細に踏み込むことなく、GoのAPIを通じてエラーを処理できるべきです。
  3. エラー処理の複雑化: 特定の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.ENOENTsyscall.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.PathErrorErrフィールドを調べたり、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.gosrc/pkg/os/error_plan9.gosrc/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
  • ErrInvalidErrPermissionErrExistErrNotExistという新しいerror型の変数が導入されました。これらはerrors.Newを使って、OS固有のエラーコードではなく、意味のある文字列で初期化されています。これにより、これらのエラーはOSに依存しない形で表現されます。

src/pkg/os/error_plan9.go

このファイルはPlan 9 OS向けのエラーハンドリングロジックを含んでいます。ここでは、新しいIsExistIsNotExistIsPermissionヘルパー関数が定義されています。

--- 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など)が削除されました。
  • 代わりに、IsExistIsNotExistIsPermission関数が追加されました。これらの関数は、エラーがPathError型であればその内部のErrを抽出し、エラーメッセージ文字列が特定のキーワード(" exists", "does not exist", "permission denied")を含むかどうかをチェックします。これはPlan 9特有の実装であり、エラーメッセージの文字列比較によってエラーの種類を判別しています。
  • containsヘルパー関数も追加され、文字列検索を効率的に行っています。

src/pkg/os/error_posix.go

このファイルはPOSIX準拠OS向けのエラーハンドリングロジックを含んでいます。ここでも、新しいIsExistIsNotExistIsPermissionヘルパー関数が定義されています。

--- 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固有のエラー定数を公開しなくなりました。
  • IsExistIsNotExistIsPermission関数が追加されました。これらの関数は、エラーが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言語のソースコード (特に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番号は読み取れませんでした。