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

[インデックス 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)を返すことが期待されます。

このコミット以前は、ReaddirnamesReaddir といった一部の *File メソッドに対してのみ、nil レシーバーのテストが個別に存在していました。しかし、他の多くのメソッドについても同様の挙動が期待されるため、個別のテストを記述するのではなく、より体系的かつ網羅的なテストフレームワークを導入する必要がありました。これにより、将来的に新しい *File メソッドが追加された際にも、nil レシーバーのテストが容易に組み込めるようになります。

前提知識の解説

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

Go言語のメソッドは、特定の型に関連付けられた関数です。メソッドはレシーバー引数を持ち、このレシーバーはメソッドが呼び出されるオブジェクトのインスタンスを表します。レシーバーには以下の2種類があります。

  1. 値レシーバー (Value Receiver): func (t Type) MethodName(...) の形式で定義されます。メソッドが呼び出されると、レシーバーの値のコピーがメソッドに渡されます。そのため、メソッド内でレシーバーの値を変更しても、元のオブジェクトには影響しません。値レシーバーは、レシーバーが nil の場合には呼び出すことができません(コンパイルエラーまたはランタイムパニック)。

  2. ポインタレシーバー (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 レシーバーテストの体系化です。

  1. 既存の個別テストの削除:

    • TestReaddirnamesNilFile
    • TestReaddirNilFile これらのテストは、ReaddirnamesReaddir メソッドが nil*File で呼び出された場合に ErrInvalid を返すことを個別に検証していました。
  2. 新しいテーブル駆動テストの導入:

    • nilFileMethodTests というグローバル変数(構造体のスライス)が導入されました。このスライスは、テスト対象となる各 *File メソッドに関する情報(メソッド名と、nil*File を引数にとりエラーを返す匿名関数)を保持します。
    • 匿名関数は、各メソッドを nil*File レシーバーで呼び出し、その結果として得られるエラーを返します。例えば、Chdir メソッドの場合、func(f *File) error { return f.Chdir() } のように定義されています。ReadWrite のように複数の戻り値を持つメソッドの場合も、エラー値のみを返すように調整されています。
    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 レシーバーで対象のメソッドを実行します。
    • 返されたエラー gotos.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 に追加された nilFileMethodTestsTestNilFileMethods です。

nilFileMethodTests は、*os.File の様々なメソッドを網羅的にテストするためのデータ構造です。各要素は以下の2つのフィールドを持ちます。

  • name: テスト対象のメソッド名(文字列)。テスト失敗時のエラーメッセージでどのメソッドが問題だったかを特定するために使用されます。
  • f: *File を引数にとり error を返す匿名関数。この関数は、対応する *File メソッドを nil レシーバーで呼び出し、その結果として得られるエラーを返します。例えば、Chdir メソッドの場合、func(f *File) error { return f.Chdir() } となります。ReadWrite のように、エラー以外にも値を返すメソッドについては、_ を使って不要な戻り値を破棄し、エラーのみを返すようにしています。

TestNilFileMethods 関数は、この nilFileMethodTests スライスをイテレートします。 ループの各イテレーションで、以下の処理が行われます。

  1. var file *File を宣言することで、nil 値を持つ *File 型のポインタ変数 file を作成します。Goでは、ポインタ型のゼロ値は nil です。
  2. got := tt.f(file) を呼び出します。これにより、nilfile をレシーバーとして、nilFileMethodTests で定義された匿名関数(つまり、対応する *File メソッド)が実行されます。
  3. if got != ErrInvalid の条件で、メソッドが返したエラー gotos.ErrInvalid と等しいかどうかをチェックします。
  4. もしエラーが os.ErrInvalid でなかった場合、t.Errorf を呼び出してテストを失敗させます。エラーメッセージには、どのメソッド (tt.name) が期待される ErrInvalid を返さなかったのか、そして実際に何が返されたのか (got) が含まれます。

このアプローチにより、os.File のメソッドが nil レシーバーで呼び出された際に、一貫して os.ErrInvalid を返すというGoの設計原則が、効率的かつ網羅的に検証されるようになります。これにより、os パッケージの堅牢性が向上し、開発者が nil*File を誤って使用した場合でも、明確なエラーハンドリングが可能になります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特に os パッケージのテストファイル)
  • Go言語におけるテーブル駆動テストに関する一般的な情報源
  • Dave Cheney氏のブログやGoコミュニティでの議論(一般的なGoのプラクティスに関する知識)
  • Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/46820043 (コミットメッセージに記載)