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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージに、複数のテンプレートを扱う方法を示す新しい例を追加するものです。具体的には、src/pkg/text/template/examplefiles_test.go という新しいテストファイルが追加され、ParseGlobExecuteTemplate、テンプレートのクローン作成といった機能を使って、異なるテンプレートを組み合わせたり、共有したりするシナリオがデモンストレーションされています。これは、ユーザーが text/template パッケージをより効果的に活用できるよう、具体的な使用例を提供することを目的としています。

コミット

commit dd001b59318cfdc5507c505ae66243459683054e
Author: Rob Pike <r@golang.org>
Date:   Thu Mar 1 14:55:18 2012 +1100

    text/template: add examples that use multiple templates
    
    Fixes #2742.
    
    R=golang-dev, peterthrun, adg
    CC=golang-dev
    https://golang.org/cl/5699083

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

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

元コミット内容

text/template: add examples that use multiple templates

Fixes #2742.

変更の背景

Go言語の text/template パッケージは、テキストベースの出力を生成するための強力なツールですが、複数のテンプレートを組み合わせて使用する際のパターンやベストプラクティスは、特に初心者にとっては直感的ではない場合があります。コミットメッセージにある "Fixes #2742" は、おそらくこのパッケージの複数のテンプレート利用に関する既存の課題や、より明確なドキュメント/例の必要性を示唆しています。

このコミットの目的は、以下のようなシナリオにおける text/template の利用方法を具体的に示すことで、開発者がより簡単に、かつ正確にテンプレートを扱えるようにすることです。

  • 複数のテンプレートファイルの読み込み: ParseGlob のような関数を使って、ディレクトリ内の複数のテンプレートファイルを一度に読み込む方法。
  • テンプレートのネストと呼び出し: {{template "name"}} アクションを使って、あるテンプレートから別のテンプレートを呼び出す方法。
  • テンプレートの共有と分離: 共通のヘルパーテンプレートを定義し、それを異なる「ドライバー」テンプレートで再利用する方法、または同じドライバーテンプレートを異なるヘルパーセットで実行する方法。

これらの例を提供することで、開発者は text/template の柔軟性と機能をより深く理解し、自身のアプリケーションで複雑なテンプレート構造を構築する際の指針を得ることができます。

前提知識の解説

このコミットの変更内容を理解するためには、Go言語の text/template パッケージに関する以下の基本的な知識が必要です。

  • text/template パッケージ: Go言語の標準ライブラリの一つで、プレーンテキストの出力を生成するためのテンプレートエンジンを提供します。HTMLの生成には html/template パッケージが推奨されますが、基本的な構文と機能は text/template と共通しています。
  • テンプレートの構文:
    • {{.FieldName}}: データ構造のフィールドやマップのキーの値を出力します。
    • {{range .Slice}}...{{end}}: スライスや配列、マップをイテレートします。
    • {{if .Condition}}...{{else}}...{{end}}: 条件分岐を行います。
    • {{define "name"}}...{{end}}: 名前付きテンプレートを定義します。これにより、テンプレートを再利用可能な部品として定義できます。
    • {{template "name"}}: 定義された名前付きテンプレートを呼び出します。
  • template.ParseGlob(pattern string): 指定されたglobパターンに一致するすべてのファイルを読み込み、それらを単一の *template.Template オブジェクトに関連付けます。最初に解析されたテンプレートが、そのテンプレートセットの「メイン」テンプレートとなります。
  • template.Execute(wr io.Writer, data interface{}): テンプレートセットのメインテンプレートを実行し、結果を指定された io.Writer に書き込みます。
  • template.ExecuteTemplate(wr io.Writer, name string, data interface{}): テンプレートセット内の指定された名前のテンプレートを実行し、結果を指定された io.Writer に書き込みます。これは、メインテンプレート以外の特定のテンプレートを実行したい場合に便利です。
  • template.Clone(): 既存の *template.Template オブジェクトのコピーを作成します。これにより、元のテンプレートセットに影響を与えることなく、新しいテンプレート定義を追加したり、既存の定義を上書きしたりすることができます。これは、異なるコンテキストで同じ基本テンプレートセットを使用したい場合に特に役立ちます。
  • template.Must(t *Template, err error): テンプレートの解析や初期化でエラーが発生した場合にパニックを発生させるヘルパー関数です。通常、プログラムの起動時にテンプレートをロードする際に使用され、テンプレートのロードが失敗した場合にはプログラムを終了させます。
  • os.TempDir()ioutil.TempDir(): 一時ディレクトリを作成するための関数です。テストや一時的なファイル操作によく使用されます。
  • filepath.Join(): OSに依存しない形でパスを結合します。

技術的詳細

追加された examplefiles_test.go ファイルは、text/template パッケージの複数のテンプレートを扱うための3つの主要な例 (ExampleTemplate_glob, ExampleTemplate_helpers, ExampleTemplate_share) を含んでいます。これらの例は、一時ディレクトリにテンプレートファイルを作成し、それらを text/template パッケージで解析・実行するという共通のパターンに従っています。

createTestDir 関数

すべての例で共通して使用されるヘルパー関数です。

  • createTestDir([]templateFile): templateFile スライスを受け取り、一時ディレクトリを作成し、その中に指定された内容でテンプレートファイルを作成します。これにより、テスト環境を簡単にセットアップできます。

ExampleTemplate_glob

この例は、template.ParseGlob を使用して、ディレクトリ内の複数のテンプレートファイルを一度に読み込む方法を示しています。

  • T0.tmpl: T1 を呼び出すプレーンなテンプレート。
  • T1.tmpl: T1 を定義し、T2 を呼び出すテンプレート。
  • T2.tmpl: T2 を定義するテンプレート。
  • template.ParseGlob(pattern)*.tmpl に一致するすべてのファイルを解析します。この際、最初にマッチしたファイル(この場合は T0.tmpl)がテンプレートセットのメインテンプレートになります。
  • tmpl.Execute(os.Stdout, nil) を呼び出すと、メインテンプレートである T0.tmpl が実行され、T1T2 とネストされた呼び出しが解決されて出力されます。

ExampleTemplate_helpers

この例は、共通のヘルパーテンプレートをロードし、それらに手動で「ドライバー」テンプレートを追加する方法を示しています。

  • T1.tmplT2.tmpl をヘルパーテンプレートとして template.ParseGlob でロードします。
  • templates.Parse("{{define driver1}}...")templates.Parse("{{define driver2}}...") を使って、driver1driver2 という新しいテンプレートを既存のテンプレートセットに追加します。
  • templates.ExecuteTemplate(os.Stdout, "driver1", nil)templates.ExecuteTemplate(os.Stdout, "driver2", nil) を使って、それぞれのドライバーテンプレートを実行します。これにより、共通のヘルパーテンプレート (T1, T2) を異なるコンテキスト (driver1, driver2) で再利用できることが示されます。

ExampleTemplate_share

この例は、同じドライバーテンプレートのグループを、異なるヘルパーテンプレートのセットと組み合わせて使用する方法を示しています。これは、template.Clone() メソッドの重要なユースケースです。

  • T0.tmplT1.tmpl をドライバーテンプレートとして template.ParseGlob でロードします。T1T2 を呼び出しますが、T2 はまだ定義されていません。
  • drivers.Clone() を使用して、ドライバーテンプレートのセットをクローンします。
  • first というクローンに対して {{define "T2"}}T2, version A{{end}} を解析し、T2 のバージョンAを定義します。
  • 再度 drivers.Clone() を使用して、別のクローン second を作成し、これには {{define "T2"}}T2, version B{{end}} を解析して T2 のバージョンBを定義します。
  • second.ExecuteTemplatefirst.ExecuteTemplate を実行することで、それぞれのクローンが独立した T2 の定義を使用し、元の drivers テンプレートセットには影響を与えないことが示されます。これは、テンプレートのスコープと独立性を管理する上で非常に強力な機能です。

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

このコミットで追加された唯一のファイルは src/pkg/text/template/examplefiles_test.go です。

--- /dev/null
+++ b/src/pkg/text/template/examplefiles_test.go
@@ -0,0 +1,182 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package template_test
+
+import (
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"text/template"
+)
+
+// templateFile defines the contents of a template to be stored in a file, for testing.
+type templateFile struct {
+	name     string
+	contents string
+}
+
+func createTestDir(files []templateFile) string {
+	dir, err := ioutil.TempDir("", "template")
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, file := range files {
+		f, err := os.Create(filepath.Join(dir, file.name))
+		if err != nil {
+			log.Fatal(err)
+		}
+		defer f.Close()
+		_, err = io.WriteString(f, file.contents)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	return dir
+}
+
+// Here we demonstrate loading a set of templates from a directory.
+func ExampleTemplate_glob() {
+	// Here we create a temporary directory and populate it with our sample
+	// template definition files; usually the template files would already
+	// exist in some location known to the program.
+	dir := createTestDir([]templateFile{
+		// T0.tmpl is a plain template file that just invokes T1.
+		{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
+		// T1.tmpl defines a template, T1 that invokes T2.
+		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
+		// T2.tmpl defines a template T2.
+		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
+	})
+	// Clean up after the test; another quirk of running as an example.
+	defer os.RemoveAll(dir)
+
+	// pattern is the glob pattern used to find all the template files.
+	pattern := filepath.Join(dir, "*.tmpl")
+
+	// Here starts the example proper.
+	// T0.tmpl is the first name matched, so it becomes the starting template,
+	// the value returned by ParseGlob.
+	tmpl := template.Must(template.ParseGlob(pattern))
+
+	err := tmpl.Execute(os.Stdout, nil)
+	if err != nil {
+		log.Fatalf("template execution: %s", err)
+	}
+	// Output:
+	// T0 invokes T1: (T1 invokes T2: (This is T2))
+}
+
+// This example demonstrates one way to share some templates
+// and use them in different contexts. In this variant we add multiple driver
+// templates by hand to an existing bundle of templates.
+func ExampleTemplate_helpers() {
+	// Here we create a temporary directory and populate it with our sample
+	// template definition files; usually the template files would already
+	// exist in some location known to the program.
+	dir := createTestDir([]templateFile{
+		// T1.tmpl defines a template, T1 that invokes T2.
+		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
+		// T2.tmpl defines a template T2.
+		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
+	})
+	// Clean up after the test; another quirk of running as an example.
+	defer os.RemoveAll(dir)
+
+	// pattern is the glob pattern used to find all the template files.
+	pattern := filepath.Join(dir, "*.tmpl")
+
+	// Here starts the example proper.
+	// Load the helpers.
+	templates := template.Must(template.ParseGlob(pattern))
+	// Add one driver template to the bunch; we do this with an explicit template definition.
+	_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\\n{{end}}")
+	if err != nil {
+		log.Fatal("parsing driver1: ", err)
+	}
+	// Add another driver template.
+	_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\\n{{end}}")
+	if err != nil {
+		log.Fatal("parsing driver2: ", err)
+	}
+	// We load all the templates before execution. This package does not require
+	// that behavior but html/template's escaping does, so it's a good habit.
+	err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
+	if err != nil {
+		log.Fatalf("driver1 execution: %s", err)
+	}
+	err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
+	if err != nil {
+		log.Fatalf("driver2 execution: %s", err)
+	}
+	// Output:
+	// Driver 1 calls T1: (T1 invokes T2: (This is T2))
+	// Driver 2 calls T2: (This is T2)
+}
+
+// This example demonstrates how to use one group of driver
+// templates with distinct sets of helper templates.
+func ExampleTemplate_share() {
+	// Here we create a temporary directory and populate it with our sample
+	// template definition files; usually the template files would already
+	// exist in some location known to the program.
+	dir := createTestDir([]templateFile{
+		// T0.tmpl is a plain template file that just invokes T1.
+		{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\\n"},
+		// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
+		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
+	})
+	// Clean up after the test; another quirk of running as an example.
+	defer os.RemoveAll(dir)
+
+	// pattern is the glob pattern used to find all the template files.
+	pattern := filepath.Join(dir, "*.tmpl")
+
+	// Here starts the example proper.
+	// Load the drivers.
+	drivers := template.Must(template.ParseGlob(pattern))
+
+	// We must define an implementation of the T2 template. First we clone
+	// the drivers, then add a definition of T2 to the template name space.
+
+	// 1. Clone the helper set to create a new name space from which to run them.
+	first, err := drivers.Clone()
+	if err != nil {
+		log.Fatal("cloning helpers: ", err)
+	}
+	// 2. Define T2, version A, and parse it.
+	_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
+	if err != nil {
+		log.Fatal("parsing T2: ", err)
+	}
+
+	// Now repeat the whole thing, using a different version of T2.
+	// 1. Clone the drivers.
+	second, err := drivers.Clone()
+	if err != nil {
+		log.Fatal("cloning drivers: ", err)
+	}
+	// 2. Define T2, version B, and parse it.
+	_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
+	if err != nil {
+		log.Fatal("parsing T2: ", err)
+	}
+
+	// Execute the templates in the reverse order to verify the
+	// first is unaffected by the second.
+	err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
+	if err != nil {
+		log.Fatalf("second execution: %s", err)
+	}
+	err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
+	if err != nil {
+		log.Fatalf("first: execution: %s", err)
+	}
+
+	// Output:
+	// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
+	// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))

コアとなるコードの解説

src/pkg/text/template/examplefiles_test.go は、Goのテストフレームワークの「Example」関数として実装されており、ドキュメントの一部として自動的にテストされ、出力が検証されます。これにより、コード例が常に最新かつ正確であることが保証されます。

このファイルは、text/template パッケージの柔軟性と強力な機能を、具体的なコード例を通じて示しています。

  1. templateFile 構造体と createTestDir 関数:

    • templateFile は、テスト用のテンプレートファイルの名前と内容を保持するシンプルな構造体です。
    • createTestDir は、これらの templateFile オブジェクトを受け取り、一時ディレクトリ内に実際のファイルとして書き出すユーティリティ関数です。これにより、各例が独立したクリーンな環境で実行され、実際のファイルシステムからのテンプレート読み込みをシミュレートできます。
  2. ExampleTemplate_glob:

    • この例は、template.ParseGlob を使って複数のテンプレートファイルを一度にロードする最も基本的な方法を示します。
    • T0.tmplT1 を呼び出し、T1T2 を呼び出すというネストされた構造を通じて、テンプレート間の依存関係と呼び出しフローを明確に示しています。
    • 出力が期待通りになることで、ParseGlob がすべての関連テンプレートを正しく解析し、それらが互いに参照できることを確認しています。
  3. ExampleTemplate_helpers:

    • この例は、共通の「ヘルパー」テンプレート(T1, T2)を定義し、それらを異なる「ドライバー」テンプレート(driver1, driver2)から利用する方法を示します。
    • templates.Parse("{{define driverX}}...") を使って、プログラム内で動的に新しいテンプレート定義を追加できることを示しています。これは、アプリケーションのロジックに基づいてテンプレートを生成したり、既存のテンプレートセットに特定の機能を注入したりする場合に非常に有用です。
    • ExecuteTemplate を使用して特定の名前のテンプレートを実行することで、テンプレートセット内の任意のテンプレートをエントリポイントとして使用できることを示しています。
  4. ExampleTemplate_share:

    • この例は、template.Clone() メソッドの最も強力なユースケースの一つを示しています。
    • 同じ「ドライバー」テンプレート(T0.tmpl, T1.tmpl)のセットをロードした後、それをクローンして、それぞれ異なるバージョンの「ヘルパー」テンプレート(T2)を定義しています。
    • first クローンでは T2, version A を、second クローンでは T2, version B を定義し、それぞれのクローンが独立した T2 の定義を持つことを確認しています。
    • この機能は、例えば、同じ基本レイアウトを持つが、特定のコンポーネント(この場合は T2)の振る舞いが異なる複数のウェブページを生成する場合など、非常に複雑なテンプレート構造を管理する際に不可欠です。元の drivers テンプレートセットがクローンによって変更されないことも保証されます。

これらの例は、text/template パッケージの設計思想である「テンプレートは名前空間を持つ」という概念を具体的に示しており、開発者がテンプレートをモジュール化し、再利用し、異なるコンテキストで独立して管理するための強力なパターンを提供します。

関連リンク

  • Go言語 text/template パッケージ公式ドキュメント: https://pkg.go.dev/text/template
  • Go言語 html/template パッケージ公式ドキュメント (セキュリティ上の理由からHTML生成にはこちらが推奨されますが、基本的な構文は共通です): https://pkg.go.dev/html/template
  • Go言語のIssueトラッカー (このコミットが修正したとされる #2742 は、Goの内部的なIssueトラッカーのIDである可能性があり、GitHubの公開Issueとは異なる場合があります): https://github.com/golang/go/issues

参考にした情報源リンク

  • Go言語 text/template パッケージのソースコードとドキュメント
  • Go言語の公式ドキュメント
  • GitHubのコミット履歴
  • Go言語のテストにおけるExample関数の慣習