[インデックス 18160] ファイルの概要
このコミットは、Go言語の標準ライブラリ os
パッケージ内の *File
型のメソッドが nil
レシーバーで呼び出された際の挙動に関するテストの追加と改善を行います。具体的には、nil
の *File
オブジェクトに対してメソッドが呼び出された場合に、os.ErrInvalid
エラーが適切に返されることを検証するための、より包括的なテストスイートが導入されています。
コミット
commit 30ba286c6140ceec7793cfac0eb47a8c939b5044
Author: Dave Cheney <dave@cheney.net>
Date: Sat Jan 4 09:58:04 2014 +1100
os: add tests for operations on nil *File methods
R=shawn.p.smith, gobot, r
CC=golang-codereviews
https://golang.org/cl/46820043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/30ba286c6140ceec7793cfac0eb47a8c939b5044
元コミット内容
このコミットの元の内容は、os
パッケージの *File
型のメソッドが nil
レシーバーで呼び出された際のテストを追加することです。これは、以前は個別のメソッドに対して行われていた nil
レシーバーのテストを、より汎用的なテーブル駆動テストに置き換えることで、テストの網羅性と保守性を向上させることを目的としています。
変更の背景
Go言語では、メソッドはポインタレシーバー (*Type
) または値レシーバー (Type
) を持つことができます。ポインタレシーバーを持つメソッドは、レシーバーが nil
である場合でも呼び出すことが可能です。この場合、メソッド内で nil
レシーバーを適切に処理する必要があります。
os
パッケージの File
型は、ファイルディスクリプタをラップしており、ファイルシステム操作のための多くのメソッドを提供します。これらのメソッドが nil
の *File
レシーバーで呼び出された場合、予期せぬパニックや未定義の動作を引き起こす可能性があります。そのため、Goの設計原則として、このような不正な操作に対しては明確なエラー(この場合は os.ErrInvalid
)を返すことが期待されます。
このコミット以前は、Readdirnames
や Readdir
といった一部の *File
メソッドに対してのみ、nil
レシーバーのテストが個別に存在していました。しかし、他の多くのメソッドについても同様の挙動が期待されるため、個別のテストを記述するのではなく、より体系的かつ網羅的なテストフレームワークを導入する必要がありました。これにより、将来的に新しい *File
メソッドが追加された際にも、nil
レシーバーのテストが容易に組み込めるようになります。
前提知識の解説
Go言語のメソッドとレシーバー
Go言語のメソッドは、特定の型に関連付けられた関数です。メソッドはレシーバー引数を持ち、このレシーバーはメソッドが呼び出されるオブジェクトのインスタンスを表します。レシーバーには以下の2種類があります。
-
値レシーバー (Value Receiver):
func (t Type) MethodName(...)
の形式で定義されます。メソッドが呼び出されると、レシーバーの値のコピーがメソッドに渡されます。そのため、メソッド内でレシーバーの値を変更しても、元のオブジェクトには影響しません。値レシーバーは、レシーバーがnil
の場合には呼び出すことができません(コンパイルエラーまたはランタイムパニック)。 -
ポインタレシーバー (Pointer Receiver):
func (t *Type) MethodName(...)
の形式で定義されます。メソッドが呼び出されると、レシーバーのポインタのコピーがメソッドに渡されます。これにより、メソッド内でレシーバーの値を変更すると、元のオブジェクトにも影響が及びます。ポインタレシーバーは、レシーバーがnil
の場合でも呼び出すことができます。この場合、メソッド内でnil
ポインタのチェックを行い、適切なエラー処理を行うことが一般的です。
このコミットで扱われているのは、*os.File
型に対するポインタレシーバーを持つメソッドです。
os.File
型
os.File
はGo言語の標準ライブラリ os
パッケージで定義されている構造体で、ファイルやディレクトリへのアクセスを提供します。これは、Unix系のシステムにおけるファイルディスクリプタやWindowsにおけるファイルハンドルに相当する抽象化された概念です。os.File
のメソッドは、ファイルの読み書き、シーク、情報の取得(Stat)、パーミッションの変更(Chmod)、所有者の変更(Chown)など、様々なファイルシステム操作を可能にします。
os.ErrInvalid
os.ErrInvalid
は、os
パッケージで定義されているエラー変数の一つです。これは、無効な引数や不正な操作が行われた場合に返される一般的なエラーです。このコミットの文脈では、nil
の *os.File
レシーバーに対してファイル操作メソッドが呼び出されるという「無効な操作」が行われた際に、このエラーが返されることが期待されています。
テーブル駆動テスト (Table Driven Tests)
Go言語のテストにおいて、テーブル駆動テストは非常に一般的なパターンです。これは、テストケースの入力、期待される出力、およびテストのメタデータを構造体のスライス(テーブル)として定義し、そのテーブルをループで回しながら各テストケースを実行する手法です。
テーブル駆動テストの利点:
- 簡潔性: 複数のテストケースを少ないコードで記述できます。
- 網羅性: 新しいテストケースの追加が容易で、テストの網羅性を高めやすいです。
- 可読性: 各テストケースの意図が明確になります。
- 保守性: テストロジックが共通化されるため、変更やデバッグが容易になります。
このコミットでは、nilFileMethodTests
という構造体のスライスが定義され、各要素がテスト対象のメソッド名と、nil
の *File
を引数にとりエラーを返す匿名関数を含んでいます。これにより、様々な *File
メソッドに対する nil
レシーバーのテストを効率的に実行しています。
技術的詳細
このコミットの主要な変更点は、src/pkg/os/os_test.go
ファイルにおける *File
メソッドの nil
レシーバーテストの体系化です。
-
既存の個別テストの削除:
TestReaddirnamesNilFile
TestReaddirNilFile
これらのテストは、Readdirnames
とReaddir
メソッドがnil
の*File
で呼び出された場合にErrInvalid
を返すことを個別に検証していました。
-
新しいテーブル駆動テストの導入:
nilFileMethodTests
というグローバル変数(構造体のスライス)が導入されました。このスライスは、テスト対象となる各*File
メソッドに関する情報(メソッド名と、nil
の*File
を引数にとりエラーを返す匿名関数)を保持します。- 匿名関数は、各メソッドを
nil
の*File
レシーバーで呼び出し、その結果として得られるエラーを返します。例えば、Chdir
メソッドの場合、func(f *File) error { return f.Chdir() }
のように定義されています。Read
やWrite
のように複数の戻り値を持つメソッドの場合も、エラー値のみを返すように調整されています。
var nilFileMethodTests = []struct { name string f func(*File) error }{ {"Chdir", func(f *File) error { return f.Chdir() }}, {"Close", func(f *File) error { return f.Close() }},\ // ... 他のメソッド ... {"WriteString", func(f *File) error { _, err := f.WriteString(""); return err }},\ }
TestNilFileMethods
という新しいテスト関数が追加されました。この関数はnilFileMethodTests
スライスをループで回し、各テストケースを実行します。- ループ内で、
var file *File
と宣言することで、nil
の*File
ポインタを作成します。 tt.f(file)
を呼び出すことで、nil
レシーバーで対象のメソッドを実行します。- 返されたエラー
got
がos.ErrInvalid
と等しいかどうかを検証します。もし等しくない場合、t.Errorf
を使用してテスト失敗を報告します。
// Test that all File methods give ErrInvalid if the receiver is nil. func TestNilFileMethods(t *testing.T) { for _, tt := range nilFileMethodTests { var file *File got := tt.f(file) if got != ErrInvalid { t.Errorf("%v should fail when f is nil; got %v", tt.name, got) } } }
この変更により、os.File
の多くのメソッドに対して、nil
レシーバーでの呼び出しが os.ErrInvalid
を返すという期待される挙動が、一貫した方法でテストされるようになりました。これは、コードの堅牢性を高め、予期せぬランタイムパニックを防ぐ上で重要です。
コアとなるコードの変更箇所
変更は src/pkg/os/os_test.go
ファイルに集中しています。
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -252,25 +252,11 @@ func TestReaddirnames(t *testing.T) {
testReaddirnames(sysdir.name, sysdir.files, t)
}
-func TestReaddirnamesNilFile(t *testing.T) {
- var f *File
- if fi, err := f.Readdirnames(1); fi != nil || err != ErrInvalid {
- t.Errorf("Readdirnames should fail when f is nil: %v, %v", fi, err)
- }
-}
-
func TestReaddir(t *testing.T) {
testReaddir(".", dot, t)
testReaddir(sysdir.name, sysdir.files, t)
}
-func TestReaddirNilFile(t *testing.T) {
- var f *File
- if fi, err := f.Readdir(1); fi != nil || err != ErrInvalid {
- t.Errorf("Readdir should fail when f is nil: %v, %v", fi, err)
- }
-}
-
// Read the directory one entry at a time.
func smallReaddirnames(file *File, length int, t *testing.T) []string {
names := make([]string, length)
@@ -1305,3 +1291,35 @@ func TestKillFindProcess(t *testing.T) {
}
})
}
+
+var nilFileMethodTests = []struct {
+ name string
+ f func(*File) error
+}{
+ {"Chdir", func(f *File) error { return f.Chdir() }},\
+ {"Close", func(f *File) error { return f.Close() }},\
+ {"Chmod", func(f *File) error { return f.Chmod(0) }},\
+ {"Chown", func(f *File) error { return f.Chown(0, 0) }},\
+ {"Read", func(f *File) error { _, err := f.Read(make([]byte, 0)); return err }},\
+ {"ReadAt", func(f *File) error { _, err := f.ReadAt(make([]byte, 0), 0); return err }},\
+ {"Readdir", func(f *File) error { _, err := f.Readdir(1); return err }},\
+ {"Readdirnames", func(f *File) error { _, err := f.Readdirnames(1); return err }},\
+ {"Seek", func(f *File) error { _, err := f.Seek(0, 0); return err }},\
+ {"Stat", func(f *File) error { _, err := f.Stat(); return err }},\
+ {"Sync", func(f *File) error { return f.Sync() }},\
+ {"Truncate", func(f *File) error { return f.Truncate(0) }},\
+ {"Write", func(f *File) error { _, err := f.Write(make([]byte, 0)); return err }},\
+ {"WriteAt", func(f *File) error { _, err := f.WriteAt(make([]byte, 0), 0); return err }},\
+ {"WriteString", func(f *File) error { _, err := f.WriteString(""); return err }},\
+}
+
+// Test that all File methods give ErrInvalid if the receiver is nil.
+func TestNilFileMethods(t *testing.T) {
+ for _, tt := range nilFileMethodTests {
+ var file *File
+ got := tt.f(file)
+ if got != ErrInvalid {
+ t.Errorf("%v should fail when f is nil; got %v", tt.name, got)
+ }
+ }
+}
コアとなるコードの解説
このコミットの核心は、os/os_test.go
に追加された nilFileMethodTests
と TestNilFileMethods
です。
nilFileMethodTests
は、*os.File
の様々なメソッドを網羅的にテストするためのデータ構造です。各要素は以下の2つのフィールドを持ちます。
name
: テスト対象のメソッド名(文字列)。テスト失敗時のエラーメッセージでどのメソッドが問題だったかを特定するために使用されます。f
:*File
を引数にとりerror
を返す匿名関数。この関数は、対応する*File
メソッドをnil
レシーバーで呼び出し、その結果として得られるエラーを返します。例えば、Chdir
メソッドの場合、func(f *File) error { return f.Chdir() }
となります。Read
やWrite
のように、エラー以外にも値を返すメソッドについては、_
を使って不要な戻り値を破棄し、エラーのみを返すようにしています。
TestNilFileMethods
関数は、この nilFileMethodTests
スライスをイテレートします。
ループの各イテレーションで、以下の処理が行われます。
var file *File
を宣言することで、nil
値を持つ*File
型のポインタ変数file
を作成します。Goでは、ポインタ型のゼロ値はnil
です。got := tt.f(file)
を呼び出します。これにより、nil
のfile
をレシーバーとして、nilFileMethodTests
で定義された匿名関数(つまり、対応する*File
メソッド)が実行されます。if got != ErrInvalid
の条件で、メソッドが返したエラーgot
がos.ErrInvalid
と等しいかどうかをチェックします。- もしエラーが
os.ErrInvalid
でなかった場合、t.Errorf
を呼び出してテストを失敗させます。エラーメッセージには、どのメソッド (tt.name
) が期待されるErrInvalid
を返さなかったのか、そして実際に何が返されたのか (got
) が含まれます。
このアプローチにより、os.File
のメソッドが nil
レシーバーで呼び出された際に、一貫して os.ErrInvalid
を返すというGoの設計原則が、効率的かつ網羅的に検証されるようになります。これにより、os
パッケージの堅牢性が向上し、開発者が nil
の *File
を誤って使用した場合でも、明確なエラーハンドリングが可能になります。
関連リンク
- Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Go言語のメソッドに関する公式ドキュメント: https://go.dev/tour/methods/1
- Go言語のテストに関する公式ドキュメント: https://go.dev/doc/tutorial/add-a-test
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
os
パッケージのテストファイル) - Go言語におけるテーブル駆動テストに関する一般的な情報源
- Dave Cheney氏のブログやGoコミュニティでの議論(一般的なGoのプラクティスに関する知識)
- Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/46820043 (コミットメッセージに記載)