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

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

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

De059f612e2f68589831e4bde0f15c83?s=128

Jumpei Takiyasu

May 18, 2019
Tweet

Transcript

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

  2. name <- "Jumpei Takiyasu" twitter <- "juntaki" company <- "M3,

    Inc." title <- "Software Engineer/TL/PM" website <- "https://juntaki.com" Me
  3. きょうの話 保守性の高いWebアプリを早く作るにはどうしたらよいか 1. アーキテクチャをかんがえる 2. 自動生成をつかって、はやくつくる

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

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

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

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

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

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

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

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

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

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

  14. どうつくるか? 1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5.

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

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

  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") } ...
  18. fillstruct:structを初期値で埋める https://github.com/davidrjenni/reftools/tree/master/cmd/fillstruct レイヤーを分けると値のマッピングが頻繁に必要

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

    6. DIする 7. テストをなるべく生成する gRPC-Webをつかいます
  20. gRPC-Web でAPIエンドポイントを生成する Protocol buffersで外部インターフェースを定義して、自動生成する • APIエンドポイントのハンドラ • クライアントライブラリ

  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) {} }
  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
  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) }
  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))
  25. 1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る

    6. DIする 7. テストをなるべく生成する
  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) ... } これを利用して アプリケーション層が作れる
  27. インフラストラクチャ層のつくりかた テーブルの構造を決める→ORMでマップできるstructを生成する ドメイン層のリポジトリのinterfaceを満たすように実装していく

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

  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としてみても一応問題ないので、コード補完などは効くようになる
  31. wireが生成するもの 依存関係に応じた初期化コードが自動生成される // Code generated by Wire. DO NOT EDIT.

    //go:generate wire //+build !wireinject func InitializeQRCodeServiceServer(db *gorp.DbMap) *application.QRCodeServiceServer {  ... }
  32. 1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る

    6. DIする 7. テストをなるべく生成する
  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 ここにテスト条件を書く
  34. fillstructと組み合わせる テストケース入力もちょっと楽になる

  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) } }
  36. まとめ 1. 全体で使えるツールを準備する:impl, fillstruct 2. スキーマファーストで外部インタフェースを整理:protoc 3. アプリケーション層で↑の実装をつくる:grpcweb 4. ドメイン層でリポジトリのインタフェースをつくる

    5. インフラストラクチャ層で↑の実装を作る:xorm 6. DIする:wire 7. テストをなるべく生成する:gotests, fix 紹介したテクニックを活用してGoでAPI サーバをつくってみてください!!