[インデックス 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/template
や html/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
ヘルパー関数
テンプレート置換のロジックをサポートするために、match
と find
という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.Stdout
やbytes.Buffer
)。prefix string
: テンプレート内の置換対象の開始を示すプレフィックス文字列(例:<!--
)。subs Substitution
: 置換ルールを定義するマップ。
Apply
メソッドの動作は以下の通りです。
- テンプレート全体を走査し、指定された
prefix
を探します。 prefix
が見つかった場合、その直後からsubs
マップのキーと一致する文字列を探します。- 一致するキーが見つかった場合、そのキーに対応する関数
action()
を実行します。このaction
関数は、io.Writer
に直接書き込むことで、動的なコンテンツを生成します。 prefix
からキーまでの部分(プレースホルダー自体)はスキップされ、次の検索は置換された部分の直後から開始されます。prefix
が見つからなくなるまでこのプロセスを繰り返します。- 最後に、テンプレートの残りの部分をすべて出力します。
このメカニズムにより、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.go
のinit
関数内で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
マップに登録された文字列と一致します。
Substitution
は map[string]func()
型であり、キーと、そのキーに対応する「アクション関数」を関連付けます。Apply
メソッドは、テンプレート内で一致するキーを見つけると、そのキーに対応するアクション関数を呼び出します。このアクション関数は、io.Writer
に直接コンテンツを書き込むことで、動的な内容をテンプレートの該当箇所に挿入します。
この設計の利点は、テンプレートの構造(template.html
)と、そのテンプレートに埋め込むコンテンツを生成するロジック(printer.go
の Substitution
マップ内の関数)が完全に分離される点です。これにより、HTMLのレイアウトを変更する際に printer.go
のコードを修正する必要がなくなり、また、コンテンツ生成ロジックを変更する際に template.html
を変更する必要がなくなります。
printer.go
との連携
printer.go
は、この新しい template
パッケージを以下のように利用しています。
- テンプレートの読み込み:
printer.go
のinit
関数で、template.html
ファイルを読み込み、template.Template
型のグローバル変数templ
に格納します。 - HTML生成の委譲:
Print
関数内で、HTML出力が必要な場合 (P.html
が true の場合) に、以前手書きで行っていたHTML生成の代わりにtempl.Apply
を呼び出します。 - プレースホルダーへのコンテンツ挿入:
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)」の原則を適用した良い例と言えます。
関連リンク
- Go言語の現在の標準テンプレートパッケージ:
text/template
: https://pkg.go.dev/text/templatehtml/template
: https://pkg.go.dev/html/template
参考にした情報源リンク
- 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)