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

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

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

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

Kazuki Higashiguchi
PRO

April 25, 2019
Tweet

More Decks by Kazuki Higashiguchi

Other Decks in Technology

Transcript

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

    View Slide

  2. © - BASE, Inc.
    このトークについて
    • Go⾔語で実装するAPIアプリケーション
    • 変更容易性‧実装の統⼀性‧テスタビリティを⽬指し
    て試⾏錯誤した結果を晒す
    • 「XXXアーキテクチャやるぞ!」というスタートでは
    ない
    • Question‧Suggestionください

    View Slide

  3. © - BASE, Inc.
    ⾃⼰紹介
    東⼝和暉 (Kazuki Higashiguchi)
    Twitter / GitHub : @hgsgtk
    バックエンドエンジニア
    BASE BANK, Inc. / Dev Division

    View Slide

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

    View Slide

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

    View Slide

  6. © - BASE, Inc.
    Layered“ish” Architecture
    + ポート‧アダプタ
    実装したアーキテクチャ
    See also:
    ブログ 『Goを運⽤アプリケーションに導⼊する際
    のレイヤ構造模索の旅路 | Go Conference
    Autumn 発表レポート』
    https://devblog.thebase.in/entry/ / / /
    書籍『実践ドメイン駆動設計』
    https://www.shoeisha.co.jp/book/detail/

    View Slide

  7. © - BASE, Inc.
    パッケージ構成
    ※ パッケージ: 名前空間を分けるためのGoの仕組み

    View Slide

  8. © - BASE, Inc.
    Domain Layer
    • Model‧Repository
    • Model
    • ビジネスロジック
    • Repository
    • Interfaceを定義

    View Slide

  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
    }

    View Slide

  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 を構造体として定義

    View Slide

  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ܕ
    }

    View Slide

  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/

    View Slide

  13. © - BASE, Inc.
    Infrastructure Layer
    • datastore‧kvs etc
    • 「技術的実装」
    • RDBMS (MySQL)
    • KVS (Redis)
    • APIClient
    • etc
    • Repository interfaceを
    実装する

    View Slide

  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
    }

    View Slide

  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を満たす実装を⾏う

    View Slide

  16. © - BASE, Inc.
    Application Layer
    • service package
    • 制御役
    • ビジネスロジックを持た
    ない

    View Slide

  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
    }

    View Slide

  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型を定義

    View Slide

  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
    }

    View Slide

  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 を満たす実装を⾏う

    View Slide

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

    View Slide

  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"`
    }

    View Slide

  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)
    }

    View Slide

  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を実⾏する

    View Slide

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

    View Slide

  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 を使う

    View Slide

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

    View Slide

  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
    }

    View Slide

  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

    View Slide

  30. © - BASE, Inc.
    Test Double
    • 各レイヤ間では、 基本的にはInterface 型に依存
    • それぞれのレイヤでのユニットテストでは、
    Interface 型を実装した Test Doubleを作成し利⽤
    • Test Doubleの作成には github.com/golang/mock
    を使⽤
    • See also: https://github.com/golang/mock

    View Slide

  31. © - BASE, Inc.
    Logging
    • 各構造体に持たせるかグローバルに定義したものを取
    り出すか
    • グローバルで取り出せるロガーを定義して使⽤する

    View Slide

  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()
    }

    View Slide

  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を初期化

    View Slide

  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を取り出して利⽤する

    View Slide

  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
    }

    View Slide

  36. © - BASE, Inc.
    BASEのプロダクトについて
    ネットショップ作成サービス
    「BASE」
    ショッピングアプリ
    「BASE」
    価値の交換をよりシンプルにし、
    世界中の⼈々が最適な経済活動を⾏えるようにする。
    MISSION

    View Slide

  37. © - BASE, Inc.

    View Slide

  38. Fin.

    View Slide