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

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

コミット

commit 065db4ea99f80cce6d9ed794467697955f0eaa2e
Author: Rob Pike <r@golang.org>
Date:   Tue Apr 3 11:44:52 2012 +1000

    text/template: pipelined arg was not typechecked
    Without this fix, an erroneous template causes a panic; should be caught safely.
    The bug did not affect correct templates.
    
    Fixes #3267.
    
    R=golang-dev, dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/5900065

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

https://github.com/golang/go/commit/065db4ea99f80cce6d9ed794467697955f0eaa2e

元コミット内容

このコミットは、Go言語の標準ライブラリである text/template パッケージにおけるバグ修正です。具体的には、パイプライン処理された引数(pipelined arg)が型チェックされていなかった問題に対処しています。このバグが存在すると、誤ったテンプレートがパニック(panic)を引き起こす可能性がありましたが、この修正により安全にエラーとして捕捉されるようになります。正しいテンプレートの動作には影響がありませんでした。

この修正は、Go issue #3267 を解決します。

変更の背景

Go言語の text/template パッケージは、Goプログラム内でテキストベースの出力を生成するための強力なツールです。HTML、XML、プレーンテキストなど、様々な形式のドキュメントを動的に生成する際に利用されます。テンプレートエンジンは、データ構造(通常はGoの構造体やマップ)とテンプレート文字列を組み合わせて最終的な出力を生成します。

このコミットが修正する問題は、テンプレート内でパイプライン処理(| 演算子)を使用して関数に引数を渡す際に発生していました。具体的には、パイプラインの最終結果が関数の引数として渡される際、その引数の型が関数の期待する型と一致しない場合に、本来であればエラーとして処理されるべきところが、Goランタイムのパニックを引き起こしていました。

パニックはGoプログラムにおける回復不可能なエラーであり、通常はプログラムの異常終了を意味します。テンプレートエンジンはユーザーからの入力や動的なデータに基づいて動作するため、不正な入力によってパニックが発生することは、アプリケーションの安定性やセキュリティにとって望ましくありません。このバグは、開発者が誤ったテンプレートを記述した場合に、デバッグが困難なパニックに遭遇する可能性を秘めていました。

この修正の目的は、このような型不一致のシナリオにおいて、パニックではなく、より安全で予測可能なエラー(errorf メソッドによるエラー報告)を発生させることで、テンプレートエンジンの堅牢性を向上させることです。これにより、開発者はテンプレートの記述ミスをより早期かつ安全に特定できるようになります。

前提知識の解説

このコミットの理解には、以下のGo言語の概念と text/template パッケージの基本的な知識が必要です。

  1. text/template パッケージ: Goの標準ライブラリの一つで、データとテンプレートを組み合わせてテキスト出力を生成するためのパッケージです。テンプレートは、Goのデータ構造のフィールドやメソッドにアクセスするためのアクション({{...}} で囲まれた部分)を含みます。

    • パイプライン (|): テンプレート内で、あるアクションの結果を別の関数やアクションの引数として渡すための演算子です。例えば、{{.Value | myFunc}}.Value の結果を myFunc に渡します。
    • 関数呼び出し: テンプレート内でGoの関数を呼び出すことができます。これらの関数は FuncMap を介してテンプレートに登録されます。
  2. reflect パッケージ: Goの標準ライブラリで、実行時に型情報(reflect.Type)や値情報(reflect.Value)を検査・操作するための機能を提供します。

    • reflect.Value: Goの変数の実行時の値を表します。
    • reflect.Type: Goの変数の実行時の型を表します。
    • Value.Call(): reflect.Value が関数を表す場合、このメソッドを使ってその関数を実行時に呼び出すことができます。引数は []reflect.Value のスライスとして渡されます。
    • Type.In(i int): 関数の reflect.Type から、i 番目の引数の reflect.Type を取得します。
    • Type.NumIn(): 関数の引数の数を返します。
    • Type.IsVariadic(): 関数が可変長引数(variadic arguments)を取るかどうかを返します。
    • Type.Elem(): ポインタ、配列、スライス、マップ、チャネルの要素型を返します。可変長引数の場合、最後の引数(スライスとして扱われる)の要素型を取得するために使用されます。
    • reflect.Zero(typ reflect.Type): 指定された型 typ のゼロ値を表す reflect.Value を返します。
  3. 型チェック: プログラムが実行される前に、変数や式の型が期待される型と一致するかどうかを確認するプロセスです。Goは静的型付け言語であり、コンパイル時に厳密な型チェックが行われますが、reflect パッケージを使用する動的な操作や、テンプレートエンジンのように実行時に型を扱う場合には、明示的な型チェックが必要になることがあります。

  4. パニック (Panic): Goにおけるランタイムエラーの一種で、通常はプログラムの異常終了を引き起こします。パニックは、配列の範囲外アクセス、nilポインタのデリファレンス、型アサーションの失敗など、回復不可能なエラー条件で発生します。

技術的詳細

このコミットの核心は、text/template パッケージの exec.go ファイル内の evalCall メソッドにあります。このメソッドは、テンプレート内で関数が呼び出される際に、その引数を評価し、関数を実際に呼び出す役割を担っています。

問題は、パイプライン処理の最終結果が関数の最後の引数として渡される際に、その値が関数の期待する型と一致しない場合に発生していました。以前の実装では、final という reflect.Value が直接 argv[i] に代入されていました。

// Old code snippet from evalCall
if final.IsValid() {
    argv[i] = final // ここで型チェックなしに代入されていた
}

ここで final はパイプラインの最終結果を表す reflect.Value です。argv は関数に渡される引数を格納する []reflect.Value のスライスです。この直接代入は、final の型が argv[i] が期待する型(つまり、呼び出される関数の引数の型)と互換性がない場合に、fun.Call(argv) が実行される際にパニックを引き起こす可能性がありました。例えば、関数が int を期待しているのに、パイプラインから string が渡された場合などです。

修正では、この直接代入の前に s.validateType メソッドを呼び出すことで、明示的な型チェックを導入しました。

// New code snippet from evalCall
if final.IsValid() {
    t := typ.In(typ.NumIn() - 1) // 関数の最後の引数の型を取得
    if typ.IsVariadic() {
        t = t.Elem() // 可変長引数の場合は、その要素型を取得
    }
    argv[i] = s.validateType(final, t) // validateType を介して型チェックと変換を行う
}

s.validateType メソッドは、渡された reflect.Value (final) が期待される reflect.Type (t) に変換可能かどうかを検証します。変換できない場合は、パニックではなく、s.errorf メソッドを使用してテンプレート実行エラーを報告します。これにより、不正な型が渡された場合でもプログラムが安全に終了し、開発者に対して具体的なエラーメッセージを提供できるようになります。

また、exec_test.go には、この修正を検証するための新しいテストケースが追加されています。特に、パイプライン処理された引数が型チェックされていなかったシナリオを再現し、修正後にパニックではなくエラーが報告されることを確認しています。

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

変更は主に以下の2つのファイルで行われています。

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

    • func (s *state) evalCall(...) メソッド内で、パイプラインの最終結果を関数の引数に代入する箇所が変更されました。
    • 変更前: argv[i] = final
    • 変更後:
      t := typ.In(typ.NumIn() - 1)
      if typ.IsVariadic() {
          t = t.Elem()
      }
      argv[i] = s.validateType(final, t)
      
    • func (s *state) validateType(...) メソッド内のコメントが更新されました。
  2. src/pkg/text/template/exec_test.go:

    • var execTests = []execTest{...} に新しいテストケースが追加されました。
      • {"bug8a", "{{3|oneArg}}", "", tVal, false},
      • {"bug8b", "{{4|dddArg 3}}", "", tVal, false},
    • 新しいヘルパー関数 dddArg が追加されました。
      func dddArg(a int, b ...string) string {
          return fmt.Sprintln(a, b)
      }
      
    • FuncMapdddArg が登録されました。
      • "dddArg": dddArg,

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/text/template/exec.goevalCall 関数内で行われた以下の行です。

t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() {
    t = t.Elem()
}
argv[i] = s.validateType(final, t)
  1. t := typ.In(typ.NumIn() - 1):

    • typ は現在呼び出そうとしている関数の reflect.Type です。
    • typ.NumIn() はその関数が受け取る引数の総数を返します。
    • typ.In(index) は指定されたインデックスの引数の reflect.Type を返します。
    • typ.NumIn() - 1 は、パイプラインの最終結果が渡される関数の最後の引数のインデックスを指します。これにより、関数が期待する最後の引数の型情報 t を取得します。
  2. if typ.IsVariadic() { t = t.Elem() }:

    • Goの関数は可変長引数(...type)を取ることができます。可変長引数は、Goの内部ではその型のスライスとして扱われます。
    • typ.IsVariadic() は、関数が可変長引数を取る場合に true を返します。
    • もし関数が可変長引数を取る場合、最後の引数 t はスライス型(例: []string)になります。しかし、パイプラインから渡される個々の値はスライスの要素型(例: string)に変換される必要があります。
    • t.Elem() は、スライス、配列、ポインタ、マップ、チャネルの要素型を返します。したがって、可変長引数の場合は、スライス型からその要素型(例: []string から string)を取得し、t を更新します。これにより、validateType が正しい要素型に対して型チェックを行うことができます。
  3. argv[i] = s.validateType(final, t):

    • final はパイプライン処理の最終結果を表す reflect.Value です。
    • t は、final が変換されるべき期待される reflect.Type です。
    • s.validateType(final, t) は、final の値が t の型に適切に変換できるかどうかを検証し、変換された reflect.Value を返します。
    • もし変換が不可能な場合、s.validateType はパニックを引き起こす代わりに、s.errorf を呼び出してテンプレート実行エラーを報告します。これにより、プログラムは安全にエラーを処理し、開発者に対してより有用なデバッグ情報を提供できます。

この変更により、text/template パッケージは、パイプライン処理された引数の型が関数の期待する型と一致しない場合に、パニックではなく、明確なエラーメッセージを返すようになり、テンプレートエンジンの堅牢性と使いやすさが向上しました。

関連リンク

参考にした情報源リンク