LoginSignup
27
14

More than 3 years have passed since last update.

Go Template、最高のプログラミング言語

Posted at

はじめに

template はGo言語標準のDSLですが、その構文は Brainf*ckインタープリターを実装できるほど強力です。

go-templateはチューリング完全?brainf*ck処理系を作ってみた - Qiita

さらに、 Masterminds/sprig の関数を使うことでより高度な処理が可能1になります。

これはもうプログラミング言語としても遜色ないのでは?
というわけで、プログラミング言語にしてみました

GitHub - Syuparn/tmplscript: executable go-template command (like awk and jq!)

Go templateはソースコード生成やdocker, kubectlなどでも使えるので、コマンドの試し打ち等にも使っていただければ幸いです。

以下、こちらのtmplscriptを使いながら、Go templateでのプログラミング方法について紹介したいと思います。

(記事を書いている途中に気が付いたのですが、すでに先駆者がいらっしゃいました(しかも★1000越え)...)

GitHub - hairyhenderson/gomplate: A flexible commandline tool for template rendering. Supports lots of local and remote datasources.

基本文法

地の文はそのまま表示され、{{ }}でかこんだ場所は中身を評価した結果が表示されます。

tmpl:1> Hello, {{print "world" "!"}}
Hello, world!

空白文字はそのまま出力されますが、- を付けると隣接する空白を取り除くことができます。
-さえつければ、可読性のためにインデントをいくらつけても問題ありません。

{{if true}}
    {{println "got it!"}}
{{else}}
    {{println "no..."}}
{{end}}

    got it!

{{- if true}}
    {{- println "got it!"}}
{{- else}}
    {{- println "no..."}}
{{- end}}
got it!

基本的に{{ }}内で改行はできませんが、コメントとraw stringのみ複数行可能です。

multiline_error.tmpl
{{- print
"foo"
-}}
template error:
template: tmpl:1: unclosed action
multiline.tmpl
{{- /*
コメントは
複数行可能
*/ -}}
{{- `
rawstringも
複数行可能
` -}}

関数

パイプラインで可読性を上げる

関数は関数名 引数1 引数2 ...という形式で呼び出します。

tmpl:1> {{add 1 2}}
3

かっこが多くなると読みづらくなりますが、パイプラインを使えばコードが追いやすくなります。

tmpl:2> {{printf "%c" (index (print "ab" "cd") 1)}}
b
tmpl:3> {{1 | index (print "ab" "cd") | printf "%c"}}
b

注意として、パイプラインの左辺値は第一引数ではなく最後の引数になります!メソッド呼び出しのような感覚で使うと間違えがちです(実話)。

Sprigは、もともとパイプラインで使うことを想定してか「レシーバっぽい引数」が最後に来る傾向があります。

tmpl:4> {{list "a" "b" | join ","}}
a,b

sprigを使う

Go Templateでは、デフォルト関数以外にも自作関数を登録することができます。

main.go
// Funcsメソッドに渡した関数も使用可能
func newTemplate(leftDelim, rightDelim string) *template.Template {
    return template.New("tmpl").Delims(leftDelim, rightDelim).Funcs(funcMap())
}

1から作るのもよいですが、sprig を呼び出せば大抵のやりたいことができるようになります。

文字列処理、数値演算、コレクション処理等、なんと100種類以上の関数を利用可能です!

Sprig Function Documentation | sprig

変数

動的型付け

Go言語本体と違い、Go Templateは動的型付け言語です。別の型を変数に再代入することができます。

tmpl:1> {{$x := 1}}{{$x}}
1
tmpl:2> {{$x = "s"}}{{$x}}
s

代入式

代入式は右辺値に評価されますが、文字は表示されません
代入だけで文字が出力されると鬱陶しいのでこの機能はありがたいです。

tmpl:1> {{$y := ($x := "s")}}

tmpl:2> y={{$y}}, x={{$x}}
y=s, x=s

これを応用して、副作用を起こした戻り値をダミー変数に代入すれば出力を隠すことができます。

{{ /* 要素を追加したいだけなのに、戻り値のdictが表示されてしまう... */ }}
tmpl:3> {{$d := dict "a" 1}}

tmpl:4> {{$d}}
map[a:1]
tmpl:5> {{set $d "b" 2}}
map[a:1 b:2]
{{ /* ダミー変数に代入すれば何も出力しない! */ }}
tmpl:6> {{$_ := set $d "c" 3}}

tmpl:7> {{$d}}
map[a:1 b:2 c:3]

使える文字

本家Go言語より柔軟で、数字だけの変数名も可能です。一時変数に使えるかもしれません。

tmpl:10> {{$1 := "foo"}}

tmpl:11> {{$1}}
foo

文字列操作

print

print, println, printfは全て文字列を返す関数(fmt.SprintXXに相当)です。

特に、printは文字列結合の関数として使うので要注意です。

tmpl:1> {{print "a" "b"}}
ab

名前の変換処理(Sprig)

ソースコード等自動生成する際に厄介なのが名前の大文字小文字変換、camelcase,snakecase変換です。

Sprigなら正規表現を使わずとも可能です。

tmpl:1> {{snakecase "FooBar"}}
foo_bar
tmpl:2> {{camelcase "FooBar"}}
Foobar
tmpl:3> {{camelcase "foo_bar"}}
FooBar
tmpl:4> {{title "fooBar"}}
FooBar
tmpl:5> {{kebabcase "FooBar"}}
foo-bar
tmpl:6> {{"Hoge" | replace "o" "i"}}
Hige
tmpl:7> {{"tmp_foo" | trimPrefix "tmp_"}}
foo
tmpl:8> {{"foo_v2" | trimSuffix "_v2"}}
foo

あとは、protoc-gen-gotemplate限定ですが lowerFirst(先頭を小文字にする)も便利です。

nindent (Sprig)

複数行の文字列にインデントを入れる場合、いちいちsplitrangeを使う必要はありません。nindentを使えば、各行にn文字のインデントを入れられます。

{{- `
rawstringも
複数行可能
` | nindent 4 -}}

    rawstringも
    複数行可能

JSON(Sprig)

toJsonは構造体をjsonに変換します。json.Marshalの仕様でキーがABC順に並ぶので見やすいです。

tmpl:1> {{toJson (dict "name" "taro" "brothers" (list "jiro" "hanako"))}}
{"brothers":["jiro","hanako"],"name":"taro"}

fromJSONはjsonをmapに変換します。

tmpl:2> {{fromJson `{"name": "Jiro", "brothers": [{"name": "Taro"}, {"name": "Hanako"}]}`}}
map[brothers:[map[name:Taro] map[name:Hanako]] name:Jiro]

組み合わせることで、jqみたいにJSONを処理することも可能です。

# .nameを抜き出す
$ echo '{"name": "Jiro", "brothers": [{"name": "Taro"}, {"name": "Hanako"}]}' | tmplscript '{{(fromJson .).name | toJson}}'
"Jiro"

条件分岐、制御構文

if

if文の使い方は本家とほぼ同じですが、bool以外も条件に使用可能です。この際、ゼロ値はfalsyとして扱われます。

tmpl:1> {{if gt 2 1}}yes!{{end}}
yes!
tmpl:2> {{if "s"}}yes!{{end}}
yes!
tmpl:3> {{if list}}yes!{{else}}no...{{end}}
no...
tmpl:4> {{if ""}}yes!{{else}}no...{{end}}
no...
tmpl:5> {{if 0}}yes!{{else}}no...{{end}}
no...

条件式を変数に代入することもできます。ifはスコープを持つのでグローバルを汚さず変数宣言できます

tmpl:6> {{if $s := "a"}}{{print "found: " $s}}{{end}}
found: a
ifはスコープを持つ
tmpl:2> {{if $s := "a"}}{{$x := 1}}{{print "found: " $s}}{{end}}
found: a

tmpl:3> {{$s}}
template error:
template: tmpl:3: undefined variable "$s"
tmpl:3> {{$x}}
template error:
template: tmpl:3: undefined variable "$x"

with

withはelseのないif文です。if同様スコープを持ちます。
ifとの唯一の違いは.の扱いです(詳しくは「.$」参照)。

  • グローバルを汚さずローカル変数を宣言できる
  • emptyでない場合のみ処理したい

場合に便利です。

withはスコープを持つ
tmpl:1> {{with $s := "foo"}}{{print $s}}{{end}}
foo

tmpl:2> {{$s}}
template error:
template: tmpl:8: undefined variable "$s"

ただし、ゼロ値(=falsy)を代入すると評価されなくなるので注意です(何度もハマった)。

tmpl:3> {{with $s := ""}}{{print $s}}{{end}}

.$

$は特殊な変数で、テンプレート実行時に渡された値が代入されています。

例えばtmplscriptでは、AWKのように使えるように標準入力の各行を渡しています。

main.go
scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()

        // 第二引数はテンプレート内で`$`として参照可能
    err = tmpl.Execute(os.Stdout, line)
        if err != nil {
            fmt.Fprintf(os.Stderr, "runtime error:\n%v\n", err)
            return
        }
    }
$ echo "foo" | tmplscript "{{$}}"
foo

.(dot)は、thisのような働きをします。トップレベルでは$を指します。

$ echo "foo" | tmplscript "{{.}}"
foo

rangeの中では、.はループ変数を指します。

tmpl:1> {{range until 3}}{{println .}}{{end}}
0
1
2
{{ /* ループ変数の代入は関係ない */ }}
tmpl:2> {{range $i := until 3}}{{println .}}{{end}}
0
1
2

withの中では、.は先頭の値を指します。

tmpl:4> {{with $i := 1}}{{println .}}{{end}}
1

注:ifはwithと違い.に影響を及ぼしません。

tmpl:7> {{if $i := 1}}{{println .}}{{end}}
<nil>

要素参照と.

.でフィールドやmapの要素を参照することが可能です。存在しない場合もエラーにはならずno valueが返ります。

tmpl:1> {{$d := dict "a" 1}}

tmpl:2> {{$d.a}}
1
tmpl:3> {{$d.b}}
<no value>
{{ /* 存在しない要素の子要素を参照してもぬるぽしない */ }}
tmpl:4> {{$d.b.c}}
<no value>

変数だけでなく、式の要素を参照することも可能です。

tmpl:5> {{(dict "a" 1).a}}
1

レシーバを省略した場合は.の要素が参照されます。

tmpl:6> {{with dict "a" 1}}{{.a}}{{end}}
1
{{ /* 以下のシンタックスシュガー */ }}
tmpl:7> {{with dict "a" 1}}{{(.).a}}{{end}}
1

注:ドットはパイプラインとは関係ありません。jqのように左辺の要素をとりだすことはできません。

間違った.の使い方
tmpl:1> {{fromJson `{"items": [1, 2, 3]}` | .items}}
<no value>

ternary(Sprig)

ifを簡潔に使いたい場合はternary(3項演算子)が便利です。

tmpl:1> {{true | ternary "yes" "no"}}
yes
tmpl:2> {{false | ternary "yes" "no"}}
no

注:if文と違いboolしか条件式に書けません

tmpl:3> {{1 | ternary "yes" "no"}}
runtime error:
template: tmpl:3:21: executing "tmpl" at <"no">: wrong type for value; expected bool; got int

fail(Sprig)

ビジネスルール上出力を取りやめたい場合はfailで例外を起こすのが便利です。

引数で与えたメッセージのエラーが生じ異常終了します。

tmpl:1> {{$x := 1}}

tmpl:2> {{if ne $x 0}}{{fail "x must be zero"}}{{end}}
runtime error:
template: tmpl:2:17: executing "tmpl" at <fail "x must be zero">: error calling fail: x must be zero

Go templateはスクリプト言語同様エラーが起こる直前までは出力されるので、先頭でfailするのがおすすめです。

ぬるぽ2回避

値が見つからなかった場合はデフォルト値を設定したい場合があると思います。そんなときはor, coalesce, defaultが便利です。

{{ /* 最初のtruthyを返す */ }}
tmpl:1> {{or "" true 1}}
true
{{ /* 最初のtruthyを返す(sprig) */ }}
tmpl:2> {{coalesce "" true 1}}
true
{{ /* 第2引数がfalsyなら代わりに第1引数を返す(sprig) */ }}
tmpl:3> {{"hoge" | default "default-str"}}
hoge
tmpl:4> {{"" | default "default-str"}}
default-str

orcoalesceの使い分けがいまひとつ分かっていないので、ご存知の方はコメント欄で教えていただけるとありがたいです)

ループ、コレクション操作

range

rangeは、ループ処理を行います。

tmpl:1> {{range $i, $v := list "a" "b" "c"}}{{printf "$i:%d $v:%s\n" $i $v}}{{end}}
$i:0 $v:a
$i:1 $v:b
$i:2 $v:c

ただし、左辺が1変数のみの場合はキーでは無くて値が代入されます。(本家もこうして欲しい)

tmpl:2> {{range $v := list "a" "b" "c"}}{{printf "$v:%s\n" $v}}{{end}}
$v:a
$v:b
$v:c

また、string[]byteとみなされずループできません 。これは本家をまねてほしい...

main.go
package main
import "fmt"
func main(){
    for _, v := range "abc" {
        fmt.Printf("%c", v)   
    } // abc
}
templateでは無理
tmpl:3> {{range $c := "abc"}}{{printf "%c" $c}}{{end}}
runtime error:
template: tmpl:6:15: executing "tmpl" at <"abc">: range can't iterate over abc

イテレーション用関数

古き良きfor文が使いたい場合は、untilが便利です。上記のstringの場合はuntilとindexで頑張ります。

tmpl:1> {{$s := "abc"}}

tmpl:2> {{range $i := until (len $s)}}{{index $s $i | printf "%c\n"}}{{end}}
a
b
c

untilStepはPythonのrangeのようにstart,stop,stepが指定できます。

tmpl:4> {{range $i := untilStep 10 3 -2}}{{println $i}}{{end}}
10
8
6
4

配列処理(Sprig)

listで配列を生成できます。型はごちゃ混ぜでOKです。

tmpl:5> {{list "a" 2 true}}
[a 2 true]
{{ /* 入れ子もOK */ }}
tmpl:6> {{list "a" 2 (list 1 3 4)}}
[a 2 [1 3 4]]

配列処理関数には以下のようなものがあります(他にもたくさんあります)。いずれもimmutable関数です。

{{ /* 要素追加 */ }}
tmpl:7> {{$x := list 1 2 3}}
tmpl:8> {{append $x 4}}
[1 2 3 4]
tmpl:9> {{prepend $x 0}}
[0 1 2 3]
{{ /* 要素削除 */ }}
tmpl:10> {{without $x 2}}
[1 3]
{{ /* 重複を無くす */ }}
tmpl:11> {{list 1 1 3 2 3 | uniq}}
[1 3 2]
{{ /* 要素チェック */ }}
tmpl:13> {{$x | has 3}}
true
{{ /* 並べ替え */ }}
tmpl:14> {{$x | reverse}}
[3 2 1]
tmpl:15> {{list "a" "c" "b" | sortAlpha}}
[a b c]

map処理(Sprig)

mapはdictで生成可能です。key0, value0, key1, value1,...の順に指定します。

tmpl:1> {{dict "a" "hoge" "b" "fuga"}}
map[a:hoge b:fuga]
tmpl:2> {{(dict "a" "hoge" "b" "fuga").a}}
hoge
{{ /* string以外のキーも使用可能 */ }}
tmpl:3> {{dict "a" "hoge" 1 2}}
map[1:2 a:hoge]

getで要素取得し、setで要素更新可能です。特にsetは貴重なmutable関数なので、下記のテンプレートと組み合わせると凶悪な黒魔術が可能です。

tmpl:1> {{$d := dict "a" 1}}

tmpl:2> {{get $d "a"}}
1
{{ /* 破壊的変更! */ }}
tmpl:3> {{set $d "b" 2}}
map[a:1 b:2]
tmpl:4> {{$d}}
map[a:1 b:2]

他には以下のような関数があります(まだまだあります)。

tmpl:1> {{$d := dict "a" 1 "b" 2 "c" 3}}
{{ /* キー */ }}
tmpl:2> {{keys $d}}
[a b c]
{{ /* 値 */ }}
tmpl:3> {{values $d}}
[3 1 2]
{{ /* 特定キーのみ抽出 */ }}
tmpl:4> {{pick $d "a" "c"}}
map[a:1 c:3]
{{ /* 複数dictから特定キーのみ抽出 */ }}
tmpl:5> {{$d2 := dict "a" "A" "b" "B"}}
tmpl:6> {{pluck "a" $d $d2}}
[1 A]

テンプレートメタプログラミング

ご利用は計画的に。

templateで処理の共通化

テンプレートにはテンプレートを入れ子(nested template)にすることができます。defineで定義し、templateで呼び出します。

tmpl:1> {{define "my-tmpl"}}Hello, world!{{end}}

tmpl:2> {{template "my-tmpl"}}
Hello, world!

さらに、templateの呼び出し時には、$にセットされる値を引数に渡すことができます。

tmpl:3> {{define "twice"}}{{print $ $}}{{end}}

tmpl:4> {{template "twice" "abc"}}
abcabc
tmpl:5> {{template "twice" 123}}
123 123

nested templateを使うことでコードをモジュール化し、重複を減らすことができます。

ただし、テンプレートには以下の制約があります。

  • 第一級ではない
    • 評価した結果を文字列として受け取ることはできません
  • 名前は静的にしか指定できない
    • {{template $x .}}のような呼び出しはできません
  • レキシカルスコープではない
    • 呼び出し時に渡した値($にセットされる)を除き、外側のテンプレートの変数を参照できません

複数引数渡す(Sprig)

前述の通りnested templateには値を1つしか渡せません。しかし1つしか渡せないのなら、複数の引数をarrayかmapにまとめてしまえばよいのです

{{- define "addition"}}
    {{- /* listを各引数にばらす */ }}
    {{- $x := index $ 0 }}
    {{- $y := index $ 1 }}
    {{- printf "%d + %d = %d" $x $y (add $x $y)}}
{{- end}}

{{template "addition" list 2 3}}
{{template "addition" list 4 6}}
2 + 3 = 5
4 + 6 = 10

この方法の欠点は、引数の順番を間違えても実行時エラー(最悪の場合暴走)になるまで気づけないことです。

テンプレートで再帰処理

nested templateの内部でもテンプレート呼び出しは可能です。すなわち、自分自身を呼び出せば再帰ができます

階乗だってお手の物。

example/template_factorial.tmpl
{{- define "_fact" -}}
    {{- $i := index . 0 -}}
    {{- $acc := index . 1 -}}
    {{- if le $i 1 -}}
        {{- println $acc -}}
    {{- else -}}
        {{- template "_fact" list (sub $i 1) (mul $acc $i) -}}
    {{- end -}}
{{- end -}}

{{- define "fact" -}}
    {{- template "_fact" list . 1 -}}
{{- end -}}


{{- range $i := until 11}}
    {{- printf "%2d! = " $i}}{{template "fact" $i}}
{{- end}}
 0! = 1
 1! = 1
 2! = 2
 3! = 6
 4! = 24
 5! = 120
 6! = 720
 7! = 5040
 8! = 40320
 9! = 362880
10! = 3628800

注意:テンプレートは10万回しか再帰できません(web-assemblyなら1000回)。上手く計算量を抑えましょう。

tmpl:1> {{define "recur"}}{{if gt . 0}}{{println .}}{{template "recur" sub . 1}}{{end}}{{end}}

tmpl:2> {{template "recur" 131072}}
runtime error:
template: tmpl:1:55: executing "recur" at <{{template "recur" sub . 1}}>: exceeded maximum template depth (100000)

テンプレートを関数として使う

テンプレートは値を返せませんが、副作用は起こせますdictを渡して、計算結果をsetで挿入させれば実質的な関数として使えます。

example/template_as_func.tmpl
{{- define "templateAdd" -}}
    {{- /* setで足し算の結果を"ret"キーに挿入 */ -}}
    {{- /* ($_への代入は文字列を表示しないため) */ -}}
    {{- $_ := set . "ret" (add (index .args 0) (index .args 1)) -}}
{{- end -}}

{{- with $d := dict "args" (list 2 3) -}}
    {{- /* 副作用だけ起こす */ -}}
    {{- template "templateAdd" $d -}}
    {{- /* 結果を取得 */ -}}
    {{- print "result: " $d.ret -}}
{{- end -}}
result: 5

処理をnested templateに切り出せるようになるのでコードがスッキリします。特にifやrangeのネストが減るのが嬉しいですね。
brainf*ckの処理系を書き直してみたところ、かなり可読性が上がりました (esolang基準)

素のGo Templateのみ: go-template-bf-interpreter/bf-interpreter.tpl at main · Syuparn/go-template-bf-interpreter · GitHub

nested template(+当記事の各種テクニック)使用: tmplscript/bf_interpreter.tmpl at main · Syuparn/tmplscript · GitHub

最後に

徒然なるままにGo Templateの小技を紹介しました。最後までお付き合いいただきありがとうございます。
「他にもこんな使い方があるよ!」等ございましたら、ぜひコメントしていただけるとありがたいです。

それでは、皆様良いGo Templateプログラミングを!

参考文献

template - The Go Programming Language

Sprig Function Documentation | sprig


  1. というよりむしろ、プログラミング言語としてはsprigがないgo templateはesolangです 

  2. 正確には「にるぽ」(nil pointer dereference)ですね...。ガッ 

27
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
14