Build Web Application With Golang を試してやってみる 27

はじめに

前回の記事の続きです。
前回の記事はこちら

テキスト処理

前回同様テキスト処理についてやっていきます。
テンプレートの処理をやっていきましょう。

テンプレートとは何か

テンプレートとはどういうものかですね、
Java なら .jsp ファイルだったり、 PHPならCakePHPの.ctp だったり、Laravelのbladeファイルだったりで使用されている、
変数を埋め込めるような機構のことですね。

おそらくあなたはMVCのデザインパターンについて聞いたことがあると思います。
Modelはデータを処理を、Viewは表示結果を、Controllerはユーザのリクエストの制御を行います。
Viewレイヤーの処理では、多くの動的な言語ではどれも静的なHTMLの中に動的言語が生成したデータを挿入します。例えばJSPでは<%=….=%>を挿入することで、PHPではを挿入することで実現します。

Goのテンプレートの使用

以前のフォームの処理などでも使っていましたね、.gtpl ってやつです。

Go言語では、templateパッケージを使用してテンプレートの処理を行います。
Parse、ParseFile、Executeといった方法を使ってファイルや文字列からテンプレートをロードします。その後植えの図で示したテンプレートのmerge操作のようなものを実行します。下の例をご覧ください

func handler(w http.ResponseWriter, r *http.Request) {
    t := template.New("some template") //テンプレートを新規に作成する。
    t, _ = t.ParseFiles("tmpl/welcome.html", nil)  //テンプレートファイルを解析
    user := GetUser() //現在のユーザの情報を取得する。
    t.Execute(w, user)  //テンプレートのmerger操作を実行する。
}

なるほど、

  1. テンプレートファイルの作成
  2. テンプレートファイルの解析
  3. 変数の埋め込み(merger操作)

のような順序なわけですね。

どのようにしてテンプレートの中にデータを挿入するのか?

これが一番知りたいところですね。

上においてどのように解析とテンプレートの適用するかデモを行いました。以降ではさらに詳しくどのようにデータを適用していくのか理解していきましょう。テンプレートはすべてGoのオブジェクト上で適用されます。Goオブジェクトのフィールドはどのようにしてテンプレートの中に挿入されるのでしょうか?

フィールドの操作

Go言語のテンプレートは{{}}を通して適用時に置換する必要のあるフィールドを含めます。{{.}}は現在のオブジェクトを示しています。これはJavaやC++の中のthisに似たものです。

なるほど、今まで使ってきた {{}} が変数を呼ぶためのもの構文だったわけです。

もし現在のオブジェクトのフィールドにアクセスしたい場合は{{.FieldName}}というようにします。ただし注意してください:このフィールドは必ずエクスポートされたものとなります(頭文字が大文字になります)、さもなければ適用時にエラーを発生させます。

ほうほう。
例を実行してみます。環境はいつものです。

package main

import (
    "html/template"
    "os"
)

type Person struct {
    UserName string
}

func main() {
    t := template.New("fieldname example")
    t, _ = t.Parse("hello {{.UserName}}!")
    p := Person{UserName: "Astaxie"}
    t.Execute(os.Stdout, p)
}

出力結果

bash-5.0# go run template.go 
hello Astaxie!bash-5.0# 

できていますね、 .UserName でちゃんと出力されています。

上のコードでは正しくhello Astaxieと出力されます。しかしもしコードに修正を加え、テンプレートにエクスポートされていないフィールドを含むと、エラーを発生させます。 上のコードはエラーを発生させます。なぜならエクスポートされていないフィールドをコールしたためです。しかしもし存在しないフィールドをコールした場合はエラーを発生させず、空文字列を出力します。

やってみましたが、空文字が出力されているようですね。
先ほどと同じ出力に見えます。

bash-5.0# go run template.go 
hello Astaxie! bash-5.0# 

ネストしたフィールドの内容の出力

上の例でどのようにひとつのオブジェクトのフィールドを出力するか示しました。もしフィールドの中にまたオブジェクトがある場合は、どのようにループしてこれらの内容を出力するのでしょうか?ここでは{{with …}}…{{end}}と{{range …}}{{end}}によってデータを出力することができます。

オブジェクトの中のオブジェクトだったり、配列とかはどうすればいいの?という感じですね。
ここで解決できるようです。

{{range}} はGo言語の中のrangeに似ています。ループしてデータを操作します {{with}}操作は現在のオブジェクトの値を指します。コンテキストの概念に似ています。

ほうほう。
早速サンプルを試してみます。

package main

import (
    "html/template"
    "os"
)

type Friend struct {
    Fname string
}

type Person struct {
    UserName string
    Emails   []string
    Friends  []*Friend
}

func main() {
    f1 := Friend{Fname: "minux.ma"}
    f2 := Friend{Fname: "xushiwei"}
    t := template.New("fieldname example")
    t, _ = t.Parse(`hello {{.UserName}}!
            {{range .Emails}}
                an email {{.}}
            {{end}}
            {{with .Friends}}
            {{range .}}
                my friend name is {{.Fname}}
            {{end}}
            {{end}}
            `)
    p := Person{UserName: "Astaxie",
        Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
        Friends: []*Friend{&f1, &f2}}
    t.Execute(os.Stdout, p)
}

出力結果です。

bash-5.0# go run template2.go 
hello Astaxie!

                an email astaxie@beego.me 

                an email astaxie@gmail.com



                my friend name is minux.ma

                my friend name is xushiwei


            bash-5.0# 

ちゃんとループして出力されていますね。

条件分岐

Goテンプレートにおいてもし条件判断が必要となった場合は、Go言語のif-else文に似た方法を使用することで処理することができます。もしpipelineが空であれば、ifはデフォルトでfalseだと考えます。

条件で表示/非表示で if-elseを使用したいときに必須ですので、このあたりも欲しいですね。
早速サンプルを試してみます。

package main

import (
    "os"
    "text/template"
)

func main() {
    tEmpty := template.New("template test")
    tEmpty = template.Must(tEmpty.Parse("空の pipeline if demo: {{if ``}} 出力されません。 {{end}}\n"))
    tEmpty.Execute(os.Stdout, nil)

    tWithValue := template.New("template test")
    tWithValue = template.Must(tWithValue.Parse("空ではない pipeline if demo: {{if `anything`}} コンテンツがあります。出力します。 {{end}}\n"))
    tWithValue.Execute(os.Stdout, nil)

    tIfElse := template.New("template test")
    tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
    tIfElse.Execute(os.Stdout, nil)
}

出力結果

bash-5.0# go run template3.go 
空の pipeline if demo: 
空ではない pipeline if demo:  コンテンツがあります。出力します。
if-else demo:  if部分
bash-5.0# 

template.Must と出てきましたが、ラップ関数のようですね。
tEmpty = template.Must~~tEmpty, _ = template.Parse~~ は同じです(errorを受け取るか受け取らないかですね)。

Must is a helper that wraps a call to a function returning (*Template, error) and panics if the error is non-nil.
https://golang.org/pkg/text/template/#Must

pipelines

Unixユーザはpipeについてよくご存知でしょう。ls | grep “beego"のような文法はよく使われるものですよね。カレントディレクトリ以下のファイルをフィルターし、“beego"を含むデータを表示します カレントディレクトリ以下のファイルをフィルターし、“beego"を含むデータを表示します。前の出力を後の入力にするという意味があります。最後に必要なデータを表示します。Go言語のテンプレートの最大のアドバンテージはデータのpipeをサポートしていることです。Go言語の中でいかなる{{}}の中はすべてpipelinesデータです。

確かに、パイプラインをテンプレートでサポートしているのは強いですね。(私は未熟なので、どこのテンプレートでも見たことないです。)

例えば上で出力したemailにもしXSSインジェクションを引き起こす可能性があるとすると、どのように変換するのでしょうか? {{. | html}} emailが出力される場所では上のような方法で出力をすべてhtmlの実体に変換することができます。

上のコードで、サニタイズができるわけですね。(phpのhtmlspecialcharsの関数みたいな感じですね。)

テンプレート変数

ときどき、テンプレートを使っていてローカル変数を定義したい場合があります。操作の中でローカル変数を宣言することができます。

<?php $localVariables = 'local' ?> みたいに、テンプレート内での変数宣言ですね。
$variable := pipeline

{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

便利そうですね。

テンプレート関数

テンプレートがオブジェクトのフィールドの値を出力する際、fmtパッケージを採用してオブジェクトを文字列に変換します。
しかしときどき我々はこうしたくはないときもあります。例えばスパムメールの送信者がウェブページから拾い集めてくる方法で我々のメールボックスへ情報を送信することを防止したいときがあります。

なるほど?
filter的なやつですね

各テンプレート関数はいずれも単一の名前をもっていて、一つのGo関数と関係しています。以下の方法によって関係をもたせます。 type FuncMap map[string]interface{}

ほう?

例えば、もしemail関数のテンプレート関数の名前をemailDealとしたい場合は、これが関係するGo関数の名前はEmailDealWithとなります。下の方法でこの関数を登録することができます。

t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})

サンプルコードを早速動かしてみましょう。

package main

import (
    "fmt"
    "html/template"
    "os"
    "strings"
)

type Friend struct {
    Fname string
}

type Person struct {
    UserName string
    Emails   []string
    Friends  []*Friend
}

func EmailDealWith(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    // find the @ symbol
    substrs := strings.Split(s, "@")
    if len(substrs) != 2 {
        return s
    }
    // replace the @ by " at "
    return (substrs[0] + " at " + substrs[1])
}

func main() {
    f1 := Friend{Fname: "minux.ma"}
    f2 := Friend{Fname: "xushiwei"}
    t := template.New("fieldname example")
    t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
    t, _ = t.Parse(`hello {{.UserName}}!
                {{range .Emails}}
                    an emails {{.|emailDeal}}
                {{end}}
                {{with .Friends}}
                {{range .}}
                    my friend name is {{.Fname}}
                {{end}}
                {{end}}
                `)
    p := Person{UserName: "Astaxie",
        Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
        Friends: []*Friend{&f1, &f2}}
    t.Execute(os.Stdout, p)
}

出力結果

hello Astaxie!

                    an emails astaxie at beego.me 

                    an emails astaxie at gmail.com



                    my friend name is minux.ma    

                    my friend name is xushiwei    


                bash-5.0# 

Must操作

テンプレートパッケージにはMustという関数があります。この作用はテンプレートが正しいか検査することです。例えば大括弧が揃っているか、コメントは正しく閉じられているか、変数は正しく書かれているかといったことです。

ほほう、検証メソッドのようですね。
さっき、置き換え可能とかいってましたがぜんぜん違う関数ですね。。
サンプルコードを早速動かしてみましょう。

package main

import (
    "fmt"
    "text/template"
)

func main() {
    tOk := template.New("first")
    template.Must(tOk.Parse(" some static text /* and a comment */"))
    fmt.Println("The first one parsed OK.")

    template.Must(template.New("second").Parse("some static text {{ .Name }}"))
    fmt.Println("The second one parsed OK.")

    fmt.Println("The next one ought to fail.")
    tErr := template.New("check parse error with Must")
    template.Must(tErr.Parse(" some static text {{ .Name }"))
}

出力結果

bash-5.0# go run template5.go 
The first one parsed OK.   
The second one parsed OK.  
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in operand

goroutine 1 [running]:
text/template.Must(...)
        /usr/local/go/src/text/template/helper.go:23
main.main()
        /go/src/app/template5.go:18 +0x566
exit status 2
bash-5.0# 

ネストしたテンプレート

Webアプリケーションを作る時はテンプレートの一部が固定され不変である場合がよくあり、抜き出して独立した部分とすることができます。例えばブログのヘッダとフッタが固定で、変更があるのは真ん中のコンテンツの部分だけだとします。そのためheader、content、footerの3つの部分として定義することができます。

これは、よくあるものですね。
共通のテンプレートがあるような場合ですね。

{{define “サブテンプレートの名前”}}コンテンツ{{end}}

ほうほう

以下の方法によってコールします:

{{template “サブテンプレートの名前”}}

サンプルコードを早速動かしてみましょう。

package main

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

func main() {
	s1, _ := template.ParseFiles("html/header.tmpl", "html/content.tmpl", "html/footer.tmpl")
	s1.ExecuteTemplate(os.Stdout, "header", nil)
	fmt.Println()
	s1.ExecuteTemplate(os.Stdout, "content", nil)
	fmt.Println()
	s1.ExecuteTemplate(os.Stdout, "footer", nil)
	fmt.Println()
	s1.Execute(os.Stdout, nil)
}
bash-5.0# go run template6.go

<html>
<head>
    <title>デモンストレーションの情報</title>
</head>
<body>



<html>
<head>
    <title>デモンストレーションの情報</title>
</head>
<body>

<h1>ネストのデモ</h1>
<ul>
    <li>ネストではdefineを使用してサブテンプレートを定義します。</li>
    <li>templateの使用をコール</li>
</ul>

</body>
</html>



</body>
</html>

bash-5.0# 

上手く行ってますね、これでテンプレートを共通化できそうです。

上の例でtemplate.ParseFilesを使ってすべてのネストしたテンプレートをテンプレートの中にパースできることがお分かりいただけたかと思います。
各定義の{{define}}はすべて独立した一個のテンプレートで、互いに独立しています。
並列して存在している関係です。内部ではmapのような関係(keyがテンプレートの名前で、valueがテンプレートの内容です。)が保存されています。

なるほど

その後ExecuteTemplateを使って対応するサブテンプレートの内容を実行します。headerとfooterのどちらも互いに独立していることがわかります。どれもコンテンツを出力できます。contentの中でheaderとfooterのコンテンツがネストしているので、同時に3つの内容を出力できます。しかし、s1.Executeを実行した時、何も出力されません。デフォルトではデフォルトのサブテンプレートが無いからです。そのため何も出力されません。

基本は、 content を出力すれば、headerfooterを含んでいるので目的のページが表示されますね。

おわりに

template は結構長いですが、Web開発には必須ですね。
次回は「ファイルの操作」です。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。