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 Slide

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

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

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

    Website: https://syum.ai

    View Slide

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

    View 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 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 Slide

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

    View Slide

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

    View Slide

  8. syumai/workers
    の紹介

    View 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 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 Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

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

    View 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 Slide

  25. 現状の課題

    View Slide

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

    View Slide

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

    View 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 Slide

  29. example

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

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

    View Slide

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

    View Slide

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

    View Slide