かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Go プログラミング実践入門を読みながら Go での Web App「テンプレート」

Go 言語って標準ライブラリにテンプレートまであるのか。便利。

ということで使ってみましょう。

使い方は簡単。template.Must(template.ParseFiles("templateFilePath1", "templateFilePath2", ...) みたいにしてテンプレートをパースする。パースしたら ExecuteTemplate メソッドで出力先とテンプレート名とテンプレートに渡す値を指定して完成。

package main

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

type pageData struct {
    Title   string
    Message string
}

func main() {
    templateFiles := []string{"templates/index.html", "templates/test.html"}
    templates := template.Must(template.ParseFiles(templateFiles...))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        templateName := "index"
        templateNames, ok := req.URL.Query()["template"]
        if ok {
            templateName = templateNames[0]
        }

        templates.ExecuteTemplate(w, templateName, pageData{Title: "Template sample title", Message: "Template sample message"})
    })

    http.ListenAndServe("localhost:8080", mux)
}

テンプレートは以下のような感じになります。

まずは templates/index.html

{{ define "index" }}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Hello</title>
    </head>
    <body>
        <h1>{{ .Title }}</h1>
        <p>{{ .Message }}</p>
    </body>
</html>
{{ end }}

templates/test.html はこんな感じ。

{{ define "test" }}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Test</title>
    </head>
    <body style="background-color: yellow">
        <h1>{{ .Title }}</h1>
        <p>{{ .Message }}</p>
    </body>
</html>
{{ end }}

基本的には {{ }} でくくった中に何か書く。先頭は define でレイアウト名(ExecuteTemplate で指定する名前)を指定して最後に end でおしまい。

{{ .プロパティ名 }} とかで ExecuteTemplate で渡されたデータにアクセスできる感じっぽい。

動かしてみるとちゃんと動いた。

f:id:okazuki:20181122165325p:plain

今回のプログラムは URL のパラメータでテンプレート名指定するからこんな感じでも動く。

f:id:okazuki:20181122165403p:plain

{{ range .xxx }} でループも行ける。テンプレートをいじって

{{ define "index" }}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Hello</title>
    </head>
    <body>
        <h1>{{ .Title }}</h1>
        <p>{{ .Message }}</p>
        <hr/>
        <ul>
            {{ range .Records }}
            <li>{{ .Name }}</li>
            {{ end }}
        </ul>
    </body>
</html>
{{ end }}

main.go もそれにあわせて変更。

package main

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

type record struct {
    Name string
}

type pageData struct {
    Title   string
    Message string
    Records []record
}

func main() {
    templateFiles := []string{"templates/index.html", "templates/test.html"}
    templates := template.Must(template.ParseFiles(templateFiles...))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        templateName := "index"
        templateNames, ok := req.URL.Query()["template"]
        if ok {
            templateName = templateNames[0]
        }

        templates.ExecuteTemplate(w, templateName, pageData{
            Title:   "Template sample title",
            Message: "Template sample message",
            Records: []record{
                record{Name: "aaaaaaaa"},
                record{Name: "bbbbbbbb"},
                record{Name: "cccccccc"},
                record{Name: "dddddddd"},
                record{Name: "eeeeeeee"},
            },
        })
    })

    http.ListenAndServe("localhost:8080", mux)
}

いい感じに動く。

f:id:okazuki:20181122171033p:plain

さらに if やカスタムの関数とかも定義出来るみたい。今回は偶数番目のデータと奇数番目のデータで色を変えたかったので、mod という関数を追加したうえでパースして実行してみました。

package main

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

type record struct {
    Name string
}

type pageData struct {
    Title   string
    Message string
    Records []record
}

func main() {
    templateFiles := []string{"templates/index.html", "templates/test.html"}
    templates := template.Must(template.New("").Funcs(template.FuncMap{
        "mod": func(x int, y int) int {
            return x % y
        },
    }).ParseFiles(templateFiles...))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        templateName := "index"
        templateNames, ok := req.URL.Query()["template"]
        if ok {
            templateName = templateNames[0]
        }

        templates.ExecuteTemplate(w, templateName, pageData{
            Title:   "Template sample title",
            Message: "Template sample message",
            Records: []record{
                record{Name: "aaaaaaaa"},
                record{Name: "bbbbbbbb"},
                record{Name: "cccccccc"},
                record{Name: "dddddddd"},
                record{Name: "eeeeeeee"},
            },
        })
    })

    http.ListenAndServe("localhost:8080", mux)
}

テンプレートはこんな感じになりました。

{{ define "index" }}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Hello</title>
    </head>
    <body>
        <h1>{{ .Title }}</h1>
        <p>{{ .Message }}</p>
        <hr/>
        <ul>
            {{ range $index, $record := .Records }}
            {{ $isEvenRow := eq (mod $index 2) 0 }}
            <li style="color: {{ if $isEvenRow}} red {{ else }} black {{ end }}">{{ $index }}{{ $record.Name }}</li>
            {{ end }}
        </ul>
    </body>
</html>
{{ end }}

if や range のインデックスの取得方法と関数呼び出しと盛りだくさん。実行するとこんな感じです。

f:id:okazuki:20181122173525p:plain

あとはサニタイズとかもあるみたいですね。普通に Go 側とこういう風に書くと…

package main

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

type record struct {
    Name string
}

type pageData struct {
    Title   string
    Message string
    Records []record
}

func main() {
    templateFiles := []string{"templates/index.html", "templates/test.html"}
    templates := template.Must(template.New("").Funcs(template.FuncMap{
        "mod": func(x int, y int) int {
            return x % y
        },
    }).ParseFiles(templateFiles...))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        templateName := "index"
        templateNames, ok := req.URL.Query()["template"]
        if ok {
            templateName = templateNames[0]
        }

        templates.ExecuteTemplate(w, templateName, pageData{
            Title:   "Template sample title",
            Message: "<script type='text/javascript`>alert('Template sample message')</script>", // !?
            Records: []record{
                record{Name: "aaaaaaaa"},
                record{Name: "bbbbbbbb"},
                record{Name: "cccccccc"},
                record{Name: "dddddddd"},
                record{Name: "eeeeeeee"},
            },
        })
    })

    http.ListenAndServe("localhost:8080", mux)
}

素晴らしい。

f:id:okazuki:20181122173712p:plain

あえてエスケープしたくないときは template.HTML を使う。まぁレアケースだけどマークダウンエディターみたいなものを作りたいときとかは必要。

こんな感じで

package main

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

type record struct {
    Name string
}

type pageData struct {
    Title   string
    Message template.HTML
    Records []record
}

func main() {
    templateFiles := []string{"templates/index.html", "templates/test.html"}
    templates := template.Must(template.New("").Funcs(template.FuncMap{
        "mod": func(x int, y int) int {
            return x % y
        },
    }).ParseFiles(templateFiles...))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        templateName := "index"
        templateNames, ok := req.URL.Query()["template"]
        if ok {
            templateName = templateNames[0]
        }

        templates.ExecuteTemplate(w, templateName, pageData{
            Title:   "Template sample title",
            Message: template.HTML("<script type='text/javascript'>alert('Template sample message')</script>"),
            Records: []record{
                record{Name: "aaaaaaaa"},
                record{Name: "bbbbbbbb"},
                record{Name: "cccccccc"},
                record{Name: "dddddddd"},
                record{Name: "eeeeeeee"},
            },
        })
    })

    http.ListenAndServe("localhost:8080", mux)
}

実行するとこうなる。ばっちり。

f:id:okazuki:20181122174547p:plain