Help us understand the problem. What is going on with this article?

Goのテンプレートをちゃんと使ってみる

はじめに

Goのテンプレートのpackageをちゃんと使ってみたいという気持ちになったので調べました。

参考

text/templateパッケージのドキュメント
テンプレートの文法についてはこっちに書いてあります。

html/templateパッケージのドキュメント
packageのインタフェースはtext/templateと同じですが、セキュリティ上の問題を引き起こさないためには出力がHTMLの場合にはこちらを使うべきと書いてあります。

Effective Go - The Go Programming Language # A web server
formに文字列を入力すると、GoogleのAPIを使ってQRコードの画像を生成して表示するサーバのサンプルコードがあります。html/templateも登場します。

Writing Web Applications - The Go Programming Language
簡易的なWikiを作ってWebアプリの作り方を学べるチュートリアルです。重複を排除するようにリファクタしつつ進んでいくので教材としていい感じです。これも同じく、html/templateも登場します。

テンプレートの文法

文字列の中に{{Action}}の形式でデータや制御構造を埋め込める。

Action

データの評価または制御構造を表す。

  • コメント
    • {{/* a comment */}}の形式で出力されないコメントを書ける
  • pipelineの値そのもの{{pipeline}}
  • if {{if pipeline}} T1 {{end}}
    • pipelineの値がempty(false,0,nilのポインタかinterface,len(x) == 0の配列、スライス、map、文字列の場合)でなければT1、emptyなら何も出力されない
  • if-else {{if pipeline}} T1 {{else}} T0 {{end}}
    • pipelineの値がemptyでなければT1、emptyならT0
  • if-else-if {{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
    • elseに別のifを入れこむことができる
    • これと同じ結果になる{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
  • with {{with pipeline}} T1 {{end}}
    • ifと同じだが、pipelineの値がemptyでない場合、dot(テンプレートに渡したデータ中の現在の参照位置を示す)がpipelineの値になる
  • with-else {{with pipeline}} T1 {{else}} T0 {{end}}
    • pipelineの値がemptyでなければwithと同じ、emptyならdotは変わらない
  • range {{range pipeline}} T1 {{end}}
    • pipelineの値が配列、スライス、map、channelのいずれかである必要がある
    • pipelineの値がempty(len(x) == 0)なら何も出力されない
    • emptyでない場合、要素の数だけループする
    • ループ中、dotは現在の要素を示す
  • range-else {{range pipeline}} T1 {{else}} T0 {{end}}
    • rangeと同じだが、pipelineの値がemptyの場合T0が実行される

Argument

単なる値を表す。ポインタでも自動的に値を見にいってくれる

  • 表記:値
  • Goの定数:untyped constantになる
  • nil:untyped nil
  • .:そのときのdotの値
  • $alphanumの形式の変数名$も有効
  • 構造体のフィールド名:フィールドの値
    • .Field の形式
    • .Field1.Field2ネストしていても参照できる
    • $x.Field1.Field2で変数のフィールドにもアクセスできる
  • mapのキー名:キーの値
    • .Key の形式
    • $x.key変数に対しても同じことができる
  • 引数を持たないメソッド名:メソッドの呼び出し結果
    • この形式で使えるメソッドは返り値を2つまでしか持てない
    • 2つの場合は、2つ目はerrorでないといけない
    • nilではないerrorが返った場合は、テンプレートの実行は止まりExecute()からそのerrorが返却される
  • 引数を持たない関数名:関数の呼び出し結果
    • 返り値についての振る舞いはメソッドの場合と同じ

Pipeline

1つ以上のcommand(単なる値あるいは関数かメソッドの呼び出し)を|で繋いだもの

関数

定義済みの関数

and

引数のうち最初に見つかったemptyの引数を返す。emptyのものが1つもなかった場合は最後の引数を返す。

or

引数のうち最初に見つかったemptyではない引数を返す。すべてemptyだった場合は最後の引数を返す。

not

引数がemptyならtrue、そうでなければfalseを返す。

call

最初の引数(関数じゃないといけない)に残りの引数を渡して実行した結果を返す。

https://play.golang.org/p/B7oAqxsBKaT

package main

import (
    "math"
    "os"
    "text/template"
)

type Point struct {
    X float64
    Y float64
}

func Distance(a, b Point) float64 {
    return math.Sqrt((a.X-b.X)*(a.X-b.X) + (a.Y-b.Y)*(a.Y-b.Y))
}

const tmplStr = `
p1: {{printf "%#v" .p1}}
p2: {{printf "%#v" .p2}}
{{/* {{.distance .p1 .p2}} はダメ */}}
distance: {{call .distance .p1 .p2}}
`

var tmpl = template.Must(template.New("call").Parse(tmplStr))

func main() {
    data := map[string]interface{}{
        "p1":       Point{X: 3.0, Y: 0.0},
        "p2":       Point{X: 0.0, Y: 4.0},
        "distance": Distance,
    }
    if err := tmpl.Execute(os.Stdout, data); err != nil {
        panic(err)
    }
}

p1: main.Point{X:3, Y:0}
p2: main.Point{X:0, Y:4}

distance: 5

print, printf, println

それぞれfmt.Sprint, fmt.Sprintf, fmt.Sprintlnの働きをする。

html, js, urlquery

引数をそれぞれHTML,JavaScript,クエリストリングとしてエスケープする。

https://play.golang.org/p/JU9sPOW42NT

package main

import (
    "os"
    "text/template"
)

const tmplStr = `
and: {{and 0 0}} {{and 1 0}} {{and 0 1}} {{and 1 1}}
or: {{or 0 0}} {{or 1 0}} {{or 0 1}} {{or 1 1}}

not: {{not "ok"}} {{0 | not}}

html: {{html "<a href=\"https://golang.org/\">Go</a>"}}
js: {{js "<script>alert('XSS');</script>"}}
urlquery: {{urlquery "http://example.com/path?key=val"}}
slice: {{slice "golang" 1 4}}

eq: {{eq "🍣" "🍣"}}    {{eq "🍣" "🍺"}}    {{eq "🍺" "🍣"}}
ne: {{ne "🍣" "🍣"}}    {{ne "🍣" "🍺"}}    {{ne "🍺" "🍣"}}
lt: {{lt "🍣" "🍣"}}    {{lt "🍣" "🍺"}}    {{lt "🍺" "🍣"}}
le: {{le "🍣" "🍣"}}    {{le "🍣" "🍺"}}    {{le "🍺" "🍣"}}
gt: {{gt "🍣" "🍣"}}    {{gt "🍣" "🍺"}}    {{gt "🍺" "🍣"}}
ge: {{ge "🍣" "🍣"}}    {{ge "🍣" "🍺"}}    {{ge "🍺" "🍣"}}
`

var tmpl = template.Must(template.New("functions").Parse(tmplStr))

func main() {
    if err := tmpl.Execute(os.Stdout, nil); err != nil {
        panic(err)
    }
}

and: 0 0 0 1
or: 0 1 1 1

not: false true

html: &lt;a href=&#34;https://golang.org/&#34;&gt;Go&lt;/a&gt;
js: \x3Cscript\x3Ealert(\'XSS\');\x3C/script\x3E
urlquery: http%3A%2F%2Fexample.com%2Fpath%3Fkey%3Dval
slice: ola

eq: true    false   false
ne: false   true    true
lt: false   true    false
le: true    true    false
gt: false   false   true
ge: true    false   true

自分で定義した関数を使う

(*Template) Funcs()template.FuncMap(map[string]interface{})を渡すと自分で定義した関数を使える。Funcs()Parse()より前に呼ぶ必要がある。

https://play.golang.org/p/7jJOVbGBqPk

package main

import (
    "math"
    "os"
    "text/template"
)

const tmplStr = `
multiply: {{multiply 2 3}}
average: {{average 2 3 5}}
round: {{round 3.14}}
average|round: {{average 2 3 5 | round}}
reverse: {{reverse "reverse"}}
sushi|beer: {{"" | sushi | beer | sushi | beer}}
`

var (
    funcMap = map[string]interface{}{
        "multiply": func(a, b int) int {
            return a * b
        },
        "average": func(nums ...int) float64 {
            sum := 0
            for _, n := range nums {
                sum += n
            }
            return float64(sum) / float64(len(nums))
        },
        "round": func(num float64) int {
            return int(math.Round(num))
        },
        "reverse": func(s string) string {
            r := []rune(s)
            for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
                r[i], r[j] = r[j], r[i]
            }
            return string(r)
        },
        "sushi": func(s string) string {
            return s + "🍣"
        },
        "beer": func(s string) string {
            return s + "🍺"
        },
    }
    tmpl = template.Must(template.New("funcmap").Funcs(funcMap).Parse(tmplStr))
)

func main() {
    if err := tmpl.Execute(os.Stdout, nil); err != nil {
        panic(err)
    }
}

multiply: 6
average: 3.3333333333333335
round: 3
average|round: 3
reverse: esrever
sushi|beer: 🍣🍺🍣🍺
rock619
「ご年齢に応じたご経験、スキルが不足されているようです」
https://rock619.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした