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

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

このコミットは、Go言語の標準ライブラリである html/template パッケージ内の template.go ファイルに対して行われた変更です。具体的には、ExecuteTemplate メソッドにおけるロック機構のクリーンアップと改善が目的とされています。

コミット

commit 0397b28a9016c07bd27e7b06055796bd70596146
Author: Rob Pike <r@golang.org>
Date:   Thu Dec 8 10:15:53 2011 -0800

    html/template: clean up locking for ExecuteTemplate
    
    R=mikesamuel, rogpeppe
    CC=golang-dev
    https://golang.org/cl/5448137

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

https://github.com/golang/go/commit/0397b28a9016c07bd27e7b06055796bd70596146

元コミット内容

html/template: clean up locking for ExecuteTemplate

変更の背景

html/template パッケージは、HTML出力におけるクロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を自動的にエスケープ処理することで防止するためのGo言語のテンプレートエンジンです。このパッケージは、Webアプリケーションにおいてユーザーからの入力を安全に表示するために不可欠です。

ExecuteTemplate メソッドは、指定された名前のテンプレートをデータオブジェクトに適用し、その結果を io.Writer に書き出す役割を担います。複数のゴルーチン(Goの軽量スレッド)が同時にテンプレートを実行する可能性があるため、内部状態の一貫性を保つために適切な同期メカニズム、特にミューテックス(sync.Mutex)によるロックが必要となります。

このコミットの背景には、ExecuteTemplate メソッド内でテンプレートのルックアップとエスケープ処理を行う際のロックの取得と解放のタイミングに関する改善の必要性がありました。以前の実装では、ロックの解放が関数の途中で行われており、エラーパスや早期リターンが発生した場合にロックが適切に解放されない可能性や、コードの可読性・保守性の低下を招く可能性がありました。この変更は、defer ステートメントを活用することで、ロックの解放を確実かつ簡潔に行うことを目的としています。

前提知識の解説

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

html/template パッケージは、Go言語でHTMLを生成する際に、自動的にコンテキストに応じたエスケープ処理を行うことで、XSS攻撃などのWeb脆弱性からアプリケーションを保護します。これは、単なる文字列置換ではなく、HTML、JavaScript、CSSなどの各コンテキストを認識し、適切なエスケープを適用する「コンテキストアウェアなエスケープ」を提供します。

Go言語の sync.Mutex

sync.Mutex は、Go言語における相互排他ロック(ミューテックス)を実装するための型です。共有リソースへのアクセスを複数のゴルーチンから同時に行われることを防ぎ、データ競合(data race)を回避するために使用されます。

  • mu.Lock(): ミューテックスをロックします。既にロックされている場合、現在のゴルーチンはロックが解放されるまでブロックされます。
  • mu.Unlock(): ミューテックスをアンロックします。

Go言語の defer ステートメント

defer ステートメントは、そのステートメントを含む関数がリターンする直前に、指定された関数呼び出しを実行するようにスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックのアンロックなど)を確実に行うために非常に便利です。defer を使用することで、関数の複数の終了点(正常終了、エラーによる早期リターンなど)において、常にリソースがクリーンアップされることを保証できます。

mu.Lock()
defer mu.Unlock() // 関数が終了する際にmu.Unlock()が必ず実行される
// クリティカルセクションのコード

テンプレートの「エスケープ」と「ルックアップ」

html/template において、テンプレートは実行される前に「エスケープ」される必要があります。これは、テンプレート内のプレースホルダーに挿入されるデータが、そのコンテキスト(例: HTML属性、JavaScriptコード、URLなど)に応じて適切にサニタイズされることを意味します。 「ルックアップ」とは、名前が与えられたテンプレートコレクションの中から、特定の名前を持つテンプレートを見つけ出すプロセスです。html/template は、複数のテンプレートを名前空間で管理し、Lookup メソッドなどを使って名前で参照します。

技術的詳細

このコミットの主要な変更点は、ExecuteTemplate メソッドからテンプレートのルックアップとエスケープ処理を lookupAndEscapeTemplate という新しいプライベートメソッドに分離し、ロックの管理を改善した点にあります。

元の ExecuteTemplate メソッドでは、t.nameSpace.mu.Lock() でロックを取得し、テンプレートのルックアップとエスケープ処理を行った後、t.nameSpace.mu.Unlock() でロックを解放していました。この方式では、エスケープ処理中にエラーが発生した場合、Unlock() が実行されずにロックが保持されたままになる可能性がありました。

新しい lookupAndEscapeTemplate メソッドでは、ロックを取得した直後に defer t.nameSpace.mu.Unlock() を配置しています。これにより、lookupAndEscapeTemplate 関数がどのような経路で終了しても(正常終了、エラーによる早期リターン、パニックなど)、必ずミューテックスが解放されることが保証されます。これは、リソース管理における defer の典型的な使用例であり、コードの堅牢性と信頼性を大幅に向上させます。

また、ExecuteTemplate メソッドは、lookupAndEscapeTemplate から返されたエスケープ済みのテンプレート (tmpl) を直接使用して tmpl.text.Execute(wr, data) を呼び出すようになりました。これにより、ExecuteTemplate 自体はテンプレートのルックアップとエスケープに関する内部的なロックの詳細から解放され、よりシンプルでクリーンな役割に集中できるようになります。

html/template パッケージの Template オブジェクトは、一度パースされた後は並行して安全に利用できるように設計されています。しかし、テンプレートオブジェクト自体に新しいテンプレートを追加したり、既存のテンプレートを再パースしたりするような変更操作は、通常、スレッドセーフではありません。このコミットで改善されたロック機構は、テンプレートのルックアップとエスケープという内部的な操作におけるデータ競合を防ぐためのものです。

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

--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -49,20 +49,28 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
 
 // ExecuteTemplate applies the template associated with t that has the given
 // name to the specified data object and writes the output to wr.
-func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) (err error) {
+func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
+\ttmpl, err := t.lookupAndEscapeTemplate(wr, name)
+\tif err != nil {\n+\t\treturn err
+\t}
+\treturn tmpl.text.Execute(wr, data)
+}
+\n+// lookupAndEscapeTemplate guarantees that the template with the given name
+// is escaped, or returns an error if it cannot be. It returns the named
+// template.
+func (t *Template) lookupAndEscapeTemplate(wr io.Writer, name string) (tmpl *Template, err error) {
 \tt.nameSpace.mu.Lock()
-\ttmpl := t.set[name]\n+\tdefer t.nameSpace.mu.Unlock()
+\ttmpl = t.set[name]
 \tif (tmpl == nil) != (t.text.Lookup(name) == nil) {\n \t\tpanic("html/template internal error: template escaping out of sync")
 \t}\n \tif tmpl != nil && !tmpl.escaped {\n \t\terr = escapeTemplates(tmpl, name)\n \t}\n-\tt.nameSpace.mu.Unlock()\n-\tif err != nil {\n-\t\treturn\n-\t}\n-\treturn t.text.ExecuteTemplate(wr, name, data)\n+\treturn tmpl, err
 }\n 
 // Parse parses a string into a template. Nested template definitions

コアとなるコードの解説

ExecuteTemplate メソッドの変更

  • 変更前:

    func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) (err error) {
        t.nameSpace.mu.Lock()
        tmpl := t.set[name]
        // ... テンプレートのルックアップとエスケープ処理 ...
        t.nameSpace.mu.Unlock() // ロックの解放
        if err != nil {
            return
        }
        return t.text.ExecuteTemplate(wr, name, data)
    }
    

    この実装では、t.nameSpace.mu.Unlock() が関数の途中にあり、escapeTemplates でエラーが発生した場合や、その他の理由で早期リターンが発生した場合に、ロックが解放されない可能性がありました。

  • 変更後:

    func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
        tmpl, err := t.lookupAndEscapeTemplate(wr, name) // 新しいヘルパー関数を呼び出し
        if err != nil {
            return err
        }
        return tmpl.text.Execute(wr, data) // エスケープ済みのテンプレートを実行
    }
    

    ExecuteTemplate は、テンプレートのルックアップとエスケープ処理を lookupAndEscapeTemplate に委譲し、その結果を受け取ってからテンプレートを実行するようになりました。これにより、ExecuteTemplate 自体はロック管理の複雑さから解放され、よりシンプルになりました。

lookupAndEscapeTemplate メソッドの追加

  • 新しく追加されたプライベートメソッドです。
    func (t *Template) lookupAndEscapeTemplate(wr io.Writer, name string) (tmpl *Template, err error) {
        t.nameSpace.mu.Lock()
        defer t.nameSpace.mu.Unlock() // ここが最も重要な変更点
        tmpl = t.set[name]
        if (tmpl == nil) != (t.text.Lookup(name) == nil) {
            panic("html/template internal error: template escaping out of sync")
        }
        if tmpl != nil && !tmpl.escaped {
            err = escapeTemplates(tmpl, name)
        }
        return tmpl, err
    }
    
    • t.nameSpace.mu.Lock(): テンプレートの名前空間に対するミューテックスをロックします。
    • defer t.nameSpace.mu.Unlock(): この defer ステートメントにより、lookupAndEscapeTemplate 関数が終了する際に、必ずミューテックスが解放されることが保証されます。これにより、エラーが発生した場合でもロックが適切に解放され、デッドロックのリスクが低減されます。
    • tmpl = t.set[name]: 指定された名前のテンプレートをセットからルックアップします。
    • if (tmpl == nil) != (t.text.Lookup(name) == nil): 内部的な整合性チェックです。html/template の内部状態が同期していることを確認します。
    • if tmpl != nil && !tmpl.escaped: テンプレートがまだエスケープされていない場合、escapeTemplates 関数を呼び出してエスケープ処理を行います。
    • return tmpl, err: ルックアップおよびエスケープされたテンプレートと、発生したエラーを返します。

この変更により、ロックの取得と解放のロジックが defer を使用して一箇所に集約され、コードの信頼性、可読性、保守性が向上しました。

関連リンク

参考にした情報源リンク