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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおいて、ポインタレシーバを持つ error インターフェースおよび fmt.Stringer インターフェースの実装が正しくフォーマットされるように修正するものです。具体的には、text/template が値を評価する際に、値型がこれらのインターフェースを直接実装していない場合でも、その値のアドレス(ポインタ)がインターフェースを実装していれば、そのポインタレシーバを持つメソッドを呼び出して適切な文字列表現を得るように改善されています。これは、以前のコミット 982d70c6d5d6 の継続として行われた変更です。

コミット

commit 39fcca60cb5a13d2836d5d92cf1ed9aea07f6366
Author: David Symonds <dsymonds@golang.org>
Date:   Fri Nov 4 23:45:38 2011 +1100

    template: format error with pointer receiver.
    
    This is a continuation of 982d70c6d5d6.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5348042

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

https://github.com/golang/go/commit/39fcca60cb5a13d2836d5d92cf1ed9aea07f6366

元コミット内容

このコミットは、コミットメッセージに「This is a continuation of 982d70c6d5d6.」と記載されている通り、以前のコミット 982d70c6d5d6 の続きとして行われたものです。残念ながら、982d70c6d5d6 の具体的な内容は今回の検索では特定できませんでしたが、text/template パッケージにおける値のフォーマットに関する改善の一環であることが示唆されています。本コミットは、特にポインタレシーバを持つ error インターフェースの実装に対する text/template の挙動を修正することに焦点を当てています。

変更の背景

Goの text/template パッケージは、データ構造をテンプレートにバインドしてテキストを生成する際に、そのデータ内の値を適切に文字列として表現する必要があります。この際、Goの型システムにおけるインターフェース、特に fmt.Stringererror インターフェースが重要な役割を果たします。

fmt.Stringer インターフェースは、String() string メソッドを持つ型が自身の文字列表現を定義するためのものです。同様に、error インターフェースは Error() string メソッドを持ち、エラーの文字列表現を提供します。

Goでは、メソッドは値レシーバ (func (t MyType) MyMethod()) とポインタレシーバ (func (t *MyType) MyMethod()) の両方を持つことができます。text/template が値をフォーマットする際、ある型が直接 fmt.Stringererror を実装していなくても、その型のポインタがこれらのインターフェースを実装している場合があります。例えば、type MyError struct { ... } という構造体があり、func (e *MyError) Error() string というメソッドが定義されている場合、MyError 型自体は error インターフェースを実装していませんが、*MyError 型は error インターフェースを実装しています。

このコミット以前は、text/template がこのようなケース(値型がインターフェースを実装していないが、そのポインタ型が実装している場合)を適切に処理できていませんでした。特に error インターフェースに関しては、この挙動が問題となり、テンプレート内でエラー型が期待通りにフォーマットされない可能性がありました。この変更は、このギャップを埋め、text/template がより堅牢に値をフォーマットできるようにするために導入されました。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを「実装している」とみなされます。Goのインターフェースは暗黙的に実装されるため、implements キーワードのような明示的な宣言は不要です。

fmt.Stringer インターフェース

fmt パッケージに定義されている fmt.Stringer インターフェースは、Goで最もよく使われるインターフェースの一つです。

type Stringer interface {
    String() string
}

このインターフェースを実装する型は、String() メソッドを提供することで、fmt.Print(), fmt.Println(), fmt.Sprintf() などの fmt パッケージの関数によって文字列として出力される際に、そのメソッドの戻り値が使用されます。これにより、カスタム型を人間が読みやすい形式で表示できるようになります。

error インターフェース

Go言語におけるエラーハンドリングの基本となるのが error インターフェースです。

type error interface {
    Error() string
}

このインターフェースも Stringer と同様に単一のメソッド Error() string を持ちます。関数がエラーを返す場合、通常はこの error インターフェース型で返されます。nil はエラーがないことを意味し、非nilerror 値はエラーが発生したことを示します。Error() メソッドは、エラーに関する詳細な文字列メッセージを提供するために使用されます。

ポインタレシーバと値レシーバ

Goのメソッドは、レシーバの型によって「値レシーバ」と「ポインタレシーバ」に分けられます。

  • 値レシーバ (func (t MyType) MyMethod()): メソッドが呼び出される際、レシーバの型の値がコピーされてメソッドに渡されます。メソッド内でレシーバの値を変更しても、元の値には影響しません。
  • ポインタレシーバ (func (t *MyType) MyMethod()): メソッドが呼び出される際、レシーバの型の値へのポインタがメソッドに渡されます。メソッド内でポインタを通じてレシーバの値を変更すると、元の値も変更されます。インターフェースを実装する際、特にレシーバの値を変更する必要がある場合や、レシーバが大きな構造体でコピーのコストを避けたい場合にポインタレシーバが使われます。

重要な点として、ある型 TString() メソッドを値レシーバで実装している場合、T*T の両方が fmt.Stringer インターフェースを実装しているとみなされます。しかし、TString() メソッドをポインタレシーバで実装している場合、T 自体は fmt.Stringer を実装しているとはみなされず、*T のみが fmt.Stringer を実装しているとみなされます。これは、T の値から *T のポインタを取得できる(アドレス可能である)場合にのみ、T の値に対してポインタレシーバのメソッドを呼び出すことができるためです。

技術的詳細

text/template パッケージは、テンプレート内の変数を評価し、その結果を文字列として出力する際に、Goの reflect パッケージを利用して型の情報を動的に取得し、適切なメソッド(特に String()Error())を呼び出します。

このコミットの変更点以前は、text/templateprintValue 関数(exec.go 内)が値をフォーマットする際に、以下のロジックを持っていました。

  1. v の型が直接 errorType または fmtStringerType インターフェースを実装しているかを確認します。
  2. もし実装していない場合、v がアドレス可能 (v.CanAddr()) であり、かつ v のポインタ型 (reflect.PtrTo(v.Type())) が fmtStringerType を実装しているかを確認します。
  3. 上記の条件が満たされれば、v のアドレスを取得し (v.Addr())、そのポインタに対して String() メソッドを呼び出そうとします。

このロジックには、errorType インターフェースに関する考慮が不足していました。つまり、値型が直接 errorType を実装していないが、そのポインタ型が errorType を実装している場合に、text/templateError() メソッドを呼び出すことができませんでした。その結果、テンプレート内でそのようなエラー型が期待通りにフォーマットされず、デフォルトの文字列表現(例えば、構造体のフィールドがそのまま出力されるなど)になってしまう可能性がありました。

このコミットは、この不足を解消し、fmt.StringerType と同様に errorType もポインタレシーバを持つケースを考慮するように printValue 関数の条件を拡張しました。これにより、text/template は、値が直接インターフェースを実装していなくても、そのポインタがインターフェースを実装していれば、適切に Error() または String() メソッドを呼び出して、期待される文字列表現を得られるようになりました。

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

diff --git a/src/pkg/text/template/exec.go b/src/pkg/text/template/exec.go
index 540fb72c8e..8ebd52bf3f 100644
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -660,7 +660,7 @@ func (s *state) printValue(n parse.Node, v reflect.Value) {
 	}
 
 	if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
-		if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(fmtStringerType) {
+		if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
 			v = v.Addr()
 		} else {
 			switch v.Kind() {
diff --git a/src/pkg/text/template/exec_test.go b/src/pkg/text/template/exec_test.go
index 2199e440bc..5721667641 100644
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -32,6 +32,9 @@ type T struct {
 	// Struct with String method.
 	V0     V
 	V1, V2 *V
+	// Struct with Error method.
+	W0     W
+	W1, W2 *W
 	// Slices
 	SI      []int
 	SIEmpty []int
@@ -77,6 +80,17 @@ func (v *V) String() string {
 	return fmt.Sprintf("<%d>", v.j)\n}\n \n+type W struct {\n+\tk int\n+}\n+\n+func (w *W) Error() string {\n+\tif w == nil {\n+\t\treturn "nilW"\n+\t}\n+\treturn fmt.Sprintf("[%d]", w.k)\n+}\n+\n var tVal = &T{\n 	True:   true,\n 	I:      17,\n@@ -85,6 +99,8 @@ var tVal = &T{\n 	U:      &U{"v"},\n 	V0:     V{6666},\n 	V1:     &V{7777}, // leave V2 as nil\n+\tW0:     W{888},\n+\tW1:     &W{999}, // leave W2 as nil\n \tSI:     []int{3, 4, 5},\n \tSB:     []bool{true, false},\n \tMSI:    map[string]int{"one": 1, "two": 2, "three": 3},\n@@ -251,6 +267,11 @@ var execTests = []execTest{\n 	{"&V{7777}.String()", "-{{.V1}}-", "-<7777>-", tVal, true},\n 	{"(*V)(nil).String()", "-{{.V2}}-", "-nilV-", tVal, true},\n \n+\t// Type with Error method.\n+\t{"W{888}.Error()", "-{{.W0}}-", "-[888]-", tVal, true},\n+\t{"&W{999}.Error()", "-{{.W1}}-", "-[999]-", tVal, true},\n+\t{"(*W)(nil).Error()", "-{{.W2}}-", "-nilW-", tVal, true},\n+\n \t// Pointers.\n \t{"*int", "{{.PI}}", "23", tVal, true},\n \t{"*[]int", "{{.PSI}}", "[21 22 23]", tVal, true},\n```

## コアとなるコードの解説

### `src/pkg/text/template/exec.go` の変更

`exec.go` の `printValue` 関数は、テンプレート内で評価された値を文字列として出力する際の主要なロジックを含んでいます。

変更前のコード:
```go
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
    if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(fmtStringerType) {
        v = v.Addr()
    } else {
        // ...
    }
}

変更後のコード:

if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
    if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
        v = v.Addr()
    } else {
        // ...
    }
}

この変更の核心は、if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(fmtStringerType) の条件式に || reflect.PtrTo(v.Type()).Implements(errorType) が追加された点です。

  • !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType): これは、現在の値 v の型が、直接 error インターフェースも fmt.Stringer インターフェースも実装していない場合に、次の条件に進むことを意味します。
  • v.CanAddr(): 値 v がアドレス可能であるか(つまり、&v のようにポインタを取得できるか)を確認します。これは、ポインタレシーバを持つメソッドを呼び出すために必要です。
  • reflect.PtrTo(v.Type()): v の型に対応するポインタ型を取得します。例えば、MyType であれば *MyType の型情報を取得します。
  • (...).Implements(errorType) || (...).Implements(fmtStringerType): 取得したポインタ型が error インターフェースまたは fmt.Stringer インターフェースのいずれかを実装しているかを確認します。

この修正により、値型が直接インターフェースを実装していなくても、その値がアドレス可能であり、かつそのポインタ型が error または fmt.Stringer を実装している場合、text/template はその値のアドレスを取得し (v = v.Addr())、ポインタレシーバを持つ Error() または String() メソッドを呼び出すことができるようになります。これにより、エラー型がテンプレート内でより適切にフォーマットされるようになります。

src/pkg/text/template/exec_test.go の変更

テストファイル exec_test.go には、この変更を検証するための新しいテストケースが追加されています。

  1. 新しい型 W の定義:

    type W struct {
    	k int
    }
    
    func (w *W) Error() string {
    	if w == nil {
    		return "nilW"
    	}
    	return fmt.Sprintf("[%d]", w.k)
    }
    

    W という新しい構造体が定義され、そのポインタレシーバ (*W) が Error() メソッドを実装することで、error インターフェースを満たすようになっています。これは、値型 W 自体は error を実装していませんが、ポインタ型 *Werror を実装しているという、まさにこのコミットが解決しようとしているシナリオを再現するためのものです。

  2. T 構造体への W 型フィールドの追加:

    type T struct {
        // ...
        W0     W
        W1, W2 *W
        // ...
    }
    

    テストで使用される T 構造体に、W 型の値 (W0) とポインタ (W1, W2) のフィールドが追加されました。

  3. tVal 変数への初期値の追加:

    var tVal = &T{
        // ...
        W0:     W{888},
        W1:     &W{999}, // leave W2 as nil
        // ...
    }
    

    tVal はテンプレートのデータとして使用される変数で、W0W1 に具体的な値が設定されています。W2nil のままにされ、nil ポインタのケースもテストできるようにしています。

  4. 新しいテストケースの追加:

    // Type with Error method.
    {"W{888}.Error()", "-{{.W0}}-", "-[888]-", tVal, true},
    {"&W{999}.Error()", "-{{.W1}}-", "-[999]-", tVal, true},
    {"(*W)(nil).Error()", "-{{.W2}}-", "-nilW-", tVal, true},
    

    これらのテストケースは、W 型のフィールドがテンプレート内でどのようにフォーマットされるかを検証します。

    • {{.W0}}: 値型 W のフィールドが、ポインタレシーバを持つ Error() メソッドによって正しくフォーマットされることを確認します。
    • {{.W1}}: ポインタ型 *W のフィールドが正しくフォーマットされることを確認します。
    • {{.W2}}: nil ポインタの *W フィールドが、Error() メソッド内の nil チェックによって正しく "nilW" とフォーマットされることを確認します。

これらのテストケースの追加により、exec.go で行われた変更が、ポインタレシーバを持つ error インターフェースの実装に対して正しく機能すること、そして nil ポインタの場合も適切に処理されることが保証されます。

関連リンク

  • Go CL 5348042: https://golang.org/cl/5348042

参考にした情報源リンク