[インデックス 12933] ファイルの概要
このコミットは、Go言語のtext/template
パッケージにおける、エクスポートされていないフィールドの検出ロジックの改善と、それに伴うエラーメッセージの明確化を目的としています。特に、マップのキーとして小文字が使用された場合に発生していた問題を修正し、以前の変更(コミットハッシュ6009048
)によって導入された回帰を元に戻すものです。
コミット
commit a8098cbcfd7772911f761e787f656f6e685c105e
Author: Rob Pike <r@golang.org>
Date: Mon Apr 23 15:39:02 2012 +1000
text/template: detect unexported fields better
Moves the error detection back into execution, where it used to be,
and improves the error message.
Rolls back most of 6009048, which broke lower-case keys in maps.
If it weren't for maps we could detect this at compile time rather than
execution time.
Fixes #3542.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6098051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a8098cbcfd7772911f761e787f656f6e685c105e
元コミット内容
このコミットは、text/template
パッケージにおいて、エクスポートされていない(小文字で始まる)フィールドへのアクセスに関するエラー検出の挙動を変更しています。具体的には、エラー検出のタイミングを実行時(runtime)に戻し、より分かりやすいエラーメッセージを提供するように改善されています。
また、以前のコミット6009048
によって導入された、マップのキーとして小文字が使用された場合にテンプレートが正しく動作しないという回帰(regression)の大部分を元に戻しています。この問題は、Go言語のテンプレートエンジンが、構造体のエクスポートされていないフィールドとマップの小文字キーを区別する際に発生していました。
コミットメッセージでは、マップの存在がなければコンパイル時にこの種のエラーを検出できたはずだが、マップの動的な性質上、実行時検出が必要であると述べられています。
変更の背景
この変更の背景には、Go言語のtext/template
パッケージにおける、構造体のエクスポートされていないフィールドへのアクセスと、マップのキーとして小文字が使用された場合の挙動に関する問題がありました。
Go言語では、構造体のフィールドが外部からアクセス可能であるためには、そのフィールド名が大文字で始まる必要があります(エクスポートされているフィールド)。小文字で始まるフィールドはエクスポートされておらず、パッケージ外からは直接アクセスできません。text/template
パッケージは、テンプレート内でデータ構造のフィールドにアクセスする際に、このGo言語の可視性ルールに従う必要があります。
以前のコミット6009048
は、おそらくテンプレートの解析時(コンパイル時)にエクスポートされていないフィールドへのアクセスを検出する試みを行いましたが、これが副作用として、マップのキーが小文字である場合に正しく処理されないという問題を引き起こしました。これは、マップのキーはGo言語のエクスポートルールとは関係なく、任意の文字列(小文字を含む)を使用できるためです。
この問題は、GitHub Issue #3542「Templates no longer accept lower-case map keys」として報告されており、このコミットはその問題を修正するために作成されました。
前提知識の解説
- Go言語の
text/template
パッケージ: Go言語に標準で備わっているテキストテンプレートエンジンです。HTMLやテキストファイルを動的に生成する際に使用されます。テンプレート内でGoのデータ構造(構造体、マップ、スライスなど)のフィールドやメソッドにアクセスできます。 - エクスポートされたフィールドとエクスポートされていないフィールド: Go言語では、識別子(変数名、関数名、構造体名、フィールド名など)が大文字で始まる場合、その識別子はパッケージ外からアクセス可能です(エクスポートされている)。小文字で始まる場合、その識別子はパッケージ内でのみアクセス可能です(エクスポートされていない)。これは、Go言語のアクセス修飾子のような役割を果たします。
- 構造体 (Struct): 複数の異なる型のフィールドをまとめた複合データ型です。構造体のフィールドにアクセスする際は、通常、
.
演算子を使用します(例:myStruct.FieldName
)。 - マップ (Map): キーと値のペアを格納するデータ構造です。キーは一意であり、値にアクセスするために使用されます。マップのキーは任意の比較可能な型(文字列、数値など)にすることができます。
- コンパイル時と実行時:
- コンパイル時: ソースコードが機械語に変換される段階です。この段階で検出されるエラーは「コンパイルエラー」と呼ばれます。
- 実行時: コンパイルされたプログラムが実際に実行される段階です。この段階で検出されるエラーは「実行時エラー」と呼ばれます。
- 回帰 (Regression): ソフトウェア開発において、以前は正しく動作していた機能が、新しい変更の導入によって動作しなくなる現象を指します。
技術的詳細
このコミットの主要な変更点は、text/template
パッケージのexec.go
ファイルとparse/parse.go
ファイルにあります。
-
エラー検出ロジックの移動:
- 以前は
parse/parse.go
(解析時)で行われていた、エクスポートされていないフィールドへのアクセス検出が、exec.go
(実行時)のevalField
関数内に移動されました。 - これにより、テンプレートの解析段階では、フィールド名が小文字で始まること自体はエラーとはみなされなくなりました。これは、マップのキーが小文字である場合を正しく処理するために必要です。
parse/parse.go
からisExported
関数とその関連ロジックが削除されています。
- 以前は
-
evalField
関数の改善:exec.go
のevalField
関数は、テンプレートが構造体のフィールドまたはマップの要素にアクセスする際に呼び出されます。- この関数内で、
receiver.Kind()
(レシーバーの型)がreflect.Struct
である場合にのみ、フィールド名がエクスポートされているかどうかのチェックが行われるようになりました。 isExported
ヘルパー関数がexec.go
に新しく追加され、フィールド名(またはマップキー)の最初の文字が大文字であるかどうかをチェックします。ただし、このチェックは構造体のフィールドに限定されます。nil
ポインタの評価に関するエラーチェックも改善され、より明確なエラーメッセージが提供されるようになりました。
-
マップの小文字キーのサポートの復元:
receiver.Kind() == reflect.Map
の場合の処理が、構造体のフィールドチェックとは独立して行われるようになりました。これにより、マップのキーが小文字であっても、以前のように正しくアクセスできるようになりました。これは、コミット6009048
によって導入された回帰を修正するものです。
-
テストケースの追加:
exec_test.go
に、エクスポートされていないフィールドへのアクセスが正しくエラーとなること、およびマップの小文字キーが正しく動作することを確認する新しいテストケースが追加されています。- 特に、
{"unexported", "{{.unexported}}", "", tVal, false}
というテストケースは、エクスポートされていないフィールドへのアクセスがエラーとなることを確認しています。 {"bug9", "{{.cause}}", "neglect", map[string]string{"cause": "neglect"}, true}
というテストケースは、マップの小文字キーが正しく動作することを確認しています。
これらの変更により、text/template
はGo言語の可視性ルールを尊重しつつ、マップの柔軟なキー命名規則にも対応できるようになりました。
コアとなるコードの変更箇所
src/pkg/text/template/exec.go
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -12,6 +12,8 @@ import (
"sort"
"strings"
"text/template/parse"
+ "unicode"
+ "unicode/utf8"
)
// state represents the state of an execution. It's not part of the
@@ -414,9 +416,13 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
return s.evalCall(dot, method, fieldName, args, final)
}
hasArgs := len(args) > 1 || final.IsValid()
- // It's not a method; is it a field of a struct?
+ // It's not a method; must be a field of a struct or an element of a map. The receiver must not be nil.
receiver, isNil := indirect(receiver)
- if receiver.Kind() == reflect.Struct {
+ if isNil {
+ s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
+ }
+ switch receiver.Kind() {
+ case reflect.Struct:
tField, ok := receiver.Type().FieldByName(fieldName)
if ok {
field := receiver.FieldByIndex(tField.Index)
@@ -428,19 +434,22 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
return field
}
}
- }
- // If it's a map, attempt to use the field name as a key.
- if receiver.Kind() == reflect.Map {
+ if !isExported(fieldName) {
+ s.errorf("%s is not an exported field of struct type %s", fieldName, typ)
+ }
+ case reflect.Map:
+ // If it's a map, attempt to use the field name as a key.
nameVal := reflect.ValueOf(fieldName)
if nameVal.Type().AssignableTo(receiver.Type().Key()) {
if hasArgs {
s.errorf("map can't be called with arguments: %s.%s %s", typ, fieldName, args)
}
return receiver.MapIndex(nameVal)
}
- }
- if isNil {
- s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
}
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
panic("not reached")
}
+// isExported reports whether the field name (which starts with a period) can be accessed.
+func isExported(fieldName string) bool {
+ r, _ := utf8.DecodeRuneInString(fieldName[1:]) // drop the period
+ return unicode.IsUpper(r)
+}
+
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
src/pkg/text/template/parse/parse.go
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -14,7 +14,6 @@ import (
"runtime"
"strconv"
"unicode"
- "unicode/utf8"
)
// Tree is the representation of a single parsed template.
@@ -474,9 +473,6 @@ Loop:
case itemVariable:
cmd.append(t.useVar(token.val))
case itemField:
- if !isExported(token.val) {
- t.errorf("field %q not exported; cannot be evaluated", token.val)
- }
cmd.append(newField(token.val))
case itemBool:
cmd.append(newBool(token.val == "true"))
@@ -502,12 +498,6 @@ Loop:
return cmd
}
-// isExported reports whether the field name (which starts with a period) can be accessed.
-func isExported(fieldName string) bool {
- r, _ := utf8.DecodeRuneInString(fieldName[1:]) // drop the period
- return unicode.IsUpper(r)
-}
-
// hasFunction reports if a function name exists in the Tree's maps.
func (t *Tree) hasFunction(name string) bool {
for _, funcMap := range t.funcs {
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更点
- インポートの追加:
unicode
とunicode/utf8
パッケージがインポートされました。これらは、フィールド名がエクスポートされているかどうかをチェックするisExported
関数で使用されます。 evalField
関数のロジック変更:- 以前は
if receiver.Kind() == reflect.Struct
の前にnil
チェックがありましたが、これがswitch receiver.Kind()
の前に移動され、より早期にnil
ポインタのエラーを検出するようになりました。 switch receiver.Kind()
が導入され、レシーバーの型に基づいて処理が分岐するようになりました。case reflect.Struct:
ブロック内で、構造体のフィールドがエクスポートされているかどうかのチェック(if !isExported(fieldName)
)が追加されました。これにより、エクスポートされていない構造体フィールドへのアクセスは実行時にエラーとなります。case reflect.Map:
ブロックは、マップのキーとしてフィールド名を使用するロジックを含んでいます。このブロックは構造体のフィールドチェックとは独立しているため、マップのキーが小文字であっても問題なくアクセスできます。これが、以前の回帰を修正する重要な変更点です。
- 以前は
isExported
関数の追加:isExported
という新しいヘルパー関数が追加されました。この関数は、与えられたフィールド名(fieldName
)の最初の文字が大文字であるかどうかをチェックし、Go言語のエクスポートルールに従っているかを判断します。fieldName[1:]
としているのは、テンプレートのフィールド名が.FieldName
のようにピリオドで始まるため、ピリオドを除外して実際のフィールド名の最初の文字をチェックするためです。
src/pkg/text/template/parse/parse.go
の変更点
- インポートの削除:
unicode/utf8
パッケージのインポートが削除されました。これは、isExported
関数がこのファイルから削除されたためです。 itemField
処理の変更:Loop
内のcase itemField:
ブロックから、if !isExported(token.val)
によるエクスポートチェックが削除されました。これにより、テンプレートの解析時には、フィールド名が小文字で始まること自体はエラーとはみなされなくなりました。このチェックは実行時(exec.go
)に移動されました。
isExported
関数の削除:- このファイルから
isExported
ヘルパー関数が完全に削除されました。
- このファイルから
これらの変更により、text/template
は、構造体のエクスポートルールとマップの柔軟なキー命名規則の両方を適切に処理できるようになりました。
関連リンク
- Go言語の
text/template
パッケージのドキュメント: https://pkg.go.dev/text/template - Go言語の可視性ルール(エクスポートされた識別子)に関する情報: https://go.dev/doc/effective_go#names
- GitHub Issue #3542: Templates no longer accept lower-case map keys: https://github.com/golang/go/issues/3542
参考にした情報源リンク
- GitHubのコミットページ: https://github.com/golang/go/commit/a8098cbcfd7772911f761e787f656f6e685c105e
- Go言語の公式ドキュメント
- Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Go言語の
unicode
パッケージのドキュメント: https://pkg.go.dev/unicode - Go言語の
unicode/utf8
パッケージのドキュメント: https://pkg.go.dev/unicode/utf8 - GitHub Issue #3542: https://github.com/golang/go/issues/3542
- Go言語のコミット
6009048
に関する情報(このコミットメッセージから得られた情報)