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

GoでAPIサーバをはやくつくる

 GoでAPIサーバをはやくつくる

Jumpei Takiyasu

May 18, 2019
Tweet

More Decks by Jumpei Takiyasu

Other Decks in Technology

Transcript

  1. Jumpei Takiyasu @juntaki
    M3, Inc.
    2019-05-18 Go Conference 2019 Spring

    View full-size slide

  2. name <- "Jumpei Takiyasu"
    twitter <- "juntaki"
    company <- "M3, Inc."
    title <- "Software Engineer/TL/PM"
    website <- "https://juntaki.com"
    Me

    View full-size slide

  3. きょうの話
    保守性の高いWebアプリを早く作るにはどうしたらよいか
    1. アーキテクチャをかんがえる
    2. 自動生成をつかって、はやくつくる

    View full-size slide

  4. サンプルアプリ
    GAE/Goで技術書典むけのダウンロードカードアプリを作った
    https://www.m3tech.blog/entry/m3techbook-01/qrcode
    「電子書籍ダウンロードカードの生成アプリ」
    の作り方が書いてある本を↑を使って頒布しました
    仕様
    ● 各カードに固有のQRコード(追跡可能)
    ● DLされるPDFの実体は1つ(差し替え可能)

    View full-size slide

  5. アーキテクチャをかんがえる

    View full-size slide

  6. システム構成
    SPAとAPIサーバをAppEngineでサクッと開発したい

    View full-size slide

  7. アーキテクチャをかんがえる
    Web APIの
    ※フロントエンド・インフラについては、今回の話では割愛します

    View full-size slide

  8. クリーンアーキテクチャを参考にする
    ● 実装詳細と、ビジネスルールを分ける
    ● 依存方向を内向きに揃える
    自動生成に向いているのは実装詳細
    Clean Architecture 達人に学ぶソフトウェアの構造と設計 Robert C. Martin p. 200

    View full-size slide

  9. 実装詳細レイヤーの責務を扱う構造で整理
    ※用語はDDD(Domain-Driven Design)より

    View full-size slide

  10. 各レイヤーの依存関係
    外側のインターフェースを元にコードの自動生成が可能になる

    View full-size slide

  11. 依存性逆転
    上位のレイヤーに下位のレイヤーが依存するという状態をどうやってつくるのか
    →上位のインターフェースを下位で実装する
    依存性逆転の原則 - Wikipedia https://ja.wikipedia.org/wiki/依存性逆転の原則

    View full-size slide

  12. いったんまとめ
    クリーンアーキテクチャのポイント
    ● 実装詳細と、ビジネスルールを分ける
    ● 依存方向を内向きに揃える
    実装詳細とビジネスルールのレイヤーを分けて、依存方向を整理した!
    →外部インターフェースの定義から、実装詳細が自動生成できそう!!

    View full-size slide

  13. 自動生成をつかって、はやくつくる

    View full-size slide

  14. どうつくるか?
    1. 全体で使えるツールを準備する
    2. スキーマファーストで外部インタフェースを整理
    3. アプリケーション層で↑の実装をつくる
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る
    6. DIする
    7. テストをなるべく生成する 作る順で説明します

    View full-size slide

  15. 1. 全体で使えるツールを準備する
    2. スキーマファーストで外部インタフェースを整理
    3. アプリケーション層で↑の実装をつくる
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る
    6. DIする
    7. テストをなるべく生成する

    View full-size slide

  16. 全体で使えるツール
    次の2つがキモだが、めんどう
    ● interfaceを作って、実装する(依存性逆転のために必要)
    ● 各レイヤで構造をマップする(データ構造によって暗に依存性を壊さない)

    View full-size slide

  17. impl:interfaceをみたすための関数群を自動生成
    https://github.com/josharian/impl
    依存性逆転のために各レイヤでinterface定義・実装を分けるとき便利
    $ impl 'i *QRCodeReposioryImpl' \
    github.com/juntaki/techbook-qrcode/src/domain.QRCodeRepository >> infra/qrcode.go
    func (i *QRCodeReposioryImpl) GetQRCodes(ctx context.Context) ([]*domain.QRCode, error) {
    panic("not implemented")
    }
    func (i *QRCodeReposioryImpl) IsExistQRCode(ctx context.Context, code *domain.QRCode) bool {
    panic("not implemented")
    }
    ...

    View full-size slide

  18. fillstruct:structを初期値で埋める
    https://github.com/davidrjenni/reftools/tree/master/cmd/fillstruct
    レイヤーを分けると値のマッピングが頻繁に必要

    View full-size slide

  19. 1. 全体で使えるツールを準備する
    2. スキーマファーストで外部インタフェースを整理
    3. アプリケーション層で↑の実装をつくる
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る
    6. DIする
    7. テストをなるべく生成する
    gRPC-Webをつかいます

    View full-size slide

  20. gRPC-Web でAPIエンドポイントを生成する
    Protocol buffersで外部インターフェースを定義して、自動生成する
    ● APIエンドポイントのハンドラ
    ● クライアントライブラリ

    View full-size slide

  21. protoファイル
    扱うべき構造とエンドポイントの
    Request/Responseを定義
    syntax = "proto3";
    package qrcode;
    message URL { string url = 1; }
    message QRCode {
    string id = 1;
    string url = 2;
    bytes image = 3;
    }
    message QRCodeList { repeated QRCode QRCodes = 1; }
    message Empty {}
    service QRCodeService {
    rpc GetURL(Empty) returns (URL) {}
    rpc UpdateURL(URL) returns (Empty) {}
    rpc GetQRCodes(Empty) returns (QRCodeList) {}
    rpc AddQRCodes(Empty) returns (Empty) {}
    }

    View full-size slide

  22. 自動生成のコマンドと生成されるもの一覧
    ● gRPCサーバのためのGoのinterface
    ● Goのクライアントライブラリ(今回は使わない)
    ● JavaScriptのクライアントライブラリ
    ● TypeScriptの型定義
    $ protoc \
    -I proto \
    --go_out=plugins=grpc:src/lib/qrcode \
    --plugin=protoc-gen-ts=front/node_modules/.bin/protoc-gen-ts \
    --js_out=import_style=commonjs,binary:./front/src/proto \
    --ts_out=service=true:./front/src/proto \
    proto/*.proto

    View full-size slide

  23. APIエンドポイントはinterfaceを実装するだけ
    impl+fillstructでダミーの値をかえすように実装しておく
    APIサーバとして成立するので、フロントエンドと結合できる
    // QRCodeServiceServer is the server API for QRCodeService service.
    type QRCodeServiceServer interface {
    GetURL(context.Context, *Empty) (*URL, error)
    UpdateURL(context.Context, *URL) (*Empty, error)
    GetQRCodes(context.Context, *Empty) (*QRCodeList, error)
    AddQRCodes(context.Context, *Empty) (*Empty, error)
    }

    View full-size slide

  24. gRPC-Webとしてマウントする
    grpcwebのMiddlewareを挟んであげる必要がある
    https://github.com/improbable-eng/grpc-web/tree/master/go/grpcweb
    s := grpc.NewServer()
    qs := application.NewQRCodeServiceServer()
    qrcode.RegisterQRCodeServiceServer(s, qs)
    // gRPC-Webのハンドラにする
    wrappedGrpc := http.StripPrefix("/grpc-web", grpcweb.WrapServer(s))

    View full-size slide

  25. 1. 全体で使えるツールを準備する
    2. スキーマファーストで外部インタフェースを整理
    3. アプリケーション層で↑の実装をつくる
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る
    6. DIする
    7. テストをなるべく生成する

    View full-size slide

  26. ドメイン層で決めること
    ● 実装詳細と関係ないビジネスルールのための構造を定義
    ● 永続化のためのインターフェース定義
    type QRCode struct {
    ID string
    Index int
    }
    func (q *QRCode) GetURL() string {
    ...
    }
    func (q *QRCode) GetQRCode() []byte {
    ...
    }
    type QRCodeRepository interface {
    GetQRCodes(ctx context.Context) ([]*QRCode, error)
    ...
    }
    これを利用して
    アプリケーション層が作れる

    View full-size slide

  27. インフラストラクチャ層のつくりかた
    テーブルの構造を決める→ORMでマップできるstructを生成する
    ドメイン層のリポジトリのinterfaceを満たすように実装していく

    View full-size slide

  28. xormでDBスキーマからモデルを生成する
    https://github.com/go-xorm/cmd
    xorm以外のORM用のモデルも、ある程度はテンプレートの工夫だけでつくれる
    ちょっと改造すれば何でもいける(例:gorp用→https://s.juntaki.com/hAU
    $ mysql -uroot -pmysql -h 127.0.0.1 < ddl.sql
    $ xorm reverse mysql "root:mysql@tcp(127.0.0.1:3306)/mydb" template
    package models
    type Qrcode struct {
    Id string `db:"id"`
    }
    CREATE TABLE IF NOT EXISTS `mydb`.`qrcode` (
    `id` CHAR(36) NOT NULL COMMENT 'ID',
    PRIMARY KEY (`id`))

    View full-size slide

  29. wire による Dependency Injection
    インフラストラクチャ層の実装を、アプリケーション層にDIする
    wire:Google が開発したDIのためのコードジェネレータ
    https://github.com/google/wire

    View full-size slide

  30. wireへの入力 //+build wireinject
    //+build wireinject
    package main
    ...
    func InitializeQRCodeServiceServer(db *gorp.DbMap) *application.QRCodeServiceServer {
    wire.Build(application.NewQRCodeServiceServer, infra.NewQRCodeRepositoryImpl)
    return &application.QRCodeServiceServer{}
    }
    ビルドタグがwireinjectなので、ビルド対象にならない
    →Goとしてみても一応問題ないので、コード補完などは効くようになる

    View full-size slide

  31. wireが生成するもの
    依存関係に応じた初期化コードが自動生成される
    // Code generated by Wire. DO NOT EDIT.
    //go:generate wire
    //+build !wireinject
    func InitializeQRCodeServiceServer(db *gorp.DbMap) *application.QRCodeServiceServer {
     ...
    }

    View full-size slide

  32. 1. 全体で使えるツールを準備する
    2. スキーマファーストで外部インタフェースを整理
    3. アプリケーション層で↑の実装をつくる
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る
    6. DIする
    7. テストをなるべく生成する

    View full-size slide

  33. gotestsでテストを生成する
    func TestQRCode_GetQRCode(t *testing.T) {
    type fields struct {
    ID string
    }
    tests := []struct {
    name string
    fields fields
    want []byte
    }{
    // TODO: Add test cases.
    }
    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    q := &QRCode{
    ID: tt.fields.ID,
    }
    if got := q.GetQRCode(); !reflect.DeepEqual(got, tt.want) {
    t.Errorf("QRCode.GetQRCode() = %v, want %v", got, tt.want)
    }
    })
    }
    }
    $ gotests -all -w domain/qrcode.go
    ここにテスト条件を書く

    View full-size slide

  34. fillstructと組み合わせる
    テストケース入力もちょっと楽になる

    View full-size slide

  35. juntaki/fixでGolden file testing
    https://github.com/juntaki/fix
    テストの正解を「前と変わらないこと」にする
    func TestQRCodeServiceServer_GetURL(t *testing.T) {
    ret, err := qrCodeServiceServer.GetQRCodes(ctx, &qrcode.Empty{})
    if err != nil {
    t.Fatal(err)
    }
    // 1回目の実行では ret をシリアライズして保存します。
    // 2回目からは同様にシリアライズして、保存されたファイルとの一致をチェックします。
    err = fix.Fix(ret)
    if err != nil {
    t.Fatal(err)
    }
    }

    View full-size slide

  36. まとめ
    1. 全体で使えるツールを準備する:impl, fillstruct
    2. スキーマファーストで外部インタフェースを整理:protoc
    3. アプリケーション層で↑の実装をつくる:grpcweb
    4. ドメイン層でリポジトリのインタフェースをつくる
    5. インフラストラクチャ層で↑の実装を作る:xorm
    6. DIする:wire
    7. テストをなるべく生成する:gotests, fix
    紹介したテクニックを活用してGoでAPI サーバをつくってみてください!!

    View full-size slide