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

[インデックス 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ファイルにあります。

  1. エラー検出ロジックの移動:

    • 以前はparse/parse.go(解析時)で行われていた、エクスポートされていないフィールドへのアクセス検出が、exec.go(実行時)のevalField関数内に移動されました。
    • これにより、テンプレートの解析段階では、フィールド名が小文字で始まること自体はエラーとはみなされなくなりました。これは、マップのキーが小文字である場合を正しく処理するために必要です。
    • parse/parse.goからisExported関数とその関連ロジックが削除されています。
  2. evalField関数の改善:

    • exec.goevalField関数は、テンプレートが構造体のフィールドまたはマップの要素にアクセスする際に呼び出されます。
    • この関数内で、receiver.Kind()(レシーバーの型)がreflect.Structである場合にのみ、フィールド名がエクスポートされているかどうかのチェックが行われるようになりました。
    • isExportedヘルパー関数がexec.goに新しく追加され、フィールド名(またはマップキー)の最初の文字が大文字であるかどうかをチェックします。ただし、このチェックは構造体のフィールドに限定されます。
    • nilポインタの評価に関するエラーチェックも改善され、より明確なエラーメッセージが提供されるようになりました。
  3. マップの小文字キーのサポートの復元:

    • receiver.Kind() == reflect.Mapの場合の処理が、構造体のフィールドチェックとは独立して行われるようになりました。これにより、マップのキーが小文字であっても、以前のように正しくアクセスできるようになりました。これは、コミット6009048によって導入された回帰を修正するものです。
  4. テストケースの追加:

    • 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の変更点

  • インポートの追加: unicodeunicode/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は、構造体のエクスポートルールとマップの柔軟なキー命名規則の両方を適切に処理できるようになりました。

関連リンク

参考にした情報源リンク