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

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

このコミットは、Go言語の text/template パッケージにおいて、テンプレートエンジンが関数に引数を渡す際に、interface{} 型でラップされた値の基底となる具象型を正しく認識し、利用できるようにする修正です。これにより、interface{} 型としてテンプレートに渡された値が、関数呼び出し時に適切な型として扱われるようになります。

コミット

  • コミットハッシュ: 4f7c33cd5ad9181068be0ed0514f9fc9fc36c6ec
  • Author: Ugorji Nwoke ugorji@gmail.com
  • Date: Tue May 22 15:21:35 2012 -0700
  • Subject: text/template: exec should accept interface value as valid.

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

https://github.com/golang.org/go/commit/4f7c33cd5ad9181068be0ed0514f9fc9fc36c6ec

元コミット内容

text/template: exec should accept interface value as valid.

Currently, if you pass some data to a template as an interface (e.g. interface{})
and extract that value that value as a parameter for a function, it fails, saying
wrong type.

This is because it is only looking at the interface type, not the interface content.

This CL uses the underlying content as the parameter to the func.

Fixes #3642.

R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/6218052

変更の背景

Goの text/template パッケージは、Goのデータ構造をHTMLやテキストにレンダリングするための強力なツールです。しかし、このコミットが修正する問題は、テンプレート内でGoの関数を呼び出す際に発生していました。

具体的には、テンプレートに渡されるデータが interface{} 型(Goにおける任意の型を受け入れることができる空のインターフェース)として提供され、その interface{} 型の値がテンプレート内の関数に引数として渡される場合、テンプレートエンジンは引数の型チェックで失敗していました。エラーメッセージは「wrong type」(間違った型)というものでした。

この問題の根本原因は、テンプレートエンジンが引数の型を検証する際に、interface{} 型の中身(具象型)ではなく、interface{} というインターフェース型そのものを見ていたことにありました。例えば、bytes.Buffer のインスタンスが interface{} として渡され、fmt.Stringer インターフェースを引数に取る関数に渡そうとすると、bytes.Bufferfmt.Stringer を実装しているにもかかわらず、interface{}fmt.Stringer の型が一致しないと判断されてしまっていたのです。

この挙動は、柔軟なデータ構造をテンプレートに渡したい開発者にとって大きな制約となっていました。このコミットは、この問題を解決し、テンプレートエンジンが interface{} の中身を正しく評価できるようにすることで、より堅牢で柔軟なテンプレート処理を可能にすることを目的としています。この問題はGoのIssue #3642として報告されていました。

前提知識の解説

このコミットの理解には、以下のGo言語の概念と標準ライブラリの知識が不可欠です。

  1. Goのインターフェース (interface{}):

    • Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。
    • interface{} は「空のインターフェース」と呼ばれ、メソッドを一つも持たないため、Goの全ての型が暗黙的に interface{} を実装します。
    • これにより、interface{} 型の変数は、任意の型の値を保持することができます。
    • インターフェース型の変数には、そのインターフェースが定義するメソッドセットと、そのインターフェースが保持する**具象値(concrete value)**の2つの情報が含まれています。
  2. Goの reflect パッケージ:

    • reflect パッケージは、Goのプログラムが実行時に自身の構造(型情報、値など)を検査・操作するための機能を提供します。
    • reflect.Value: Goの変数の実行時の値を表します。
    • reflect.Type: Goの型の実行時の型情報を表します。
    • Value.Kind(): reflect.Value が表す値の基本的な種類(例: reflect.Int, reflect.String, reflect.Struct, reflect.Interface など)を返します。
    • Value.IsNil(): reflect.Value が表す値がnilであるかどうかをチェックします。インターフェース、ポインタ、マップ、スライス、チャネル、関数などで使用されます。
    • Value.Elem():
      • reflect.Value がインターフェース型の場合、Elem() はそのインターフェースが保持している具象値reflect.Value を返します。
      • reflect.Value がポインタ型の場合、Elem() はそのポインタが指す先の値の reflect.Value を返します。
    • Type.AssignableTo(Type): ある型が別の型に代入可能であるかをチェックします。これは、型アサーションやインターフェースの実装チェックに似た概念です。
  3. text/template パッケージ:

    • Goの標準ライブラリの一部で、テキストベースのテンプレートを処理するためのパッケージです。
    • テンプレート内でGoのデータ構造にアクセスしたり、Goの関数を呼び出したりすることができます。
    • テンプレートエンジンは、データと関数の型を内部的に reflect パッケージを使用して検査し、適切な処理を行います。
  4. fmt.Stringer インターフェース:

    • fmt パッケージで定義されている標準インターフェースの一つです。
    • String() string という単一のメソッドを持ちます。
    • このインターフェースを実装する型は、String() メソッドを呼び出すことで、その値を文字列として表現できます。fmt.Print 系の関数などで自動的に利用されます。

これらの知識が、コミットの変更内容、特に reflect パッケージの利用方法と、それが text/template の型チェックの問題をどのように解決しているかを理解する上で重要となります。

技術的詳細

このコミットの核心は、text/template パッケージの exec.go ファイルにある validateType 関数への変更です。この関数は、テンプレートエンジンが関数呼び出しの引数や、データアクセス時の型変換を行う際に、値の型が期待される型に適合するかどうかを検証する役割を担っています。

変更前の validateType 関数は、value.Type().AssignableTo(typ) というチェックを行っていました。ここで value はテンプレートから渡された値の reflect.Valuetyp は期待される引数の reflect.Type です。

問題は、valueinterface{} 型の reflect.Value であった場合、value.Type() は常に interface{} 型を返し、その中に含まれる具象型の情報は直接利用されなかった点にあります。したがって、たとえ interface{} の中に fmt.Stringer を実装する bytes.Buffer のような具象型が入っていたとしても、interface{} 型自体が fmt.Stringer インターフェースに AssignableTo ではないため、型チェックが失敗していました。

このコミットでは、この問題を解決するために、validateType 関数に以下のロジックが追加されました。

  1. インターフェースの検出: まず、渡された valuereflect.Interface 型であり、かつ nil でないことを確認します。nil のインターフェースは具象値を持たないため、このチェックは重要です。
  2. 具象値の抽出: value.Elem() を呼び出すことで、interface{} 型の value が内部に保持している具象値reflect.Value を取得します。これにより、interface{} の「中身」にアクセスできるようになります。
  3. 具象値での型チェック: 抽出した具象値の reflect.Value に対して、再度 value.Type().AssignableTo(typ) を実行します。ここで value.Type() は具象型の reflect.Type を返すため、期待される typ との適合性が正しく評価されます。
  4. 適合時の返却: もし具象値が期待される型に適合する場合、その具象値の reflect.Value を返します。これにより、テンプレートエンジンは具象値を使って関数を呼び出すことができます。
  5. フォールスルー: 具象値が適合しない場合、または元の value がインターフェースでなかった場合は、既存のロジック(ポインタのデリファレンスなど)に処理を委ねます。これは、インターフェースのアンラップが常に解決策となるわけではないため、既存の挙動を維持するための「フォールスルー」パスです。

この変更により、text/templateinterface{} 型の引数をより賢く処理できるようになり、開発者はテンプレートに渡すデータの型を interface{} で抽象化しても、内部の具象型に基づいて関数呼び出しが正しく行われるようになりました。

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

src/pkg/text/template/exec.govalidateType 関数に以下のコードが追加されました。

--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -520,6 +520,13 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu
 		}
 	}
 	if !value.Type().AssignableTo(typ) {
+		if value.Kind() == reflect.Interface && !value.IsNil() {
+			value = value.Elem()
+			if value.Type().AssignableTo(typ) {
+				return value
+			}
+			// fallthrough
+		}
 		// Does one dereference or indirection work? We could do more, as we
 		// do with method receivers, but that gets messy and method receivers
 		// are much more constrained, so it makes more sense there than here.

また、src/pkg/text/template/exec_test.go には、この修正を検証するためのテストケースが追加されました。

--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -314,6 +314,7 @@ var execTests = []execTest{\n 	{\".VariadicFuncInt\", \"{{call .VariadicFuncInt 33 `he` `llo`}}\", \"33=<he+llo>\", tVal, true},\n 	{\"if .BinaryFunc call\", \"{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}\", \"[1=2]\", tVal, true},\n 	{\"if not .BinaryFunc call\", \"{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}\", \"No\", tVal, true},\n+\t{\"Interface Call\", `{{stringer .S}}`, \"foozle\", map[string]interface{}{\"S\": bytes.NewBufferString(\"foozle\")}, true},\n \n \t// Erroneous function calls (check args).\n \t{\".BinaryFuncTooFew\", \"{{call .BinaryFunc `1`}}\", \"\", tVal, false},\
@@ -512,6 +513,10 @@ func vfunc(V, *V) string {\n \treturn \"vfunc\"\n }\n \n+func stringer(s fmt.Stringer) string {\n+\treturn s.String()\n+}\n+\n func testExecute(execTests []execTest, template *Template, t *testing.T) {\n \tb := new(bytes.Buffer)\n \tfuncs := FuncMap{\n@@ -521,6 +526,7 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) {\n \t\t\"typeOf\":   typeOf,\n \t\t\"vfunc\":    vfunc,\n \t\t\"zeroArgs\": zeroArgs,\n+\t\t\"stringer\": stringer,\n \t}\n \tfor _, test := range execTests {\n \t\tvar tmpl *Template\n```

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

追加されたコードブロックは、`validateType` 関数内で、値が期待される型に直接代入可能でない場合に実行されます。

```go
if value.Kind() == reflect.Interface && !value.IsNil() {
    value = value.Elem()
    if value.Type().AssignableTo(typ) {
        return value
    }
    // fallthrough
}
  1. if value.Kind() == reflect.Interface && !value.IsNil():

    • この条件は、現在の valuereflect.Interface 型であり、かつ nil でないことを確認します。
    • value.Kind() は、reflect.Value が表す値の基本的な種類を返します。ここでは、それがインターフェース型であるかをチェックしています。
    • !value.IsNil() は、インターフェースが具象値を持っている(つまり、nil インターフェースではない)ことを保証します。nil インターフェースは具象値を持たないため、Elem() を呼び出すとパニックを起こす可能性があります。
  2. value = value.Elem():

    • もし value が非nilのインターフェースであれば、value.Elem() を呼び出して、そのインターフェースが内部に保持している具象値reflect.Value を取得し、それを新しい value として再代入します。
    • 例えば、interface{} 型の valuebytes.Buffer のインスタンスを保持していた場合、この行の実行後、valuebytes.Bufferreflect.Value になります。
  3. if value.Type().AssignableTo(typ):

    • 具象値の reflect.Value を取得した後、その具象値の型 (value.Type()) が、期待される引数の型 (typ) に代入可能であるかを再度チェックします。
    • このチェックが成功すれば、インターフェースの「中身」が期待される型に適合していることが確認できます。
  4. return value:

    • 具象値が期待される型に適合する場合、その具象値の reflect.Value を返します。これにより、テンプレートエンジンは、インターフェースのラッパーを剥がした具象値を使って、関数呼び出しを続行できます。
  5. // fallthrough:

    • もし具象値が期待される型に適合しなかった場合、このブロックは何も返さずに終了し、関数の残りの部分(コメントにあるように、ポインタのデリファレンスなど)に処理が移ります。これは、インターフェースのアンラップが常に解決策となるわけではないため、既存の型変換ロジックを維持するためのものです。

この変更により、text/templateinterface{} 型の引数をより柔軟に処理できるようになり、Goの型システムとテンプレートエンジンの間の整合性が向上しました。

関連リンク

参考にした情報源リンク