Goのテンプレートエンジン「templ」が便利(TinyGoでも動くよ)

あらすじ

Go で素朴な HTML を返すアプリケーションを動かしたくなり、 Wasm に変換してデプロイすることができる Cloudflare Workers を使うことにしました。

Cloudflare Workers に Go アプリケーションをデプロイする際には、id:f_syumai さんのテンプレートがオススメです。

github.com

Go + text/template

まずは、素直に text/template 使ってビルドしてみます。

ls -lh ./build 
   total 15656
   -rwxr-xr-x  1 ergofriend  staff   7.6M  8  8 20:12 app.wasm
   -rw-r--r--  1 ergofriend  staff   1.2K  8  8 20:12 shim.mjs
   -rw-r--r--  1 ergofriend  staff    16K  8  8 20:12 wasm_exec.js
   -rw-r--r--  1 ergofriend  staff   160B  8  8 20:12 worker.mjs

およそ8MB近くなってしまいました。

developers.cloudflare.com

Cloudflare Workers にはプランによって Worker のサイズ制限があります。

無料枠では 1MB、有料枠($5~)では 10MB となっています。

最終的な圧縮後のサイズによる制限とはいえ、8MB からでは無料枠に収めるのは厳しそうです。

TinyGo + text/template

そこで、WebAssembly(Wasm) 向けとされている TinyGo に乗り換えてみることにしました。

ビルドしてみると、およそ 0.75MB となり無料枠に収まりそうです。

ls -lh ./build
total 1160
-rwxr-xr-x  1 ergofriend  staff   556K  8  8 20:23 app.wasm
-rw-r--r--  1 ergofriend  staff   1.2K  8  8 20:23 shim.mjs
-rw-r--r--  1 ergofriend  staff    15K  8  8 20:23 wasm_exec.js
-rw-r--r--  1 ergofriend  staff   160B  8  8 20:23 worker.mjs

Oops!

しかし、ここで悲劇が起こります。

ビルドしたアプリケーションにアクセスしてみると、エラーになってしまいます。

[wrangler:inf] GET / 200 OK (35ms)[ERROR] Uncaught (in response) RuntimeError: unreachable

      at main.runtime._panic (wasm://wasm/main-0022bc46:wasm-function[35]:0x2b4a)
      at main.(*text/template.state).evalField
  (wasm://wasm/main-0022bc46:wasm-function[540]:0x6c5f4)
      at main.(*text/template.state).evalFieldChain
  (wasm://wasm/main-0022bc46:wasm-function[531]:0x697fe)
      at main.(*text/template.state).evalFieldNode
  (wasm://wasm/main-0022bc46:wasm-function[530]:0x6959a)
      at main.(*text/template.state).evalPipeline
  (wasm://wasm/main-0022bc46:wasm-function[535]:0x6a1d2)
      at main.(*text/template.state).walk (wasm://wasm/main-0022bc46:wasm-function[569]:0x72cdd)
      at main.(*text/template.state).walk (wasm://wasm/main-0022bc46:wasm-function[569]:0x730b0)
      at main.main.main$1 (wasm://wasm/main-0022bc46:wasm-function[261]:0x2ac52)
      at main.(net/http.HandlerFunc).ServeHTTP
  (wasm://wasm/main-0022bc46:wasm-function[463]:0x5973a)
      at
  main.interface:{ServeHTTP:func:{named:net/http.ResponseWriter,pointer:named:net/http.Request}{}}.ServeHTTP$invoke
  (wasm://wasm/main-0022bc46:wasm-function[459]:0x56f72)

panic: unimplemented: (reflect.Value).MethodByName()

template.ExecuteTemplate() でテンプレート変数を渡した時に呼ばれる、MethodByName というメソッドが未実装のようです。

TinyGo は本家のサブセットであるため未対応の機能が多々あり、 text/template が未対応のメソッドを呼んでしまっていることが分かりました。

tinygo/src/reflect/type.go at 1154212c15e6e97048e122068730dab5a1a9427f · tinygo-org/tinygo · GitHub

さて、ここでカッコよく TinyGo に p-r をと言いたいところですが、今回は手っ取り早く別の手段を探すことにします。

TEMPL

ということで、text/template の代替えとなる別のテンプレートエンジンを探していたところに templ と出会いました。

github.com

特徴

ここで、templ についてまとめてみます。

独自DSL

独自といっても難しいわけではなく、templ ≒ Go + JSX のような書き方ができます。

templ hello() {
  <div>Welcome back!</div>
}

とても馴染みのある書き方ができそうですね。

templ login(isLoggedIn bool) {
  if isLoggedIn {
    @hello() <!-- @で他のコンポーネントを利用できる -->
  } else {
    <input name="login" type="button" value="Log in"/>
  }
}

利用する際はtempl generateDSL から生成された、 Go の関数をコンポーネントごとに呼び出すことになります。

func login(isLoggedIn bool) templ.Component {
  return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5
  ...
}
http.Handle("/", templ.Handler(login(false)))

生成されたコンポーネントの関数には、テンプレートで定義した引数の型が引き継がれるので安全に呼び出すことができます。 どこかの ExecuteTemplate と違って安心ですね。

VSCode拡張機能

marketplace.visualstudio.com

Syntax Highlight や LSP による補完が効くのでとても便利そうです。

TinyGo + templ

念のため調べてみると reflect のTypeOf にのみ依存していましたが、これはすでに TinyGo でも実装されています。

tinygo/src/reflect/type.go at 1154212c15e6e97048e122068730dab5a1a9427f · tinygo-org/tinygo · GitHub

では早速 templ を使ってみることにします。 ビルドしてみると十分に余裕がありそうでした。

ls -lh ./build
total 1008
-rwxr-xr-x  1 ergofriend  staff   477K  8  8 20:35 app.wasm
-rw-r--r--  1 ergofriend  staff   1.2K  8  8 20:35 shim.mjs
-rw-r--r--  1 ergofriend  staff    15K  8  8 20:35 wasm_exec.js
-rw-r--r--  1 ergofriend  staff   160B  8  8 20:35 worker.mjs

実際のアプリケーションがこちら。

https://goworkers-demo.ergofriend.workers.dev/

アクセスしてみても、エラーにならずにHTMLを返却できています。

> wrangler deploy
Total Upload: 493.48 KiB / gzip: 187.91 KiB

最終的なデプロイサイズも 187.91 KiB となっており、アプリケーションを拡張する余裕があり安心です。

今回の検証はこちらのリポジトリに残しています。

github.com