GoでのAPIアプリケーションのアーキテクチャ実装を赤裸々に晒すものです。
© - BASE, Inc.GoでのAPI開発現場のアーキテクチャ実装事例2019.04.25 #突撃!!隣のアーキテクチャ - @hgsgtk
View Slide
© - BASE, Inc.このトークについて• Go⾔語で実装するAPIアプリケーション• 変更容易性‧実装の統⼀性‧テスタビリティを⽬指して試⾏錯誤した結果を晒す• 「XXXアーキテクチャやるぞ!」というスタートではない• Question‧Suggestionください
© - BASE, Inc.⾃⼰紹介東⼝和暉 (Kazuki Higashiguchi)Twitter / GitHub : @hgsgtkバックエンドエンジニアBASE BANK, Inc. / Dev Division
© - BASE, Inc.お品書き実装詳細アーキテクチャ‧実装概要まとめ
© - BASE, Inc.Layered“ish” Architecture+ ポート‧アダプタ実装したアーキテクチャSee also:ブログ 『Goを運⽤アプリケーションに導⼊する際のレイヤ構造模索の旅路 | Go ConferenceAutumn 発表レポート』https://devblog.thebase.in/entry/ / / /書籍『実践ドメイン駆動設計』https://www.shoeisha.co.jp/book/detail/
© - BASE, Inc.パッケージ構成※ パッケージ: 名前空間を分けるためのGoの仕組み
© - BASE, Inc.Domain Layer• Model‧Repository• Model• ビジネスロジック• Repository• Interfaceを定義
© - BASE, Inc.model packageの実装package modelimport "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}
© - BASE, Inc.model packageの実装package modelimport "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 を構造体として定義
© - BASE, Inc.repository packageの実装package repositoryimport ("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ܕ}
© - BASE, Inc.repository packageの実装package repositoryimport ("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/
© - BASE, Inc.Infrastructure Layer• datastore‧kvs etc• 「技術的実装」• RDBMS (MySQL)• KVS (Redis)• APIClient• etc• Repository interfaceを実装する
© - BASE, Inc.datastore package の実装package datastoreimport (// ུ)type UserStore struct {}func (*UserStore) GetByID(db repository.DBHandler, id int) (user model.User,err error) {q := `SELECTid, family_name, given_name, created, modifiedFROM usersWHERE 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}
© - BASE, Inc.datastore package の実装type UserStore struct {}func (*UserStore) GetByID(db repository.DBHandler, id int) (usermodel.User, err error) {q := `SELECTid, family_name, given_name, created, modifiedFROM usersWHERE 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を満たす実装を⾏う
© - BASE, Inc.Application Layer• service package• 制御役• ビジネスロジックを持たない
© - BASE, Inc.service package の実装package serviceimport (// লུ)type UserViewService interface {Run(input UserViewInput) (UserViewOutput, *Error)}type UserViewInput struct {ID int}type UserViewOutput struct {User model.User}type Error struct {Code stringMessage stringStatus intDetail map[string]string}
© - 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 stringMessage stringStatus intDetail map[string]string}service package の実装ひとつのServiceごとにInterface型を定義
© - BASE, Inc.service package の実装type UserViewServiceImpl struct {Conn repository.DBConnectorUser 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}
© - BASE, Inc.service package の実装type UserViewServiceImpl struct {Conn repository.DBConnectorUser 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 を満たす実装を⾏う
© - BASE, Inc.User Interface Layer• Controller‧Middleware• Controller• リクエストを処理し、• service を呼び出し、• レスポンスを作成するSee also:middlewareの概念についての参考資料https://book.cakephp.org/ . /en/controllers/middleware.html
© - BASE, Inc.controller package の実装package controllerimport (// ུ)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"`}
© - 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)}
© - 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を実⾏する
© - BASE, Inc.トランザクション制御• databaseを操作する際に⽤いる database/sqlpackage• See also: https://golang.org/pkg/database/sql/• *sql.DB: database handle a pool of connections• ⼀度Openしたら基本的に使い続ける• 起動時に⽤意した *sql.DB を使う
© - BASE, Inc.トランザクション制御• Application service 層で制御• トランザクション制御のための connection handlerを持つpackage servicetype UserAddServiceImpl struct {Conn repository.DBConnectorUser repository.UserRepository}
© - BASE, Inc.トランザクション制御• begin / commit / rollbackfunc (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 handlingtx.Rollback()}tx.Commit()return UserAddServiceOutput{}, nil}
© - BASE, Inc.Extra: github.com/jmoiron/sqlx• sqlx is a library which provides a set ofextensions on go’s standard database/sql library.• See also: https://github.com/jmoiron/sqlx• Merits• Marshal rows into structs
© - BASE, Inc.Test Double• 各レイヤ間では、 基本的にはInterface 型に依存• それぞれのレイヤでのユニットテストでは、Interface 型を実装した Test Doubleを作成し利⽤• Test Doubleの作成には github.com/golang/mockを使⽤• See also: https://github.com/golang/mock
© - BASE, Inc.Logging• 各構造体に持たせるかグローバルに定義したものを取り出すか• グローバルで取り出せるロガーを定義して使⽤する
© - BASE, Inc.Loggingpackage loggerimport (// লུ)// 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()}
© - BASE, Inc.Loggingpackage loggerimport (// লུ)// 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を初期化
© - BASE, Inc.Loggingpackage loggerimport (// লུ)// 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を取り出して利⽤する
© - BASE, Inc.Logging: detailspackage loggerimport (// লུ)func newLogger(writer zapcore.WriteSyncer) *zap.Logger {atom := zap.NewAtomicLevel()encoderCfg := zap.NewProductionEncoderConfig()encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoderbl := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg),zapcore.Lock(writer),atom,))l := bl.With(zap.String("out", "stdout"))return l}
© - BASE, Inc.BASEのプロダクトについてネットショップ作成サービス「BASE」ショッピングアプリ「BASE」価値の交換をよりシンプルにし、世界中の⼈々が最適な経済活動を⾏えるようにする。MISSION
© - BASE, Inc.
Fin.