あらすじ
Go で素朴な HTML を返すアプリケーションを動かしたくなり、 Wasm に変換してデプロイすることができる Cloudflare Workers を使うことにしました。
Cloudflare Workers に Go アプリケーションをデプロイする際には、id:f_syumai さんのテンプレートがオススメです。
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近くなってしまいました。
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 と出会いました。
特徴
ここで、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 generate
で DSL から生成された、 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の拡張機能
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
となっており、アプリケーションを拡張する余裕があり安心です。
今回の検証はこちらのリポジトリに残しています。