まずは問題のコードから
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