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

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

このコミットは、Go言語の初期の段階におけるテンプレートエンジンの導入と、それを用いたコードの整形(pretty printing)機能の改善に関するものです。具体的には、usr/gri/pretty ディレクトリ内のファイルが変更されています。

変更されたファイルは以下の通りです。

  • usr/gri/pretty/Makefile: ビルド設定ファイル。新しく追加された template.go ファイルをビルドに含めるように変更されています。
  • usr/gri/pretty/printer.go: コードの整形(pretty printing)を担当するファイル。手書きのHTML生成ロジックが削除され、新しく導入されたテンプレートパッケージを使用するように変更されています。
  • usr/gri/pretty/template.go: 新しく追加されたテンプレートパッケージのソースコード。シンプルなテンプレート置換機能を提供します。
  • usr/gri/pretty/template.html: テンプレートパッケージが使用するHTMLテンプレートファイル。

コミット

commit 5e400ebf18aeee7bc26f10b25044d15b8b4379e6
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Mar 6 16:11:40 2009 -0800

    - wrote simple template substitution package
    - use template in pretty printing now instead of hand-coded mechanism
    
    R=r
    OCL=25863
    CL=25863

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

https://github.com/golang/go/commit/5e400ebf18aeee7bc26f10b25044d15b8b4379e6

元コミット内容

このコミットの目的は以下の2点です。

  • シンプルなテンプレート置換パッケージを作成した。
  • 手書きのメカニズムの代わりに、このテンプレートパッケージを整形出力(pretty printing)に使用するようにした。

変更の背景

このコミットは2009年3月に行われており、Go言語がまだ開発の非常に初期段階にあったことを示しています。当時のGo言語には、現在標準ライブラリとして提供されている text/templatehtml/template パッケージは存在していませんでした(これらのパッケージは2011年9月頃に現在の形に導入されました)。

printer.go は、GoのコードをHTML形式で整形出力する機能を持っていましたが、そのHTML生成ロジックはファイル内に直接ハードコードされており、手動で文字列結合や置換を行っていました。このような手書きのHTML生成は、コードの可読性を低下させ、保守を困難にし、将来的な変更や拡張を非効率にするという問題がありました。

このコミットは、このような問題を解決するために、より汎用的なテンプレート置換メカニズムを導入し、整形出力ロジックからHTML構造の定義を分離することを目的としています。これにより、HTMLの構造変更が容易になり、printer.go のコードがよりクリーンで保守しやすくなります。

前提知識の解説

テンプレートエンジンとは

テンプレートエンジンとは、動的なコンテンツ(HTML、XML、テキストなど)を生成するためのソフトウェアコンポーネントです。データとテンプレート(コンテンツの構造を定義するファイル)を組み合わせて最終的な出力を生成します。主な目的は、プレゼンテーションロジック(表示方法)とビジネスロジック(データの処理)を分離することにあります。

テンプレートエンジンは通常、以下の要素で構成されます。

  • テンプレートファイル: プレースホルダーや制御構造(条件分岐、ループなど)を含む、出力の骨格となるファイル。
  • データ: テンプレートに埋め込まれる動的な情報。
  • テンプレートエンジン本体: テンプレートファイルとデータを処理し、最終的な出力を生成するプログラム。

このコミットで導入されたテンプレートパッケージは、非常にシンプルな「置換」機能に特化しており、特定のプレフィックスとキーに一致する部分を、対応する関数が生成するコンテンツで置き換えるというメカニズムを採用しています。

Go言語の初期開発段階

Go言語は2009年11月に一般公開されましたが、このコミットはその数ヶ月前に行われています。当時のGo言語はまだ実験的な段階であり、標準ライブラリも現在のように充実していませんでした。そのため、現在では標準ライブラリで提供されているような基本的な機能(例:テンプレートエンジン)も、個々のプロジェクトや実験的なコードベースで独自に実装されることが一般的でした。このコミットは、Go言語の進化の過程で、どのようにして基本的なユーティリティが構築されていったかを示す一例と言えます。

技術的詳細

このコミットの核となるのは、新しく追加された usr/gri/pretty/template.go ファイルで定義される template パッケージです。

template パッケージの構造

template パッケージは、Template という構造体を定義しています。

type Template struct {
	template []byte;
}

この構造体は、読み込まれたテンプレートの内容をバイトスライス template として保持します。

Init メソッド

Template 構造体には Init メソッドがあり、指定されたファイルからテンプレートの内容を読み込みます。

func (T *Template) Init(filename string) *os.Error {
	fd, err0 := os.Open(filename, os.O_RDONLY, 0);
	defer fd.Close();
	if err0 != nil {
		return err0;
	}

	var buf io.ByteBuffer;
	len, err1 := io.Copy(fd, &buf);
	if err1 == io.ErrEOF {
		err1 = nil;
	}
	if err1 != nil {
		return err1;
	}

	T.template = buf.Data();
	
	return nil;
}

このメソッドは、ファイルを開き、その内容を io.ByteBuffer にコピーし、最終的に Template 構造体の template フィールドにバイトスライスとして格納します。エラーハンドリングも含まれています。

match および find ヘルパー関数

テンプレート置換のロジックをサポートするために、matchfind という2つのヘルパー関数が定義されています。

  • match(buf []byte, s string) bool: buf の先頭が文字列 s と一致するかどうかを判定します。
  • find(buf []byte, s string, i int) int: buf 内で文字列 s を、指定されたインデックス i から検索し、最初に見つかった位置を返します。見つからない場合は負の値を返します。

これらの関数は、テンプレート内のプレースホルダーを効率的に見つけるために使用されます。

Substitution 型と Apply メソッド

テンプレート置換の核心は Substitution 型と Apply メソッドにあります。

type Substitution map [string] func()

func (T *Template) Apply(w io.Write, prefix string, subs Substitution) *os.Error {
	i0 := 0;  // position from which to write from the template
	i1 := 0;  // position from which to look for the next prefix
	
	for {
		// look for a prefix
		i2 := find(T.template, prefix, i1);  // position of prefix, if any
		if i2 < 0 {
			// no prefix found, we are done
			break;
		}

		// we have a prefix, look for a matching key
		i1 = i2 + len(prefix);
		for key, action := range subs {
			if match(T.template[i1 : len(T.template)], key) {
				// found a match
				i1 += len(key);  // next search starting pos				
				len, err := w.Write(T.template[i0 : i2]);  // TODO handle errors
				i0 = i1;  // skip placeholder
				action();
				break;
			}
		}
	}

	// write the rest of the template
	len, err := w.Write(T.template[i0 : len(T.template)]);  // TODO handle errors
	return err;
}
  • Substitution: map[string]func() 型として定義されており、キー(文字列)と、そのキーに対応する処理を行う関数(引数なし、戻り値なし)のペアを保持します。この関数が、テンプレート内のプレースホルダーが置換される際に実行されるロジックを提供します。
  • Apply(w io.Write, prefix string, subs Substitution) *os.Error: このメソッドがテンプレートの置換処理を実行します。
    • w io.Write: 出力先となる io.Writer インターフェース(例: os.Stdoutbytes.Buffer)。
    • prefix string: テンプレート内の置換対象の開始を示すプレフィックス文字列(例: <!--)。
    • subs Substitution: 置換ルールを定義するマップ。

Apply メソッドの動作は以下の通りです。

  1. テンプレート全体を走査し、指定された prefix を探します。
  2. prefix が見つかった場合、その直後から subs マップのキーと一致する文字列を探します。
  3. 一致するキーが見つかった場合、そのキーに対応する関数 action() を実行します。この action 関数は、io.Writer に直接書き込むことで、動的なコンテンツを生成します。
  4. prefix からキーまでの部分(プレースホルダー自体)はスキップされ、次の検索は置換された部分の直後から開始されます。
  5. prefix が見つからなくなるまでこのプロセスを繰り返します。
  6. 最後に、テンプレートの残りの部分をすべて出力します。

このメカニズムにより、template.html のようなファイルに <!--BODY--><!--PACKAGE--> のようなプレースホルダーを定義し、printer.go 側でこれらのプレースホルダーに対応するコンテンツ生成関数を Substitution マップとして提供することで、HTMLの構造とコンテンツ生成ロジックを分離しています。

printer.go の変更

printer.go は、この新しいテンプレートパッケージを利用するように大幅に修正されました。

  • 手書きHTML生成コードの削除: 以前の HtmlPrologue および HtmlEpilogue 関数内で直接HTML文字列を生成していた部分が削除されました。これには、template_name, html_template, body_tag, body_index といった定数や変数、およびそれらを初期化する init ブロックも含まれます。
  • template パッケージのインポート: import "template"; が追加されました。
  • template.Template インスタンスの利用: var templ template.Template; が宣言され、printer.goinit 関数内で templ.Init("template.html"); が呼び出され、テンプレートファイルが読み込まれるようになりました。
  • templ.Apply の呼び出し: Print 関数内で、HTML出力が有効な場合 (P.html が true の場合) に templ.Apply が呼び出されるようになりました。
	if P.html {
		err := templ.Apply(text, "<!--", template.Substitution {
			"PACKAGE-->" : func() { /* P.Expr(prog.Ident); */ },
			"BODY-->" : func() { P.Program(prog); },
		});
		if err != nil {
			panic("print error - exiting");
		}
	} else {
		P.Program(prog);
	}

ここで注目すべきは、Substitution マップに渡される関数リテラルです。

  • "PACKAGE-->" に対応する関数は、コメントアウトされていますが、本来はパッケージ名を整形出力するロジックが来ることを意図しています。
  • "BODY-->" に対応する関数は P.Program(prog); を呼び出しています。これは、Goプログラムの抽象構文木 (AST) を実際に整形出力する主要なロジックです。これにより、HTMLテンプレートの <!--BODY--> の位置に、整形されたGoコードが出力されるようになります。

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

usr/gri/pretty/Makefile

--- a/usr/gri/pretty/Makefile
+++ b/usr/gri/pretty/Makefile
@@ -44,7 +44,7 @@ parser.6:	ast.6 symboltable.6
 
 platform.6:	 utils.6
 
-printer.6:	 utils.6 ast.6 symboltable.6
+printer.6:	 utils.6 ast.6 symboltable.6 template.6
 
 %.6:	%.go
 	$(G) $(F) $<\

printer.6 の依存関係に template.6 が追加され、template.go がビルドプロセスに含まれるようになりました。

usr/gri/pretty/printer.go

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -15,6 +15,7 @@ import (
 	Utils "utils";
 	"token";
 	"ast";
+	"template"; // 新しいテンプレートパッケージのインポート
 	SymbolTable "symboltable";
 )
 
@@ -389,98 +390,6 @@ func (P *Printer) Error(pos int, tok int, msg string) {
 // ----------------------------------------------------------------------------
 // HTML support
 
-const template_name = "template.html" // 削除
-var html_template string  // TODO should probably be []byte // 削除
-
-// tags for substitution in html_template // 削除
-const body_tag = "<!--BODY-->"; // 削除
-
-// indexes of various tags in html_template // 削除
-var body_index int; // 削除
-
-func init() { // 削除
-	fd, err0 := os.Open(template_name, os.O_RDONLY, 0);
-	defer fd.Close();
-	if err0 != nil {
-		panic("cannot open html template");
-	}
-
-	// TODO not sure why this didn't work
-	/*
-	var buf io.ByteBuffer;
-	len, err1 := io.Copy(fd, buf);
-	if err1 == io.ErrEOF {
-		err1 = nil;
-	}
-	if err1 != nil {
-		panic("cannot read html template");
-	}
-	if len == 0 {
-		panic("html template empty");
-	}
-	html_template = string(buf.AllData());
-	*/
-
-	var buf [8*1024]byte;
-	len, err1 := io.Readn(fd, buf);
-	if err1 == io.ErrEOF {
-		err1 = nil;
-	}
-	if err1 != nil {
-		panic("cannot read html template");
-	}
-	if len == 0 {
-		panic("html template empty");
-	}
-	html_template = string(buf[0 : len]);
-
-	body_index = strings.Index(html_template, body_tag);
-	if body_index < 0 {
-		panic("html_template has no BODY tag");
-	}
-}
-
-
-func (P *Printer) HtmlPrologue(title string) { // 削除
-	if P.html {
-		P.Printf("%s\n", html_template[0 : body_index]);
-		P.Printf("<h1>%s</h1>\n", "package " + title);
-		P.Printf("<pre>\n");
-		/*
-		P.TaggedString(0,
-			"<html>\n"
-			"<head>\n"
-			"\t<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n"
-			"\t<title>" + P.htmlEscape(title) + "</title>\n"
-			"\t<style type=\"text/css\">\n"
-			"\t</style>\n"
-			"</head>\n"
-			"<body>\n"
-			"<pre>\n",
-			"", ""
-		)
-		*/
-	}
-}
-
-
-func (P *Printer) HtmlEpilogue() { // 削除
-	if P.html {
-		P.String(0, "");  // flush
-		P.Printf("</pre>\n");
-		P.Printf("%s", html_template[body_index : len(html_template)]);
-		/*
-		P.TaggedString(0,
-			"</pre>\n"
-			"</body>\n"
-			"<html>\n",
-			"", ""
-		)
-		*/
-	}
-}
-
-
 func (P *Printer) HtmlIdentifier(x *ast.Ident) {
  	P.String(x.Pos_, x.Str);
  	/*
@@ -1182,10 +1091,17 @@ func (P *Printer) Program(p *ast.Program) {
 // ----------------------------------------------------------------------------
 // External interface
 
+var templ template.Template; // template.Template型の変数を宣言
+
+func init() { // 新しいinit関数でテンプレートを初期化
+	templ.Init("template.html");
+}
+
+
 func Print(writer io.Write, html bool, prog *ast.Program) {
  	// setup
  	var P Printer;
  	P.Init(writer, html, prog.Comments);
  	text := tabwriter.New(writer, *tabwidth, 1, padchar, true, html);
  	P.Init(text, html, prog.Comments);
 
-	// TODO would be better to make the name of the src file be the title
-	P.HtmlPrologue(prog.Ident.Str); // 削除
-	P.Program(prog); // 削除
-	P.HtmlEpilogue(); // 削除
+	if P.html { // HTML出力の場合、テンプレートを適用
+		err := templ.Apply(text, "<!--", template.Substitution {
+			"PACKAGE-->" : func() { /* P.Expr(prog.Ident); */ }, // PACKAGEプレースホルダーの置換関数
+			"BODY-->" : func() { P.Program(prog); }, // BODYプレースホルダーの置換関数
+		});
+		if err != nil {
+			panic("print error - exiting");
+		}
+	} else { // HTML出力でない場合、直接プログラムを整形出力
+		P.Program(prog);
+	}
  
  	P.String(0, "");  // flush pending separator/newlines
  	err := text.Flush();

手書きのHTML生成ロジックが削除され、template パッケージの Apply メソッドを呼び出す形に変更されました。

usr/gri/pretty/template.go

--- /dev/null
+++ b/usr/gri/pretty/template.go
@@ -0,0 +1,105 @@
+// Copyright 2009 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 // 新しいtemplateパッケージの定義
+
+import (
+	"os";
+	"io";
+)
+
+
+type Template struct { // Template構造体の定義
+	template []byte;
+}
+
+
+func (T *Template) Init(filename string) *os.Error { // Initメソッド:テンプレートファイルを読み込む
+	fd, err0 := os.Open(filename, os.O_RDONLY, 0);
+	defer fd.Close();
+	if err0 != nil {
+		return err0;
+	}
+
+	var buf io.ByteBuffer;
+	len, err1 := io.Copy(fd, &buf);
+	if err1 == io.ErrEOF {
+		err1 = nil;
+	}
+	if err1 != nil {
+		return err1;
+	}
+
+	T.template = buf.Data();
+	
+	return nil;
+}
+
+
+// Returns true if buf starts with s, returns false otherwise.
+
+func match(buf []byte, s string) bool { // matchヘルパー関数
+	if len(buf) < len(s) {
+		return false;
+	}
+	for i := 0; i < len(s); i++ {
+		if buf[i] != s[i] {
+			return false;
+		}
+	}
+	return true;
+}
+
+
+// Find the position of string s in buf, starting at i.
+// Returns a value < 0 if not found.
+
+func find(buf []byte, s string, i int) int { // findヘルパー関数
+    if s == "" {
+        return i;
+    }
+L:	for ; i + len(s) <= len(buf); i++ {
+		for k := 0; k < len(s); k++ {
+			if buf[i+k] != s[k] {
+				continue L;
+			}
+		}
+		return i;
+    }
+    return -1
+}
+
+
+type Substitution map [string] func() // Substitution型の定義
+
+func (T *Template) Apply(w io.Write, prefix string, subs Substitution) *os.Error { // Applyメソッド:テンプレート置換を実行
+	i0 := 0;  // position from which to write from the template
+	i1 := 0;  // position from which to look for the next prefix
+	
+	for {
+		// look for a prefix
+		i2 := find(T.template, prefix, i1);  // position of prefix, if any
+		if i2 < 0 {
+			// no prefix found, we are done
+			break;
+		}
+
+		// we have a prefix, look for a matching key
+		i1 = i2 + len(prefix);
+		for key, action := range subs {
+			if match(T.template[i1 : len(T.template)], key) {
+				// found a match
+				i1 += len(key);  // next search starting pos				
+				len, err := w.Write(T.template[i0 : i2]);  // TODO handle errors
+				i0 = i1;  // skip placeholder
+				action(); // 対応する関数を実行
+				break;
+			}
+		}
+	}
+
+	// write the rest of the template
+	len, err := w.Write(T.template[i0 : len(T.template)]);  // TODO handle errors
+	return err;
+}

template.go が新規追加され、シンプルなテンプレート置換機能が実装されました。

usr/gri/pretty/template.html

--- a/usr/gri/pretty/template.html
+++ b/usr/gri/pretty/template.html
@@ -1,5 +1,8 @@
+<h1><!--PACKAGE--></h1>
 
+<pre>
 <!--BODY-->
+</pre>
 
 </div>  <!-- content -->
 </body>

HTMLテンプレートファイルが変更され、<!--PACKAGE--><!--BODY--> という新しいプレースホルダーが導入されました。

コアとなるコードの解説

このコミットの核心は、template パッケージが提供する汎用的なテンプレート置換メカニズムと、それが printer.go でどのように利用されているかです。

template パッケージ

template パッケージは、非常にシンプルなテキストベースのテンプレートエンジンとして機能します。その主要な機能は Apply メソッドに集約されています。

Apply メソッドは、テンプレート文字列(T.template)を走査し、特定の prefix(この場合は <!--)で始まるプレースホルダーを探します。prefix の直後には「キー」が続くと想定されており、このキーは Substitution マップに登録された文字列と一致します。

Substitutionmap[string]func() 型であり、キーと、そのキーに対応する「アクション関数」を関連付けます。Apply メソッドは、テンプレート内で一致するキーを見つけると、そのキーに対応するアクション関数を呼び出します。このアクション関数は、io.Writer に直接コンテンツを書き込むことで、動的な内容をテンプレートの該当箇所に挿入します。

この設計の利点は、テンプレートの構造(template.html)と、そのテンプレートに埋め込むコンテンツを生成するロジック(printer.goSubstitution マップ内の関数)が完全に分離される点です。これにより、HTMLのレイアウトを変更する際に printer.go のコードを修正する必要がなくなり、また、コンテンツ生成ロジックを変更する際に template.html を変更する必要がなくなります。

printer.go との連携

printer.go は、この新しい template パッケージを以下のように利用しています。

  1. テンプレートの読み込み: printer.goinit 関数で、template.html ファイルを読み込み、template.Template 型のグローバル変数 templ に格納します。
  2. HTML生成の委譲: Print 関数内で、HTML出力が必要な場合 (P.html が true の場合) に、以前手書きで行っていたHTML生成の代わりに templ.Apply を呼び出します。
  3. プレースホルダーへのコンテンツ挿入: templ.Apply に渡される Substitution マップには、"PACKAGE-->""BODY-->" という2つのキーが定義されています。
    • "PACKAGE-->" に対応する関数は、コメントアウトされていますが、将来的にはGoパッケージの名前を出力する役割を担うことが意図されています。
    • "BODY-->" に対応する関数は P.Program(prog); を呼び出します。これは printer.go の主要な機能であり、Goプログラムの抽象構文木 (AST) を解析し、整形されたコードをHTMLとして出力します。これにより、template.html<!--BODY--> の位置に、実際のGoコードの整形出力が挿入されます。

この変更により、printer.go はHTMLの構造に関する知識を持つ必要がなくなり、純粋にGoコードの整形ロジックに集中できるようになりました。これは、ソフトウェア設計における「関心の分離 (Separation of Concerns)」の原則を適用した良い例と言えます。

関連リンク

参考にした情報源リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/5e400ebf18aeee7bc26f10b25044d15b8b4379e6
  • Go言語のテンプレート機能の歴史に関するウェブ検索結果:
    • newmarch.name (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEyU03Q3qHARTrENLHq63gJ5PIp4ga11IojLIgTahBpmd3sRrD_-KlHL5B4EK588Yx-HYJUNa56zTaQTf6VvDGyPut2w4CxZa61DtrYSaA4Vev4kic7mcex6vjsHcU6o0GmQUSQDdSUnn_dvZgnFu0eqLhj6y5dNoKoIA==)
    • go.dev (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGgzKxfi5Q8F4VpyCs-fnVZ4mRz5GXH-FlDUUIv0aVkMS8fCWsS7jPCy1124UCNSWKAT1NFR8kaMe6uMukmEOsX3Yq69aEVJQqpBCsjTaTeBgdl_g5hWJogORUi)
    • go.dev (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFohIuQOpt8qb-Uq7HawHqAV_wHuzNv9F2_1MK1edEle2RAJ8jvmerz3A6ldmRln5oeBjVck6W0yAD0f_M_Xv_zPvH3mHScxiVembbziReJ9SJ75DdUJWcryWq5)