/note/tech

Go言語のTemplateで別ディレクトリに存在する同名ファイルをロードしたい

↑の質問者と同じことをしようとしていてハマったのでメモ。

Ginの Engine.LoadHTMLFiles は 内部で template.ParseFiles を使っているので同名のファイルがあると後からロードしたもので上書きされてしまう。

例えば、↓のようなファイルがある場合、 template/test1/index.html は template/test2/index.html で上書きされてしまう。

template/test1/index.html
template/test2/index.html

これは template.ParseFiles のドキュメントにも明記されている(とは言え、この挙動は直感を大きく裏切る初見殺しであり、限りなくバグに近いと思う)。

When parsing multiple files with the same name in different directories, the last one mentioned will be the one that results. For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template named "foo", while "a/foo" is unavailable.

(異なるディレクトリにある同じ名前の複数のファイルを解析する場合、最後に記載されているファイルが結果になります。たとえば、ParseFiles( "a/foo"、 "b/foo")は、 "b/foo"を "foo"という名前のテンプレートとして格納しますが、 "a/foo"は使用できません。)

https://pkg.go.dev/html/template#ParseFiles

ちなみに Engine.LoadHTMLGlob という関数も用意されており、 こちらは同名ファイルを扱えるかのようにリファレンスに書かれているが、Glob特有の問題(深い階層を作っていると再帰的にロードしてくれないなど)もあり、使い勝手がよろしくなかった。

とはいえ、これではあまり開発体験がよくないので解決策が無いものか調査したところ、↓のサンプルコードのように template.Parse を使うことで同じ名前のテンプレートを取り扱うことに成功した。

試してはいないが、 html/template だけでなく text/template でも同じだと思う。

package main

import (
    "embed"
    "html/template"
    "io/fs"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

// ↓embed.FSでファイルをバイナリに埋め込んでいる
//go:embed template/*
var templates embed.FS

func main() {

    r := gin.Default()

    // テンプレートをセット
    r.SetHTMLTemplate(loadTemplate())

    r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "template/index.html", gin.H{})
    })
    r.GET("/test1", func(c *gin.Context) {
        c.HTML(http.StatusOK, "template/test1/index.html", gin.H{})
    })
    r.GET("/test2", func(c *gin.Context) {
        c.HTML(http.StatusOK, "template/test2/index.html", gin.H{})
    })
    r.Run(":8080")
}


func loadTemplate() *template.Template {

    // 1. templateディレクトリ以下の全ファイルのパスを抽出
    fileList := []string{}
    err := fs.WalkDir(templates, "template", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            panic(err)
        }

        if d.IsDir() {
            return nil
        }

        // ファイル名をリストに保存
        fileList = append(fileList, path)
        return nil
    })
    if err != nil {
        panic(err)
    }

    // 2. 取得したファイルのリストからtemplate.Templateを作成する
    t := template.New("")
    for _, item := range fileList {
        bytes, err := fs.ReadFile(templates, item)
        if err != nil {
            panic(err)
        }
        // ↓t.New(item)とすることでファイル名がテンプレート呼び出し時のキー的なものになる模様
        t.New(item).Parse(string(bytes))
    }
    return t
}

とはいえ、今回は必要に迫られて動的なWEBサイト(JSONではなくHTMLを返却するタイプ)をGo言語で作ることになったが、動的なWEBサイトをGo言語で作成するメリットは全く無いように感じる。

動的なWEBサイトを作りたければ、PHP/Python/Rubyなどの動的型付け・インタプリタ系の言語を使った方が圧倒的に楽だし、知見も蓄積されている。