JavaScriptを有効にしてください

【Go】template.ExecuteTemplate中にエラーが起きて中途半端にレンダリングされてしまう問題

 ·  ☕ 2 分で読めます

まずは問題のコードから

package main

import (
	"html/template"
	"net/http"
)

func exampleHandler(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles(
		"example.html",
	))

	err := t.ExecuteTemplate(w, "example.html", nil)
	if err != nil {
		http.NotFound(w, r)
		return
	}
}

func main() {
	http.HandleFunc("/", exampleHandler)
	log.Fatalln(http.ListenAndServe(":8080", nil))
}

何が起きる?

上のコードは http.ListenAndServeでwebサーバーを立ち上げて、/にアクセスが来たらexample.htmlを返してあげるというシンプルなものです。

exampleHandlerの中でExecuteTemplateを呼んで、ResponseWriterに書き込んでいますね。
この時、レンダリング中にエラーが起きた場合は、その後のifでキャッチしてnot foundのページを改めて書き込んであげます。

つまり、templateのレンダリングが成功したらそのまま、失敗したらエラーページを表示としたいわけです。

…しかし

実際には、templateのレンダリングの途中でエラーが起こった場合、途中まで書き込んだものはそのまま放っておいて、その途中からエラーページが書き込まれることになります。

つまり結果として、正常なページが途中まで表示されていて、途中からエラーメッセージが表示されている、みたいな中途半端で汚い画面になってしまうことがあります。

解決策

ExecuteTemplateを呼ぶとき、いきなりResponseWriterに書き込むのではなく一旦バッファに書き込み、成功したらバッファを改めてResponseWriter に書き込む
とすれば良い。

テンプレートのレンダリングに失敗したら、エラーページをResponseWriter に書き込めば良い。

package main

import (
	"bytes"
	"html/template"
	"log"
	"net/http"
)

func exampleHandler(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles(
		"example.html",
	))

	// バッファを作る
	buf := new(bytes.Buffer)

	// バッファに書き込む
	err := t.ExecuteTemplate(buf, "example.html", nil)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	// バッファをResponseWriterに書き込む
	buf.WriteTo(w)
}

func main() {
	http.HandleFunc("/", exampleHandler)
	log.Fatalln(http.ListenAndServe(":8887", nil))
}

もう少し工夫して、レンダリングのたびにバッファを作らず、バッファプールを確保しておいて使いまわす手もある。

package main

import (
	"html/template"
	"log"
	"net/http"

	"github.com/oxtoacart/bpool"
)

// グローバル変数(パッケージローカル?)としてバッファプールを確保
var bufpool *bpool.BufferPool = bpool.NewBufferPool(64)

func exampleHandler(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles(
		"example.html",
	))

	buf := bufpool.Get() // プールからバッファを取得
	defer bufpool.Put(buf) // funcの最後にプールに返す

	err := t.ExecuteTemplate(buf, "example.html", nil)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	buf.WriteTo(w)
}

func main() {
	http.HandleFunc("/", exampleHandler)
	log.Fatalln(http.ListenAndServe(":8887", nil))
}

これでOK

参考

Dealing with Go Template errors at runtime | by Lee Provoost | Medium

共有