Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cloudflare WorkersでGoのHTTPサーバーを動かすライブラリを作った話

syumai
August 01, 2022

Cloudflare WorkersでGoのHTTPサーバーを動かすライブラリを作った話

Zennに投稿した下記の記事に基づく発表です。
https://zenn.dev/syumai/articles/ca9n4e91eqljc44k6ebg

syumai

August 01, 2022
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. Cloudflare Workers
    でGo
    のHTTP
    サー
    バーを動かすライブラリを作った話
    syumai
    DP Engineering Monday (2022/8/1)

    View full-size slide

  2. 自己紹介
    syumai
    Go Documentation
    輪読会 / ECMAScript

    様輪読会 主催
    株式会社ベースマキナ所属
    Go
    でGraphQL
    サーバー (gqlgen)

    TypeScript
    でフロントエンドを書いています
    Twitter: @__syumai

    Website: https://syum.ai

    View full-size slide

  3. 話すこと
    Cloudflare Workers
    とは?
    syumai/workers
    の紹介
    実装テクニックの紹介
    syscall/js
    の処理など
    現状の課題
    example
    集の紹介

    View full-size slide

  4. Cloudflare Workers
    とは?
    Service Worker API
    ベースのJavaScript
    のWorker
    WebAssembly
    も実行可能
    Cloudflare
    経由のリクエストに割り込んで処理を行いレスポンスを返
    せる
    裏側にトラフィックを流すことなく、Worker
    が単独でレスポンスを組
    み立てて返してもOK
    https://blog.cloudflare.com/ja-jp/cloudflare-workers-unleashed-ja-
    jp/#worker

    View full-size slide

  5. Worker
    の例
    Request
    を受け取って、Response
    を返す関数を書く形式
    addEventListener("fetch", (event) => {
    event.respondWith(handleRequest(event.request));
    });
    async function handleRequest(request) {
    return new Response("Hello world");
    }
    https://developers.cloudflare.com/workers/
    より引用

    View full-size slide

  6. Cloudflare Workers
    の機能
    KV
    分散 key / value store
    結果整合 (
    複数edge
    からの同一キーへの書き込みは後勝ち)
    Durable Objects
    edge
    間でのデータ同期に使えるObject
    強整合
    Cache API
    ブラウザのキャッシュAPI
    のようなもの
    edge
    のキャッシュを操作出来る

    View full-size slide

  7. Cloudflare Workers
    の用途の広がり
    最近発表された新機能 (
    まだ使えないものもあります)
    R2
    S3
    互換のオブジェクトストレージ
    D1
    SQLite
    ベースの分散データベース
    Pub/Sub
    MQTT
    互換のメッセージング基盤
    => Cloudflare
    上でフルスタックアプリケーションを作れる環境に近付い
    ている

    View full-size slide

  8. syumai/workers
    の紹介

    View full-size slide

  9. workers
    https://github.com/syumai/workers
    http.Handler
    を作って、 workers.Serve
    に渡すだけでCloudflare
    Workers
    上でHTTP
    サーバーとして動作する
    必要なツールはtinygo
    とwrangler (Cloudflare Workers
    のCLI)
    だけ
    tinygo
    を使ってWebAssembly
    を生成して実行する
    JavaScript
    側のコードを触る必要が無い
    Cloudflare
    の機能のバインディングを提供 (R2, KV)

    View full-size slide

  10. workers
    を使ったコードのサンプル
    普通に http.HandlerFunc
    を作るだけ
    func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    name := req.URL.Query().Get("name")
    if name == "" {
    name = "world"
    }
    fmt.Fprintf(w, "Hello, %s!", name)
    })
    workers.Serve(handler)
    }
    https://hello.syumai.workers.dev/?name=syumai (=> Hello, syumai
    と表示)

    View full-size slide

  11. 作ったモチベーション
    WebAssembly
    とJavaScript
    の知識が無くてもGo
    でWorker
    を書けるよ
    うにしたかった
    最終的に、template
    のリポジトリをコピーして、Go
    のコードと
    wrangler.toml
    を編集するだけで済む形になった
    https://github.com/syumai/worker-template-tinygo
    entrypoint
    としてJS
    のコードを含んでいるが、修正は不要
    実用的なWorker
    を短時間で実装できるようなライブラリが欲しかった
    http.Handler
    のサポートで実現

    View full-size slide

  12. workers package
    を使わなかった場合の例
    Go
    とJS
    の2
    ファイルでそれぞれ実装が必要
    https://github.com/syumai/workers-playground/tree/main/tinygo-add

    View full-size slide

  13. Go
    側は、処理のexport
    が必要
    export comment
    による関数export
    はtinygo
    の機能
    package main
    //export add
    func add(a, b int) int {
    return a + b
    }
    func main() {}

    View full-size slide

  14. JS
    側は、Go
    インスタンスの初期化、Wasm
    のロード、Worker
    の処理
    の実装が必要
    const go = new Go();
    const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => { ... });
    export default {
    async fetch(req) {
    const instance = await load;
    const url = new URL(req.url);
    const a = url.searchParams.get("a");
    const b = url.searchParams.get("b");
    if (a === null || b === null) {
    return new Response("invalid", { status: 400 })
    }
    const result = `${a} + ${b} = ${instance.exports.add(a, b)}`;
    return new Response(result);
    },
    };

    View full-size slide

  15. 実装テクニックの紹介

    View full-size slide

  16. syumai/workers
    の基本的な実装形式
    Cloudflare Workers
    は、基本的にRequest
    オブジェクトを処理し、
    Response
    オブジェクトを返すと言う構造になっているため、下記が実
    装できればOK
    JavaScript
    側で受け取ったRequest
    オブジェクトをGo
    に渡す
    Go
    側でResponse
    オブジェクトを組み立ててJavaScript
    側に渡す
    これをGo
    の標準ライブラリのsyscall/js
    を使って実装した

    View full-size slide

  17. 紹介するもの
    基本的な値の変換
    バイト列の変換
    Promise
    の待ち受け
    ストリームの変換

    View full-size slide

  18. JS
    からGo
    への基本的な値の変換
    Go
    のコード上で、JavaScript
    側の値は、全て js.Value
    型で扱われる
    js.Value
    のメソッドを使って値を変換、操作出来る
    Go
    の基本的な型の値への変換 => Int()
    、String()
    など
    Object
    やArray
    など、複雑なObject
    の操作 => Index()
    、Set()
    、Get()
    JavaScript
    側のメソッド、関数の呼び出し => Call()
    、Invoke()

    View full-size slide

  19. Go
    からJS
    への基本的な値の変換
    基本的に、ValueOf
    のルールに従って自動的に行われるため、実はあ
    まり考えなくていい
    JavaScript
    側のnumber
    型は浮動小数点数型なので精度に注意
    | Go | JavaScript |
    | ---------------------- | ---------------------- |
    | js.Value | [its value] |
    | js.Func | function |
    | nil | null |
    | bool | boolean |
    | integers and floats | number |
    | string | string |
    | []interface{} | new array |
    | map[string]interface{} | new object |

    View full-size slide

  20. バイト列の変換
    syscall/js
    の CopyBytesToGo / CopyBytesToJS
    関数を使う
    Uint8Array
    と []byte
    で相互にデータのコピーが可能

    func (kv *kvNamespace) PutReader(key string, value io.Reader, opts *KVNamespacePutOptions) error {
    b, _ := io.ReadAll(value)
    ua := newUint8Array(len(b))
    js.CopyBytesToJS(ua, b)
    p := kv.instance.Call("put", key, ua.Get("buffer"), opts.toJS())
    return awaitPromise(p)
    }

    View full-size slide

  21. Go
    からJS
    側に値をコピーする時のテクニック
    コピー先のUint8Array
    がJavaScript
    側に無い時は、Go
    側から作る必要
    がある
    (Value).New
    を使ってclass
    のインスタンスを生成できるのでこれを使

    var uint8ArrayClass = global.Get("Uint8Array")
    func newUint8Array(size int) js.Value {
    return uint8ArrayClass.New(size)
    }
    https://github.com/syumai/workers/blob/v0.3.0/jsutil.go

    View full-size slide

  22. ストリームの変換
    JavaScript
    側のコードがReadableStream
    を返した時、これを
    io.Reader
    として扱えるようにしたかった
    逆もしかり
    deno_std
    のコードを参考に実装した
    https://github.com/syumai/workers/blob/v0.3.0/stream.go
    後から気付いたが、Go
    の標準ライブラリにも同様の実装があったので
    これを使っても良かったかもしれない
    net/http/roudtrip_js.go

    View full-size slide

  23. Promise
    の待ち受け
    Go
    から呼んだJavaScript
    側の関数がPromise
    を返す時、Go
    側のコード
    がブロックされない
    Promise
    を待ち受けるための処理はsyscall/js
    では提供されていない
    自前でchannel
    を使って書く必要がある

    View full-size slide

  24. then
    とcatch
    を呼んでchannel
    に送信、select
    文で待ち受けreturn
    func awaitPromise(promiseVal js.Value) (js.Value, error) {
    resultCh := make(chan js.Value)
    errCh := make(chan error)
    var then, catch js.Func
    then = js.FuncOf(func(_ js.Value, args []js.Value) any {
    defer then.Release()
    result := args[0]
    resultCh <- result
    return js.Undefined()
    })
    catch = js.FuncOf(func(_ js.Value, args []js.Value) any {
    defer catch.Release()
    result := args[0]
    errCh <- fmt.Errorf("failed on promise: %s", result.Call("toString").String())
    return js.Undefined()
    })
    promiseVal.Call("then", then).Call("catch", catch)
    select {
    case result := <-resultCh:
    return result, nil
    case err := <-errCh:
    return js.Value{}, err
    }
    }

    View full-size slide

  25. 現状の課題

    View full-size slide

  26. ファイルサイズ制限
    圧縮後のサイズが1MB
    以内でないといけない制約があるため、バイナ
    リサイズが大きくなる通常のGo
    ではpublish
    に失敗した
    実はtinygo
    でもギリギリで、依存ライブラリを増やすと簡単に越える

    View full-size slide

  27. tinygo
    でencoding/json
    が動かない
    tinygo
    のreflect
    の実装が完全でないため、encoding/json
    が動かない
    easyjson
    などの別のJSON encoder / decoder
    を使う必要がある

    View full-size slide

  28. tinygo
    でnet/http
    のHTTP Client
    が動かない
    tinygo
    を使った場合、Worker
    上でhttp.Get
    などが動かないため、プロ
    キシ用途で使うことが出来ない
    一応回避策を見つけて、ベーシック認証プロキシを作ることに成功し

    https://github.com/syumai/workers/tree/v0.3.0/examples/basic-
    auth-proxy
    tinygo 0.24.0
    で動かなくなってしまったが、takasago
    さんの修正で
    0.25.0
    でまた動くようになるはず
    https://github.com/tinygo-org/tinygo/pull/3036

    View full-size slide

  29. example

    JSON API
    サーバー
    R2
    を使った画像アップロード /
    配信サーバー
    KV
    を使ったアクセスカウンター
    ぜひ、template
    リポジトリからWorker
    を作ってpublish
    してみてくださ

    https://github.com/syumai/worker-template-tinygo

    View full-size slide

  30. 発表内容について
    Zenn
    の方にも記事を投稿しているので、興味があればぜひご覧ください
    Cloudflare Workers
    で簡単にGo
    のHTTP
    サーバーを動かすためのライ
    ブラリを作った
    https://zenn.dev/syumai/articles/ca9n4e91eqljc44k6ebg

    View full-size slide

  31. ご清聴ありがとうございました!

    View full-size slide