GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example

GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example

GoでのAPIアプリケーションのアーキテクチャ実装を赤裸々に晒すものです。

88964b936e864ca7d326272eaa70fa9a?s=128

Kazuki Higashiguchi

April 25, 2019
Tweet

Transcript

  1. © - BASE, Inc. GoでのAPI開発現場の アーキテクチャ実装事例 2019.04.25 #突撃!!隣のアーキテクチャ - @hgsgtk

  2. © - BASE, Inc. このトークについて • Go⾔語で実装するAPIアプリケーション • 変更容易性‧実装の統⼀性‧テスタビリティを⽬指し て試⾏錯誤した結果を晒す

    • 「XXXアーキテクチャやるぞ!」というスタートでは ない • Question‧Suggestionください
  3. © - BASE, Inc. ⾃⼰紹介 東⼝和暉 (Kazuki Higashiguchi) Twitter /

    GitHub : @hgsgtk バックエンドエンジニア BASE BANK, Inc. / Dev Division
  4. © - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

  5. © - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

  6. © - BASE, Inc. Layered“ish” Architecture + ポート‧アダプタ 実装したアーキテクチャ See

    also: ブログ 『Goを運⽤アプリケーションに導⼊する際 のレイヤ構造模索の旅路 | Go Conference Autumn 発表レポート』 https://devblog.thebase.in/entry/ / / / 書籍『実践ドメイン駆動設計』 https://www.shoeisha.co.jp/book/detail/
  7. © - BASE, Inc. パッケージ構成 ※ パッケージ: 名前空間を分けるためのGoの仕組み

  8. © - BASE, Inc. Domain Layer • Model‧Repository • Model

    • ビジネスロジック • Repository • Interfaceを定義
  9. © - BASE, Inc. model packageの実装 package model import "time"

    type User struct { ID int db:"id"` FamilyName string db:"family_name" validate:"require,max=50"` GivenName string db:"given_name" validate:"require,max=50"` Created time.Time db:"created"` Modified time.Time db:"modified"` } func (u *User) GetFullName() string { return u.FamilyName + " " + u.GivenName }
  10. © - BASE, Inc. model packageの実装 package model import "time"

    type User struct { ID int db:"id"` FamilyName string db:"family_name" validate:"require,max=50"` GivenName string db:"given_name" validate:"require,max=50"` Created time.Time db:"created"` Modified time.Time db:"modified"` } func (u *User) GetFullName() string { return u.FamilyName + " " + u.GivenName } User model を構造体として定義
  11. © - BASE, Inc. repository packageの実装 package repository import (

    "database/sql" "github.com/jmoiron/sqlx" ) type UserRepository interface { GetByID(db DBHandler, id int) (model.User, error) } // *sqlx.DB ߏ଄ମͷϨγʔόϝιουͷ͏ͪ࢖༻͢Δ΋ͷΛఆٛ type DBHandler interface { Exec // insert/update/delete ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Query // select ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Prepare // prepare ͷࡍʹ༻͍Δϝιουͷinterfaceܕ }
  12. © - BASE, Inc. repository packageの実装 package repository import (

    "database/sql" "github.com/jmoiron/sqlx" ) type UserRepository interface { GetByID(db DBHandler, id int) (model.User, error) } // *sqlx.DB ߏ଄ମͷϨγʔόϝιουͷ͏ͪ࢖༻͢Δ΋ͷΛఆٛ type DBHandler interface { Exec // insert/update/delete ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Query // select ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Prepare // prepare ͷࡍʹ༻͍Δϝιουͷinterfaceܕ } interface型を定義 See also: https://tour.golang.org/methods/
  13. © - BASE, Inc. Infrastructure Layer • datastore‧kvs etc •

    「技術的実装」 • RDBMS (MySQL) • KVS (Redis) • APIClient • etc • Repository interfaceを 実装する
  14. © - BASE, Inc. datastore package の実装 package datastore import

    ( // ུ ) type UserStore struct { } func (*UserStore) GetByID(db repository.DBHandler, id int) (user model.User, err error) { q := ` SELECT id, family_name, given_name, created, modified FROM users WHERE id = ?` st, err := db.Preparex(q) if err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } if err := st.QueryRowx(id).StructScan(&user); err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } return }
  15. © - BASE, Inc. datastore package の実装 type UserStore struct

    { } func (*UserStore) GetByID(db repository.DBHandler, id int) (user model.User, err error) { q := ` SELECT id, family_name, given_name, created, modified FROM users WHERE id = ?` st, err := db.Preparex(q) if err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } if err := st.QueryRowx(id).StructScan(&user); err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } return } UserRepository interfaceを満たす実装を⾏う
  16. © - BASE, Inc. Application Layer • service package •

    制御役 • ビジネスロジックを持た ない
  17. © - BASE, Inc. service package の実装 package service import

    ( // লུ ) type UserViewService interface { Run(input UserViewInput) (UserViewOutput, *Error) } type UserViewInput struct { ID int } type UserViewOutput struct { User model.User } type Error struct { Code string Message string Status int Detail map[string]string }
  18. © - BASE, Inc. type UserViewService interface { Run(input UserViewInput)

    (UserViewOutput, *Error) } type UserViewInput struct { ID int } type UserViewOutput struct { User model.User } type Error struct { Code string Message string Status int Detail map[string]string } service package の実装 ひとつのServiceごとにInterface型を定義
  19. © - BASE, Inc. service package の実装 type UserViewServiceImpl struct

    { Conn repository.DBConnector User repository.UserRepository } func NewUserViewService(conn repository.DBConnector) *UserViewServiceImpl { return &UserViewServiceImpl{ Conn: conn, User: &datastore.UserStore{}, } } func (s *UserViewServiceImpl) Run(input UserViewInput) (UserViewOutput, *Error) { user, err := s.User.GetByID(s.Conn, input.ID) if err != nil { return UserViewOutput{}, &Error{ Code: "internal_server_error", Message: "Internal Server Error", Status: http.StatusInternalServerError, } } return UserViewOutput{User: user}, nil }
  20. © - BASE, Inc. service package の実装 type UserViewServiceImpl struct

    { Conn repository.DBConnector User repository.UserRepository } func NewUserViewService(conn repository.DBConnector) *UserViewServiceImpl { return &UserViewServiceImpl{ Conn: conn, User: &datastore.UserStore{}, } } func (s *UserViewServiceImpl) Run(input UserViewInput) (UserViewOutput, *Error) { user, err := s.User.GetByID(s.Conn, input.ID) if err != nil { return UserViewOutput{}, &Error{ Code: "internal_server_error", Message: "Internal Server Error", Status: http.StatusInternalServerError, } } return UserViewOutput{User: user}, nil } UserViewService interface を満たす実装を⾏う
  21. © - BASE, Inc. User Interface Layer • Controller‧Middleware •

    Controller • リクエストを処理し、 • service を呼び出し、 • レスポンスを作成する See also: middlewareの概念についての参考資料 https://book.cakephp.org/ . /en/controllers/ middleware.html
  22. © - BASE, Inc. controller package の実装 package controller import

    ( // ུ ) type UserViewController struct { Service service.UserViewService } type UserViewRequest struct { ID int `json:"id"` } type UserViewResponse struct { ID int `json:"id"` FullName string `json:"full_name"` }
  23. © - BASE, Inc. controller package の実装 func (c *UserViewController)

    Handler(w http.ResponseWriter, r *http.Request) { rb := UserViewRequest{} if err := decodeRequestBodyJSON(r, &rb); err != nil { respondError(w, err) return } output, err := c.Service.Run(service.UserViewInput{ID: rb.ID}) if err != nil { // respond error } rs := UserViewResponse{ ID: output.User.ID, FullName: output.User.GetFullName(), } respond(w, rs, http.StatusOK) }
  24. © - BASE, Inc. controller package の実装 func (c *UserViewController)

    Handler(w http.ResponseWriter, r *http.Request) { rb := UserViewRequest{} if err := decodeRequestBodyJSON(r, &rb); err != nil { respondError(w, err) return } output, err := c.Service.Run(service.UserViewInput{ID: rb.ID}) if err != nil { // respond error } rs := UserViewResponse{ ID: output.User.ID, FullName: output.User.GetFullName(), } respond(w, rs, http.StatusOK) } serviceを実⾏する
  25. © - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

  26. © - BASE, Inc. トランザクション制御 • databaseを操作する際に⽤いる database/sql package •

    See also: https://golang.org/pkg/database/sql/ • *sql.DB: database handle a pool of connections • ⼀度Openしたら基本的に使い続ける • 起動時に⽤意した *sql.DB を使う
  27. © - BASE, Inc. トランザクション制御 • Application service 層で制御 •

    トランザクション制御のための connection handler を持つ package service type UserAddServiceImpl struct { Conn repository.DBConnector User repository.UserRepository }
  28. © - BASE, Inc. トランザクション制御 • begin / commit /

    rollback func (s *UserAddServiceImpl) Run(input UserAddServiceInput) (UserAddServiceOutput, *Error) { tx, err := s.Conn.Beginx() if err != nil { // error handling } u := model.User{ FamilyName: input.FamilyName, GivenName: input.GivenName, } if err := s.User.Save(tx, u); err != nil { // error handling tx.Rollback() } tx.Commit() return UserAddServiceOutput{}, nil }
  29. © - BASE, Inc. Extra: github.com/jmoiron/sqlx • sqlx is a

    library which provides a set of extensions on go’s standard database/sql library. • See also: https://github.com/jmoiron/sqlx • Merits • Marshal rows into structs
  30. © - BASE, Inc. Test Double • 各レイヤ間では、 基本的にはInterface 型に依存

    • それぞれのレイヤでのユニットテストでは、 Interface 型を実装した Test Doubleを作成し利⽤ • Test Doubleの作成には github.com/golang/mock を使⽤ • See also: https://github.com/golang/mock
  31. © - BASE, Inc. Logging • 各構造体に持たせるかグローバルに定義したものを取 り出すか • グローバルで取り出せるロガーを定義して使⽤する

  32. © - BASE, Inc. Logging package logger import ( //

    লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() }
  33. © - BASE, Inc. Logging package logger import ( //

    লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } 起動時に、グローバルで取得するLoggerを初期化
  34. © - BASE, Inc. Logging package logger import ( //

    লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } Logger() によって、 グローバルLoggerを取り出して利⽤する
  35. © - BASE, Inc. Logging: details package logger import (

    // লུ ) func newLogger(writer zapcore.WriteSyncer) *zap.Logger { atom := zap.NewAtomicLevel() encoderCfg := zap.NewProductionEncoderConfig() encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder bl := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(encoderCfg), zapcore.Lock(writer), atom, )) l := bl.With(zap.String("out", "stdout")) return l }
  36. © - BASE, Inc. BASEのプロダクトについて ネットショップ作成サービス 「BASE」 ショッピングアプリ 「BASE」 価値の交換をよりシンプルにし、

    世界中の⼈々が最適な経済活動を⾏えるようにする。 MISSION
  37. © - BASE, Inc.

  38. Fin.