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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおけるエラーのフォーマット処理を改善するものです。具体的には、テンプレートエンジンが値を評価する際に、error 型の値を適切に処理し、fmt.Stringer インターフェースと同様に文字列として出力できるように変更されています。これにより、テンプレート内でエラーオブジェクトが渡された場合に、そのエラーメッセージが期待通りに表示されるようになります。

コミット

  • Author: Russ Cox rsc@golang.org
  • Date: Fri Nov 4 07:33:55 2011 -0400
  • Commit Message:
    template: format errors
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5340043
    

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

https://github.com/golang/go/commit/e73680aec0f6d6de3a8cd0f48c01c854703a5b72

元コミット内容

template: format errors

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

変更の背景

text/template パッケージは、Goアプリケーションでテキストベースの出力を生成するための強力なツールです。これまでの実装では、テンプレート内で fmt.Stringer インターフェースを実装する型は自動的にその String() メソッドが呼び出され、その結果が文字列として出力されていました。しかし、Goのエラー処理において中心的な役割を果たす error インターフェースは、fmt.Stringer とは異なるものの、その Error() メソッドによって文字列表現を提供します。

このコミット以前は、error 型の変数がテンプレートに渡された場合、fmt.Stringer として扱われないため、期待通りの文字列出力が得られない可能性がありました。この変更の背景には、error 型の値を fmt.Stringer と同様に、その文字列表現(Error() メソッドの結果)をテンプレート内で直接利用できるようにすることで、テンプレートの柔軟性と使いやすさを向上させる目的があります。これにより、エラーメッセージをテンプレート内で直接表示したり、デバッグ情報を出力したりする際に、より自然な記述が可能になります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  1. text/template パッケージ: Go言語の標準ライブラリの一つで、テキストベースのテンプレートを解析し、データ構造を適用して出力を生成するための機能を提供します。HTMLテンプレートにも利用できますが、このコミットは汎用的なテキストテンプレートの実行エンジンに関するものです。テンプレートは、{{.FieldName}} のようなアクションを使って、Goのデータ構造から値を取り出して表示します。

  2. reflect パッケージ: Goの reflect パッケージは、実行時に型情報を検査したり、変数の値を操作したりするための機能を提供します。このコミットでは、reflect.TypeOf を使用して特定のインターフェースの型情報を取得し、Implements メソッドで値がそのインターフェースを実装しているかを確認しています。

  3. error インターフェース: Go言語におけるエラー処理の基本的なインターフェースです。

    type error interface {
        Error() string
    }
    

    このインターフェースを実装する型は、Error() メソッドを通じてエラーメッセージの文字列表現を提供します。

  4. fmt.Stringer インターフェース: fmt パッケージで定義されているインターフェースで、型の文字列表現を提供するために使用されます。

    type Stringer interface {
        String() string
    }
    

    fmt.Print 系の関数は、このインターフェースを実装する値に対して String() メソッドを呼び出し、その結果を出力します。text/template も同様に、fmt.Stringer を実装する値を特別に扱います。

  5. reflect.TypeOf((*interface{})(nil)).Elem() パターン: これはGoの reflect パッケージで特定のインターフェースの reflect.Type を取得するためのイディオムです。

    • (*interface{})(nil): これは、指定されたインターフェース型へのnilポインタを作成します。例えば、(*error)(nil)error インターフェースへのnilポインタです。
    • reflect.TypeOf(...): この関数は、引数の動的な型を返します。この場合、ポインタの型(例: *error)を返します。
    • .Elem(): ポインタ型の場合、.Elem() メソッドはそのポインタが指す要素の型を返します。したがって、reflect.TypeOf((*error)(nil)).Elem()error インターフェース自体の reflect.Type を返します。 このパターンは、コンパイル時にインターフェースの具体的な実装型が不明な場合でも、そのインターフェースの型情報を取得するために非常に有用です。

技術的詳細

このコミットの核心は、text/template パッケージの実行エンジンが値を文字列として出力する際のロジックに error 型の特別扱いを追加した点にあります。

変更前は、printValue 関数内で値が fmt.Stringer インターフェースを実装しているかどうかのみをチェックしていました。もし実装していればその String() メソッドの結果を出力し、そうでなければ他のフォールバックロジック(ポインタが fmt.Stringer を実装しているかなど)を試みていました。

このコミットでは、以下の2つの主要な変更が行われています。

  1. errorType の導入: src/pkg/text/template/exec.goerrorType という新しい reflect.Type 変数が追加されました。これは reflect.TypeOf((*error)(nil)).Elem() を用いて error インターフェースの型情報を保持します。

    // 変更前
    // osErrorType     = reflect.TypeOf((*error)(nil)).Elem() // コメントアウトまたは削除された古い変数名
    errorType       = reflect.TypeOf((*error)(nil)).Elem()
    fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
    

    osErrorType という古い変数名が errorType に変更されていますが、これは単なるリネームではなく、error インターフェースの型をより明確に参照するためのものです。

  2. printValue 関数での errorType のチェック: src/pkg/text/template/exec.goprintValue 関数内で、値が fmt.Stringer を実装しているかどうかのチェックに加えて、errorType を実装しているかどうかのチェックが追加されました。

    // 変更前
    // if !v.Type().Implements(fmtStringerType) {
    // 変更後
    if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
    

    この変更により、verror インターフェースを実装している場合、!v.Type().Implements(errorType)false となり、if 文の条件全体が false になります。つまり、error 型の値は fmt.Stringer と同様に、そのデフォルトの文字列表現(Error() メソッドの結果)が利用されるようになります。これにより、error 型の値が fmt.Stringer を実装していなくても、テンプレート内で適切にフォーマットされるようになりました。

  3. goodFunc 関数での errorType の利用: src/pkg/text/template/funcs.gogoodFunc 関数でも、関数の戻り値がエラー型であるかどうかのチェックに errorType が利用されるようになりました。

    // 変更前
    // case typ.NumOut() == 2 && typ.Out(1) == osErrorType:
    // 変更後
    case typ.NumOut() == 2 && typ.Out(1) == errorType:
    

    これは osErrorType から errorType への変数名変更に伴う修正であり、機能的な変更というよりはコードの一貫性を保つためのものです。

テストファイル src/pkg/text/template/exec_test.go では、errors.New("erroozle") で作成された error 型の値をテンプレートに渡し、その出力が期待通りになることを確認するテストケース bug5a が追加されています。

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

このコミットで変更された主要なファイルは以下の3つです。

  1. src/pkg/text/template/exec.go:

    • osErrorType 変数の名前が errorType に変更されました。
    • printValue 関数内の値のフォーマットロジックが変更され、errorType を実装する値も fmt.Stringer と同様に扱われるようになりました。
  2. src/pkg/text/template/exec_test.go:

    • errors パッケージがインポートされました。
    • テスト用の構造体 TErr error フィールドが追加されました。
    • tVal 変数に Err フィールドの初期値として errors.New("erroozle") が設定されました。
    • execTests{{.Err}} を評価する新しいテストケース bug5a が追加されました。
  3. src/pkg/text/template/funcs.go:

    • goodFunc 関数内で osErrorType の代わりに errorType が使用されるようになりました。

コアとなるコードの解説

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

--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -445,7 +445,7 @@ func methodByName(receiver reflect.Value, name string) (reflect.Value, bool) {
 }
 
 var (
-	osErrorType     = reflect.TypeOf((*error)(nil)).Elem()
+	errorType       = reflect.TypeOf((*error)(nil)).Elem()
 	fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
 )
 
@@ -659,7 +659,7 @@ func (s *state) printValue(n parse.Node, v reflect.Value) {
 		return
 	}
 
-	if !v.Type().Implements(fmtStringerType) {
+	if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
 		if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(fmtStringerType) {
 			v = v.Addr()
 		} else {
  • var ( ... ) ブロック: osErrorType という変数名が errorType に変更されました。これは、error インターフェースの reflect.Type を保持するための変数であり、より直感的な名前に変更されたものです。reflect.TypeOf((*error)(nil)).Elem() は、error インターフェース自体の型情報を取得するGoのイディオムです。

  • func (s *state) printValue(n parse.Node, v reflect.Value): この関数は、テンプレート内で値を評価し、その結果を文字列として出力する主要なロジックを含んでいます。 変更された if 文の条件 !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) がこのコミットの核心です。

    • 変更前は !v.Type().Implements(fmtStringerType) のみでした。これは、「もし値 vfmt.Stringer インターフェースを実装していないならば」という条件でした。
    • 変更後は、「もし値 verror インターフェースを実装しておらず、かつ fmt.Stringer インターフェースも実装していないならば」という条件になりました。 これにより、verror 型である場合(つまり v.Type().Implements(errorType)true の場合)、最初の条件 !v.Type().Implements(errorType)false となるため、if 文のブロック内には入らず、error 型の値は fmt.Stringer と同様に、そのデフォルトの文字列表現(Error() メソッドの結果)が利用されるようになります。

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

--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -6,6 +6,7 @@ package template
 
 import (
 	"bytes"
+	"errors"
 	"flag"
 	"fmt"
 	"os"
@@ -52,6 +53,7 @@ type T struct {
 	NonEmptyInterface I
 	// Stringer.
 	Str fmt.Stringer
+	Err error
 	// Pointers
 	PI  *int
 	PSI *[]int
@@ -99,6 +101,7 @@ var tVal = &T{
 	Empty4:            &U{"UinEmpty"},
 	NonEmptyInterface: new(T),
 	Str:               bytes.NewBuffer([]byte("foozle")),
+	Err:               errors.New("erroozle"),
 	PI:                newInt(23),
 	PSI:               newIntSlice(21, 22, 23),
 	Tmpl:              Must(New("x").Parse("test template")), // "x" is the value of .X
@@ -416,6 +419,7 @@ var execTests = []execTest{
 	{"bug4", "{{if .Empty0}}non-nil{{else}}nil{{end}}", "nil", tVal, true},
 	// Stringer.
 	{"bug5", "{{.Str}}\", "foozle", tVal, true},
+	{"bug5a", "{{.Err}}\", "erroozle", tVal, true},
 	// Args need to be indirected and dereferenced sometimes.
 	{"bug6a", "{{vfunc .V0 .V1}}\", "vfunc", tVal, true},
 	{"bug6b", "{{vfunc .V0 .V0}}\", "vfunc", tVal, true},
  • import "errors": errors.New を使用するために errors パッケージがインポートされました。
  • type T struct { ... }: テスト用の構造体 TErr error フィールドが追加されました。これにより、error 型の値をテンプレートに渡すための準備ができました。
  • var tVal = &T{ ... }: tValT 型のインスタンスで、テストケースでテンプレートに渡されるデータです。ここに Err: errors.New("erroozle") が追加され、Err フィールドに具体的なエラーオブジェクトが設定されました。
  • var execTests = []execTest{ ... }: テンプレートの実行テストケースの配列です。
    • {"bug5a", "{{.Err}}", "erroozle", tVal, true} という新しいテストケースが追加されました。
      • "bug5a": テストケースの名前。
      • "{{.Err}}": 実行するテンプレート文字列。tValErr フィールドの値を評価します。
      • "erroozle": 期待される出力文字列。errors.New("erroozle")Error() メソッドが返す文字列です。
      • tVal: テンプレートに渡すデータ。
      • true: 成功が期待されることを示すフラグ。 このテストケースの追加により、error 型の値がテンプレート内で正しくフォーマットされることが検証されます。

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

--- a/src/pkg/text/template/funcs.go
+++ b/src/pkg/text/template/funcs.go
@@ -72,7 +72,7 @@ func goodFunc(typ reflect.Type) bool {
 	switch {
 	case typ.NumOut() == 1:
 		return true
-	case typ.NumOut() == 2 && typ.Out(1) == osErrorType:
+	case typ.NumOut() == 2 && typ.Out(1) == errorType:
 		return true
 	}
 	return false
  • func goodFunc(typ reflect.Type) bool: この関数は、テンプレート内で呼び出し可能な関数が、Goの関数として適切であるかどうかを判断します。具体的には、戻り値の数や型をチェックします。 変更された行 case typ.NumOut() == 2 && typ.Out(1) == errorType: は、関数が2つの戻り値を持ち、2番目の戻り値が error 型である場合に true を返します。これはGoの慣習的なエラーハンドリングパターン(result, err := someFunc())に対応しています。 ここでの変更は、exec.go での変数名変更に合わせて osErrorType から errorType に修正されたもので、機能的な変更はありません。

関連リンク

参考にした情報源リンク