$30 off During Our Annual Pro Sale. View Details »

Go言語で始めるCloudflare Workers

syumai
December 15, 2023

Go言語で始めるCloudflare Workers

syumai

December 15, 2023
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. 自己紹介 syumai ECMAScript 仕様輪読会 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー

    (gqlgen) や TypeScript でフロント エンドを書いています Software Design 12 月号からCloudflare Workers の連載をして ます Twitter: @__syumai Website: https://syum.ai
  2. workers https://github.com/syumai/workers http.Handler を作って、 workers.Serve に渡すだけでCloudflare Workers 上でHTTP サ ーバーとして動作する

    http.ListenAndServe の代わりに workers.Serve 関数を呼ぶ形にするだけ TinyGo / Go を使ってWebAssembly を生成して実行する JavaScript 側のコードを触る必要が無い Cloudflare の機能のバインディングを提供 (R2, KV, D1 など)
  3. Quick Start 必要なツール Node.js ( とnpm) wrangler npm install -g

    wrangler . tinygo 0.29.0 以上 gonew go install golang.org/x/tools/cmd/gonew@latest 最近Go 公式から試験的なものとして出た、Go のプロジェクトをテンプレートから 立ち上げるためのコマンド
  4. プロジェクトの作成 $ gonew github.com/syumai/workers/_templates/cloudflare/worker-tinygo your.module/my-app $ cd my-app $ go

    mod tidy $ make dev # 開発サーバーの起動 $ curl http://localhost:8787/hello # outputs "Hello!" 今回は、 モジュール名を github.com/syumai/workers-tech-talk/hello-tinygo に します
  5. ファイル構成 ├── Makefile ├── README.md ├── build # ここはいじる必要がない │

    ├── app.wasm │ ├── polyfill_performance.js │ ├── shim.mjs │ ├── wasm_exec.js │ └── worker.mjs ├── go.mod ├── go.sum ├── main.go # アプリケーション本体 └── wrangler.toml
  6. アプリケーションの中身 func main() { http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {

    msg := "Hello!" w.Write([]byte(msg)) }) http.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) { io.Copy(w, req.Body) }) workers.Serve(nil) // use http.DefaultServeMux } 普通のHTTP Server 同様、HandleFunc を使っているだけ /hello ハンドラーの実装を変更すると、curl の結果が変わるのが確認できる
  7. syumai/workers の仕組み Cloudflare Workers は、基本的にRequest オブジェクトを処理し、Response オブジェクトを 返すと言う構造になっているため、下記が実装できればOK JavaScript 側で受け取ったRequest

    オブジェクトをGo に渡す Go 側でResponse オブジェクトを組み立ててJavaScript 側に渡す これをGo の標準ライブラリのsyscall/js を使って実装した
  8. syumai/workers の仕組み Worker のendpoint は(当然ながら)JS JS 側は、Go 側の初期化処理が終わるのを待つためのPromise を作成してglobalThis に設

    定する Go 側の初期化処理で、Go にJS のRequest オブジェクトを渡すための handleRequest 関数をglobalThis に設定する Go 側の初期化処理が終わったら、Promise を解決 JS 側からhandleRequest 関数を呼んで、結果を返す
  9. Worker のendpoint (JS 側) のコード async function run() { //

    Go側の初期化完了を待つPromiseをglobalThisに設定 const readyPromise = new Promise((resolve) => { globalThis.ready = resolve; }); const instance = new WebAssembly.Instance(mod, go.importObject); go.run(instance); await readyPromise; } // Fetch Handlerを使うのは普通のWorkerと同じ export async function fetch(req, env, ctx) { await run(); // RequestをJS側からGo側に渡して処理する return handleRequest(req, { env, ctx }); } https://github.com/syumai/workers/blob/v0.18.0/cmd/workers-assets- gen/assets/common/shim.mjs
  10. Go 側の初期化処理のコード ( 色々省略しています) init 関数の中で、JS 側からGo 側にRequest オブジェクトを渡して処理する handleRequest

    関数をglobalThis に設定する func init() { handleRequestCallback := js.FuncOf(func(this js.Value, args []js.Value) any { reqObj := args[0] cb := js.FuncOf(func(_ js.Value, pArgs []js.Value) any { resolve := pArgs[0] go func() { res := handleRequest(reqObj) resolve.Invoke(res) }() return js.Undefined() }) return jsutil.NewPromise(cb) }) jsutil.Global.Set("handleRequest", handleRequestCallback) } https://github.com/syumai/workers/blob/v0.18.0/handler.go
  11. Go 側の初期化処理のコード workers.Serve 関数が呼ばれた時点では既にmain 関数の処理に入っており、init 関数 の処理は終わっているので、JS 側で待ち受けていたPromise を解決する func

    Serve(handler http.Handler) { if handler == nil { handler = http.DefaultServeMux } httpHandler = handler jsutil.Global.Call("ready") select {} } https://github.com/syumai/workers/blob/v0.18.0/handler.go
  12. 変換処理 Go 側、JS 側でストリームの変換処理が必要 Go のhttp.ResponseWriter -> JS のReadableStream この辺りの話はZenn

    に書きました Cloudflare Workers で簡単にGo のHTTP サーバーを動かすためのライブラリを作っ た - https://zenn.dev/syumai/articles/ca9n4e91eqljc44k6ebg
  13. gqlgen 製のGraphQL サーバー gqlgen 公式から拾ってきたSTAR WARS server STAR WARS 関連の情報をGraphQL

    経由で引っ張るAPI 通常のGo を使っています (2023 年12 月現在、TinyGo では動作しませんでした) https://gqlgen-starwars-example.syumai.workers.dev/ https://github.com/syumai/workers-playground/tree/main/gqlgen-starwars-example こんな感じのクエリを投げると結果が返ってくる query Starships { starship(id: "3003") { id name length history } }
  14. connect-go 製のサーバー 事前に定義したProtobuf をベースに、Buf でconnect-go のコードを生成、サーバーを実 装したもの 通常のGo を使っています (2023

    年12 月現在、TinyGo では動作しませんでした) https://emoji.syum.ai message GetEmojiRequest { string short_name = 1; } message Emoji { string short_name = 1; string emoji = 2; } service EmojiService { rpc GetEmoji(GetEmojiRequest) returns (GetEmojiResponse) {} }
  15. connect-go 製のサーバー $ curl -s -H 'Content-Type: application/json' \ https://emoji.syum.ai/emoji.v1.EmojiService/GetEmoji

    \ -d '{"short_name": "star"}' | gzip -d | jq . { "emoji": { "shortName": "star", "emoji": " " } } 実装はこちら: https://github.com/syumai/workers-playground/blob/main/connect-go- emoji-server
  16. 画像生成サーバー Go 製のsyumai のプロフィールページ TinyGo で動いています もともとGoogle App Engine で動いていたもの

    を、Cloudflare Workers に移管した グリーンピースの色をランダムに変化させて表 示したりする https://syum.ai/
  17. 画像生成サーバー image/png の利用イメージ import "image/png" func writePNG(w http.ResponseWriter, cMap syumaigen.ColorMap)

    { w.Header().Set("Content-Type", "image/png") img, _ := syumaigen.GenerateImage( syumaigen.Pattern, cMap, 10, ) var buf bytes.Buffer png.Encode(&buf, img) w.WriteHeader(http.StatusOK) io.Copy(w, &buf) } https://github.com/syumai/syum.ai/blob/main/server/image.go
  18. 静的HTML 配信のベンチマーク (hey https://syum.ai で200req) Summary: Total: 0.2159 secs Slowest:

    0.1564 secs Fastest: 0.0164 secs Average: 0.0427 secs Requests/sec: 926.2291 Response time histogram: 0.016 [1] | 0.030 [144] |▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪ 0.044 [5] |▪ 0.058 [5] |▪ 0.072 [2] |▪ 0.086 [0] | 0.100 [0] | 0.114 [26] |▪▪▪▪▪▪▪ 0.128 [10] |▪▪▪ 0.142 [6] |▪▪ 0.156 [1] | Latency distribution: 10% in 0.0182 secs 25% in 0.0197 secs 50% in 0.0216 secs 75% in 0.0508 secs 90% in 0.1131 secs 95% in 0.1256 secs 99% in 0.1304 secs <- 99 percentileで約130ms
  19. 画像生成のベンチマーク (hey https://syum.ai/image で200req) Summary: Total: 0.3666 secs Slowest: 0.1868

    secs Fastest: 0.0253 secs Average: 0.0608 secs Requests/sec: 545.4886 Response time histogram: 0.025 [1] | 0.041 [127] |▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪ 0.058 [20] |▪▪▪▪▪▪ 0.074 [0] | 0.090 [1] | 0.106 [0] | 0.122 [18] |▪▪▪▪▪▪ 0.138 [23] |▪▪▪▪▪▪▪ 0.154 [1] | 0.171 [7] |▪▪ 0.187 [2] |▪ Latency distribution: 10% in 0.0296 secs 25% in 0.0341 secs 50% in 0.0384 secs 75% in 0.1172 secs 90% in 0.1294 secs 95% in 0.1390 secs 99% in 0.1773 secs <- 99 percentileで約180ms
  20. TCP sockets 対応 Cloudflare Workers 的には、 cloudflare:sockets の connect 関数でTCP

    sockets に 対応している これを使えばTCP 上で動作する任意のプロトコルが使えるようになるので、あらゆる DB が選択肢に入る Go 製のDB ドライバが使えるのは魅力的 コネクションプールの問題も、Hyperdrive が登場したことにより、ある程度解消が見 込めるのでセットで使いたい 実はPR はもらっている 年末年始休暇で何とかします!