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

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

このコミットは、Go言語の標準ライブラリosパッケージにおけるFile型のメソッドが、nilレシーバで呼び出された際の挙動を統一することを目的としています。以前は、nilレシーバで呼び出された場合にクラッシュするものと、そうでないものが混在していましたが、この変更により、すべてのFileメソッドがnilレシーバで呼び出された際にos.ErrInvalidエラーを返すように修正されました。これにより、プログラムの堅牢性が向上し、予期せぬパニックを防ぐことができます。

コミット

  • コミットハッシュ: 4cb086b838548fa5dbdcb502a51b29294e268db6
  • Author: Rob Pike r@golang.org
  • Date: Tue Aug 20 14:33:03 2013 +1000

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4cb086b838548fa5dbdcb502a51b29294e268db6

元コミット内容

os: be consistent about File methods with nil receivers
Some crashed, some didn't. Make a nil receiver always
return ErrInvalid rather than crash.
Fixes #5824.
The program in the bug listing is silent now, at least on my Mac.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/13108044

変更の背景

Go言語では、メソッドはポインタレシーバと値レシーバの両方を持つことができます。ポインタレシーバを持つメソッドは、レシーバがnilである場合でも呼び出すことが可能です。しかし、そのメソッド内でnilレシーバが指す値にアクセスしようとすると、ランタイムパニック(nilポインタデリファレンス)が発生します。

このコミットが作成された当時、osパッケージのFile型の一部のメソッドは、*os.File型のレシーバがnilである場合にパニックを引き起こしていました。一方で、他のメソッドはパニックを起こさずに適切にエラーを返していました。このような一貫性のない挙動は、開発者にとって混乱の原因となり、予期せぬクラッシュにつながる可能性がありました。

特に、コミットメッセージで言及されているFixes #5824は、この問題が具体的なバグとして報告されていたことを示唆しています。ユーザーがnil*os.Fileに対してメソッドを呼び出した際に、クラッシュするのではなく、予測可能なエラーを返すようにすることで、より堅牢なアプリケーションを構築できるようになります。

前提知識の解説

Go言語のメソッドとレシーバ

Go言語では、関数にレシーバを付与することで、その型に紐づくメソッドを定義できます。レシーバには「値レシーバ」と「ポインタレシーバ」の2種類があります。

  • 値レシーバ: func (f File) MethodName(...) のように定義されます。メソッドが呼び出される際、レシーバの型の値がコピーされて渡されます。
  • ポインタレシーバ: func (f *File) MethodName(...) のように定義されます。メソッドが呼び出される際、レシーバの型の値へのポインタが渡されます。

ポインタレシーバの場合、レシーバ自体がnilであるポインタであってもメソッドを呼び出すことができます。これは、メソッドがポインタのコピーを受け取るため、そのポインタがnilであってもメソッドの実行自体は可能だからです。しかし、メソッド内でそのnilポインタが指すメモリ領域にアクセスしようとすると、ランタイムパニックが発生します。

nilとエラーハンドリング

Go言語では、初期化されていないポインタ、スライス、マップ、チャネル、インターフェースなどはnilというゼロ値を取ります。nilは「値がない」状態を示します。

osパッケージのFile型は、ファイルディスクリプタなどのシステムリソースをラップする構造体であり、通常はos.Openなどの関数によって返されます。これらの関数がエラーを返した場合、*os.File型の戻り値はnilになることがあります。

Goのエラーハンドリングは、多値戻り値(通常はresult, errの形式)とif err != nilによるチェックが基本です。このコミットでは、nilレシーバの場合にos.ErrInvalidという特定のエラーを返すことで、呼び出し元がnilレシーバによる不正な操作を検知し、適切に処理できるようにしています。

os.ErrInvalid

os.ErrInvalidは、osパッケージで定義されているエラー定数の一つで、無効な引数や操作が試みられた場合に返される一般的なエラーです。このコミットでは、nil*os.Fileに対してファイル操作を行おうとすることが「無効な操作」と見なされ、このエラーが返されるように統一されました。

syscallパッケージ

osパッケージは、内部でオペレーティングシステム固有のシステムコールを呼び出すためにsyscallパッケージを利用しています。例えば、ファイルの読み書き、ディレクトリの変更、ファイル情報の取得などは、最終的にsyscallパッケージを介してOSの機能にアクセスします。このコミットの変更箇所にもsyscall.Fchdir, syscall.Fchmod, syscall.Fchown, syscall.Ftruncate, syscall.Fstatなどのシステムコール関連の関数が見られます。

技術的詳細

このコミットの主要な変更は、osパッケージ内の*Fileレシーバを持つ複数のメソッドに、レシーバfnilであるかどうかのチェックを追加したことです。

具体的には、各メソッドの冒頭に以下の形式のコードが追加されています。

if f == nil {
    return ..., ErrInvalid
}

ここで...の部分は、メソッドの戻り値の型に応じて適切なゼロ値(例えば、int型なら0FileInfoインターフェースならnil)が入ります。

この変更により、nil*os.Fileに対してこれらのメソッドが呼び出された場合、以前のようにパニックを起こす代わりに、os.ErrInvalidエラーが返されるようになります。これにより、呼び出し元はエラーを捕捉し、適切に処理することが可能になります。例えば、以下のようなコードがあったとします。

var f *os.File // f は nil

// 変更前: ここでパニックが発生する可能性があった
// 変更後: ErrInvalid が返される
err := f.Chdir()
if err != nil {
    fmt.Println("Error:", err) // "Error: invalid argument" などが出力される
}

この修正は、osパッケージが提供するファイル操作のAPI全体で一貫したエラーハンドリングの挙動を保証するために重要です。異なるオペレーティングシステム(Plan 9, POSIX, Unix, Windows)向けのファイル操作の実装(file_plan9.go, file_posix.go, file_unix.go, file_windows.go, stat_windows.go)にも同様のチェックが追加されており、クロスプラットフォームでの挙動の統一が図られています。

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

このコミットでは、以下のファイルが変更され、*os.Fileレシーバを持つメソッドにnilチェックが追加されました。

  • src/pkg/os/doc.go: Readdir, Readdirnames メソッドのドキュメントと実装例にnilチェックを追加。
  • src/pkg/os/file.go: Seek, Chdir メソッドにnilチェックを追加。
  • src/pkg/os/file_plan9.go: Close, Stat, Truncate, Chmod, Chown メソッドにnilチェックを追加。
  • src/pkg/os/file_posix.go: Chmod, Chown, Truncate メソッドにnilチェックを追加。
  • src/pkg/os/file_unix.go: Close, Stat メソッドにnilチェックを追加。
  • src/pkg/os/file_windows.go: Close メソッドにnilチェックを追加。
  • src/pkg/os/stat_windows.go: Stat メソッドにnilチェックを追加。

変更の具体的な例は、各ファイルのdiffで示されているように、メソッドの冒頭にif f == nil { return ..., ErrInvalid }というガード節が追加されています。

コアとなるコードの解説

変更の核となるのは、*os.File型のメソッドが呼び出される際に、そのレシーバfnilであるかどうかを明示的にチェックするコードです。

例えば、src/pkg/os/doc.go内のReaddirメソッドの変更を見てみましょう。

// 変更前
// func (f *File) Readdir(n int) (fi []FileInfo, err error) {
// 	return f.readdir(n)
// }

// 変更後
func (f *File) Readdir(n int) (fi []FileInfo, err error) {
	if f == nil {
		return nil, ErrInvalid
	}
	return f.readdir(n)
}

この変更により、fnilの場合、内部のf.readdir(n)が呼び出される前にnil, ErrInvalidが即座に返されます。これにより、nilポインタデリファレンスによるパニックが回避されます。

同様に、src/pkg/os/file.goSeekメソッドでは、戻り値がint64errorであるため、nilチェックの際には0, ErrInvalidが返されます。

func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
	if f == nil {
		return 0, ErrInvalid // int64のゼロ値は0
	}
	r, e := f.seek(offset, whence)
	// ...
}

これらの変更は、Go言語の「パニックではなくエラーを返す」という設計思想に沿ったものであり、ライブラリの利用者にとってより予測可能で扱いやすいAPIを提供します。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語のIssue Tracker: https://github.com/golang/go/issues (ただし、Issue #5824は直接見つからなかったため、一般的なリンクとして記載)
  • Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されているhttps://golang.org/cl/13108044はGerritのチェンジリストへのリンクです)
  • Go言語のメソッドとレシーバに関する一般的な情報源 (例: Effective Go, Go by Exampleなど)