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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおける、Templates メソッドが nilTemplate オブジェクトに対して呼び出された際に発生するクラッシュ(パニック)を修正するものです。具体的には、Template 構造体の common フィールドが nil である場合に Templates メソッドが安全に nil を返すように変更されています。

コミット

commit bcccad40202ba895d237d9d0a921b33bc2c5601f
Author: Rob Pike <r@golang.org>
Date:   Sun Oct 7 09:26:59 2012 +1100

    text/template: fix nil crash on Templates
    Fixes #3872.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6612060

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

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

元コミット内容

text/template: fix nil crash on Templates Fixes #3872.

このコミットは、text/template パッケージの Templates メソッドが nilTemplate オブジェクトに対して呼び出された際に発生するクラッシュを修正します。これは Issue 3872 に対応するものです。

変更の背景

この変更は、Go言語の Issue トラッカーで報告された Issue 3872: text/template: Templates() crashes on nil Template に対応するものです。

報告された問題は、text/template パッケージにおいて、template.New("Name").Templates() のように、まだパースされていない(つまり、Parse メソッドなどが呼び出されていない)新しい Template オブジェクトに対して Templates() メソッドを呼び出すと、nil ポインタ参照によるパニックが発生するというものでした。

New 関数で作成された Template オブジェクトは、初期状態では内部の common フィールドが nil になっています。Templates() メソッドは、この common フィールドを通じてテンプレートのマップにアクセスしようとしますが、commonnil の場合、その参照がパニックを引き起こしていました。

この修正は、このような不正な状態での Templates() メソッドの呼び出しを安全に処理し、クラッシュを防ぐことを目的としています。

前提知識の解説

Go言語の text/template パッケージ

text/template パッケージは、Go言語の標準ライブラリの一部であり、テキストベースのテンプレートを生成するための機能を提供します。これは、HTML、XML、プレーンテキストなどの動的なコンテンツを生成する際に非常に便利です。

主要な概念は以下の通りです。

  • Template 構造体: テンプレートのインスタンスを表します。テンプレートの定義、パース、実行(データとの結合)を行います。
  • New 関数: 新しい Template オブジェクトを作成します。この時点ではテンプレートの内容は空です。
  • Parse メソッド: テンプレート文字列をパースし、Template オブジェクトにその構造をロードします。
  • Execute メソッド: パースされたテンプレートにデータを適用し、結果を io.Writer に書き出します。
  • Templates メソッド: 現在の Template オブジェクトに関連付けられているすべてのテンプレート(自身を含む)のスライスを返します。text/template パッケージでは、テンプレートは名前空間を持ち、{{template "name"}} のように他のテンプレートを呼び出すことができます。Templates メソッドは、これらの関連するテンプレートを一覧表示するために使用されます。

nil ポインタとパニック

Go言語では、ポインタが何も指していない状態を nil と表現します。nil ポインタに対してメソッドを呼び出したり、フィールドにアクセスしようとすると、ランタイムパニック(panic)が発生します。これは、プログラムの異常終了を引き起こす深刻なエラーです。

このコミットの背景にある問題は、Template オブジェクトが完全に初期化されていない(特に内部の common フィールドが nil のまま)状態で Templates() メソッドが呼び出されたときに、nil ポインタ参照が発生し、プログラムがクラッシュするというものでした。

Template.common フィールド

Template 構造体には、common という内部フィールドが存在します。これは、複数のテンプレート間で共有される共通の状態(例えば、名前付きテンプレートのマップなど)を保持するために使用されます。New 関数でテンプレートが作成された直後や、エラーが発生してテンプレートが適切に初期化されなかった場合などには、この common フィールドが nil になる可能性があります。

Templates() メソッドは、この common フィールドを通じて、関連するテンプレートのマップ (t.tmpl) にアクセスします。したがって、t.commonnil の場合、t.common.tmpl のようなアクセスはパニックを引き起こします。

技術的詳細

この修正は、text/template パッケージの Template 構造体における Templates() メソッドの堅牢性を向上させるものです。

修正前は、Templates() メソッドは Template オブジェクトの内部状態(特に t.common フィールド)が適切に初期化されていることを前提としていました。しかし、template.New("Name") のように、テンプレートが作成されただけでまだパースされていない場合、t.common フィールドは nil のままです。この状態で Templates() メソッドが呼び出されると、t.common.tmpl のような内部マップへのアクセスが nil ポインタデリファレンスを引き起こし、ランタイムパニックが発生していました。

このコミットでは、Templates() メソッドの冒頭に nil チェックを追加することでこの問題を解決しています。

func (t *Template) Templates() []*Template {
	if t.common == nil { // 追加されたnilチェック
		return nil
	}
	// 既存のロジック: common.tmpl マップからテンプレートを収集
	m := make([]*Template, 0, len(t.tmpl))
	for _, v := range t.tmpl {
		m = append(m, v)
	}
	return m
}

この変更により、t.commonnil の場合、Templates() メソッドは安全に nil スライスを返します。これにより、nil ポインタパニックが回避され、プログラムの安定性が向上します。Templates() メソッドのドキュメントには、Templatest 自身を含む t に関連付けられたテンプレートのスライスを返すと記載されていますが、nilTemplate オブジェクトに対しては、関連付けられたテンプレートが存在しないため、nil スライスを返すのが適切な振る舞いと判断されたと考えられます。

また、この修正にはテストケース TestExecuteOnNewTemplate が追加されています。このテストは、New("Name").Templates() を呼び出すことで、以前パニックを引き起こしていたシナリオを再現し、修正が正しく機能することを確認します。テストは単にこの呼び出しを実行し、パニックが発生しないことを暗黙的に検証します。

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

src/pkg/text/template/exec_test.go

--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -811,3 +811,8 @@ func TestTree(t *testing.T) {
 		t.Errorf("expected %q got %q", expect, result)
 	}
 }
+
+func TestExecuteOnNewTemplate(t *testing.T) {
+	// This is issue 3872.
+	_ = New("Name").Templates()
+}

src/pkg/text/template/template.go

--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -117,6 +117,9 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
 // Templates returns a slice of the templates associated with t, including t
 // itself.
 func (t *Template) Templates() []*Template {
+\tif t.common == nil {
+\t\treturn nil
+\t}
 	// Return a slice so we don't expose the map.
 	m := make([]*Template, 0, len(t.tmpl))
 	for _, v := range t.tmpl {

コアとなるコードの解説

このコミットの核心的な変更は、src/pkg/text/template/template.go ファイル内の Template 構造体の Templates() メソッドに追加された nil チェックです。

func (t *Template) Templates() []*Template {
	if t.common == nil { // ここが追加された行
		return nil
	}
	// ... 既存のロジック ...
}
  • func (t *Template) Templates() []*Template: これは Template 型のメソッド Templates のシグネチャです。このメソッドは Template オブジェクトに関連付けられたすべてのテンプレートのスライス ([]*Template) を返します。
  • if t.common == nil { ... }: この条件文が追加された修正の肝です。t.commonTemplate 構造体の内部フィールドで、複数のテンプレート間で共有される共通のデータ(例えば、名前付きテンプレートのマップなど)を保持します。template.New("Name") のように、テンプレートが作成されたばかりでまだパースされていない場合、この t.common フィールドは nil になります。
  • return nil: t.commonnil である場合、つまりテンプレートがまだ完全に初期化されていないか、有効な内部状態を持っていない場合、このメソッドは nil[]*Template スライスを返します。これにより、後続の t.common.tmpl へのアクセスが nil ポインタデリファレンスを引き起こすのを防ぎ、パニックを回避します。

このシンプルな nil チェックの追加により、Templates() メソッドはより堅牢になり、不正な状態の Template オブジェクトに対しても安全に呼び出すことができるようになりました。

また、src/pkg/text/template/exec_test.go に追加されたテストケース TestExecuteOnNewTemplate は、この修正が意図通りに機能することを確認するためのものです。

func TestExecuteOnNewTemplate(t *testing.T) {
	// This is issue 3872.
	_ = New("Name").Templates()
}

このテストは、New("Name").Templates() を呼び出すだけで、以前パニックが発生していたシナリオを再現します。Goのテストフレームワークでは、テスト関数内でパニックが発生するとテストが失敗するため、このテストが成功するということは、Templates() メソッドが nilTemplate に対して呼び出されてもパニックが発生しないことを意味します。

関連リンク

参考にした情報源リンク