Echo(Golang web framework)のディレクトリ構成の参考情報 - OSS「echo-sample」のコードリーディング

TL;DR

echo-sampleは、こんな構成だった スクリーンショット 2020-01-13 18.34.23.png (199.7 kB)

きっかけ

業務で、GolangとそのWebフレームワークのEchoで、APIサーバーを作る機会があったのですが、初めて触るのでディレクトリ構成で悩みました。
必要なのは、Clean Architectureなどでガッツリ分離する必要も無いくらいのシンプルな機能のみなので、OSSで程よい構成のお手本を探しました。

いくつかEchoを利用したリポジトリがある中で、echo-sampleが簡潔な構成で参考になりそうでした。
そこで、それぞれのファイルがどういった役割を担っているのか調査したので、結果をまとめました。

echo-sampleの概要

名前の通り、Echoを用いたアプリケーションのサンプルです。
POSTで新しいメンバーの登録、GETでメンバーのデータを取得(個別or全件)出来るという非常に簡素なAPIです。

github.com

ディレクトリ構成

echo-sample 
    ├── .gitignore
    ├── LICENSE
    ├── glide.lock
    ├── glide.yaml
    │ 
    ├── main.go ・ ・ ・ 「①サーバー起動」
    │ 
    ├── api
    │    └── member.go ・ ・ ・ 「③レスポンスの制御」
    │ 
    ├── conf
    │    └── config.go  ・ ・ ・ 「④'DBの認証情報を定義」
    │ 
    ├── db
    │    ├── ddl.sql ・ ・ ・ 「⓪DBの作成」
    │    └── mysql.go ・ ・ ・ 「③'DBの設定」
    │ 
    ├── handler
    │    └── handler.go ・ ・ ・ 「③'''エラーハンドリング」
    │ 
    ├── middleware
    │    └── transaction.go ・ ・ ・ 「③’'DBの起動・トランザクション制御」
    │ 
    ├── model
    │    └── member.go ・ ・ ・ 「④ビジネスロジックの実装」
    │ 
    ├── route
    │    └── router.go ・ ・ ・ 「②ルーティング設定」
    │ 
    └── vendor
         ├── github.com
         │   └── ...
         └── golang.org/x
             └── ...

個々の構成毎の役割

パッケージ管理のglideや外部ライブラリの置き場所のvendor.gitignoreLICENSEは自明なので飛ばします

main.go

echo-sample/main.go at master · eurie-inc/echo-sample · GitHub

役割

  • サーバー起動
    • logrusの読み込み及び設定

詳細

全ての起点となる/main.goですが、最低限の内容に留めていました。
サーバー起動を行っていますが、echoインスタンスの生成は、後述するrouteで行っています。

func main() {

    router := route.Init()
    router.Run(fasthttp.New(":8888"))
}

また、ログ出力に標準logではなく、外部パッケージのlogrusを利用しており、それの読み込み/設定も行っています。
標準logより高機能で良いらしいです。

func init() {

    logrus.SetLevel(logrus.DebugLevel)
    logrus.SetFormatter(&logrus.JSONFormatter{})
}

qiita.com

route

echo-sample/router.go at master · eurie-inc/echo-sample · GitHub

役割

詳細

先ほど説明した通り、ここではechoインスタンスの生成を行っています。

func Init() *echo.Echo {

    e := echo.New()

        // 省略

}

インスタンス生成の中で、各種ミドルウェアの読み込みと、ルーティングの設定を行っています。

ミドルウェアについては、echo標準のものをいくつか読み込んでいます。

   // Set Bundle MiddleWare
    e.Use(echoMw.Logger())
    e.Use(echoMw.Gzip())
    e.Use(echoMw.CORSWithConfig(echoMw.CORSConfig{
        AllowOrigins: []string{"*"},
        AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAcceptEncoding},
    }))

echo.labstack.com

echo.labstack.com

また後述するmiddlewareで定義したミドルウェアも読み込んでいます。
その中で、後述するdbを呼び出して、DB接続も行っています。

   // Set Custom MiddleWare
    e.Use(myMw.TransactionHandler(db.Init()))

また、エラーハンドリングを行う為に、後述するhandlerの設定も行っています。

   e.SetHTTPErrorHandler(handler.JSONHTTPErrorHandler)

ルーティングでは、バージョン管理が容易に出来るように、Groupでパスを束ねていました。
なお、実際の処理は、後述するapiで実装しているようです。

   v1 := e.Group("/api/v1")
    {
        v1.POST("/members", api.PostMember())
        v1.GET("/members", api.GetMembers())
        v1.GET("/members/:id", api.GetMember())
    }

middleware

echo-sample/transaction.go at master · eurie-inc/echo-sample · GitHub

役割

詳細

transaction.goという名前の通り、DBのトランザクション 制御を請負っているようです。

const (
    TxKey = "Tx"
)

func TransactionHandler(db *dbr.Session) echo.MiddlewareFunc {

    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return echo.HandlerFunc(func(c echo.Context) error {

            tx, _ := db.Begin()

            c.Set(TxKey, tx)

            if err := next(c); err != nil {
                tx.Rollback()
                logrus.Debug("Transction Rollback: ", err)
                return err
            }
            logrus.Debug("Transaction Commit")
            tx.Commit()

            return nil
        })
    }
}

DBの操作にはdbrというパッケージを利用しています。

godoc.org

handler

echo-sample/handler.go at master · eurie-inc/echo-sample · GitHub

役割

  • HTTPのエラーハンドリング

詳細

こちらでHTTPのエラーハンドリングを行っている様です。
なお、echo.Contextは、現在のHTTPリクエストのコンテキストを表します。

func JSONHTTPErrorHandler(err error, c echo.Context) {
    code := fasthttp.StatusInternalServerError
    msg := "Internal Server Error"
    if he, ok := err.(*echo.HTTPError); ok {
        code = he.Code
        msg = he.Message
    }
    if !c.Response().Committed() {
        c.JSON(code, map[string]interface{}{
            "statusCode": code,
            "message":    msg,
        })
    }
}

echo.labstack.com

db

echo-sample/mysql.go at master · eurie-inc/echo-sample · GitHub

役割

  • DBへの接続

詳細

ここではデータベースへの接続を行っています。

func Init() *dbr.Session {

    session := getSession()

    return session
}

func getSession() *dbr.Session {

    db, err := dbr.Open("mysql",
        conf.USER+":"+conf.PASSWORD+"@tcp("+conf.HOST+":"+conf.PORT+")/"+conf.DB,
        nil)

    if err != nil {
        logrus.Error(err)
    } else {
        session := db.NewSession(nil)
        return session
    }
    return nil
}

ここでもdbrというパッケージを利用しています。 なお、認証情報については後述するconfから読み出しています。

conf

echo-sample/config.go at master · eurie-inc/echo-sample · GitHub

役割

  • DBの認証情報やサーバーの接続情報の定義

詳細

ここでは DBの認証情報やサーバーの接続情報の定義しています。
定数は全てここで管理している様です。

const (
    USER     string = "root"
    PASSWORD string = "mysql01"
    DB       string = "sample"
    HOST     string = "192.168.99.100"
    PORT     string = "32769"
)

api

echo-sample/member.go at master · eurie-inc/echo-sample · GitHub

役割

  • レスポンスのフォーマットを指定
  • modelのエラーハンドリング

詳細

apiでは、modelの処理が正常に完了すれば、その処理結果をJSONとして返します。
また、処理がエラーになった場合は、HTTPエラーを返します。
実際のデータの取得や更新についてはmodelで行っているようです。

func PostMember() echo.HandlerFunc {
    return func(c echo.Context) (err error) {

        m := new(model.Member)
        c.Bind(&m)

        tx := c.Get("Tx").(*dbr.Tx)

        member := model.NewMember(m.Number, m.Name, m.Position)

        if err := member.Save(tx); err != nil {
            logrus.Debug(err)
            return echo.NewHTTPError(fasthttp.StatusInternalServerError)
        }
        return c.JSON(fasthttp.StatusCreated, member)
    }
}

func GetMember() echo.HandlerFunc {
    return func(c echo.Context) (err error) {

        number, _ := strconv.ParseInt(c.Param("id"), 0, 64)

        tx := c.Get("Tx").(*dbr.Tx)

        member := new(model.Member)
        if err := member.Load(tx, number); err != nil {
            logrus.Debug(err)
            return echo.NewHTTPError(fasthttp.StatusNotFound, "Member does not exists.")
        }
        return c.JSON(fasthttp.StatusOK, member)
    }
}

func GetMembers() echo.HandlerFunc {
    return func(c echo.Context) (err error) {
        tx := c.Get("Tx").(*dbr.Tx)

        position := c.QueryParam("position")
        members := new(model.Members)
        if err = members.Load(tx, position); err != nil {
            logrus.Debug(err)
            return echo.NewHTTPError(fasthttp.StatusNotFound, "Member does not exists.")
        }

        return c.JSON(fasthttp.StatusOK, members)
    }

model

echo-sample/member.go at master · eurie-inc/echo-sample · GitHub

役割

  • DBの操作

詳細

/modelは大きく分けると、以下の3つで構成されています。

  • JSONでデータを取り扱う為の構造体
// メンバー(個別)
type Member struct {
    Number    int64  `json:"number"`
    Name      string `json:"name"`
    Position  string `json:"position"`
    CreatedAt int64  `json:"createdAt"`
}
................................................
// メンバー(全件)
type Members []Member
// メンバー保存
func NewMember(member int64, name, position string) *Member {
    return &Member{
        Number:    member,
        Name:      name,
        Position:  position,
        CreatedAt: time.Now().Unix(),
    }
}
  • メンバー取得・保存用の関数
// メンバー保存
func (m *Member) Save(tx *dbr.Tx) error {

    _, err := tx.InsertInto("member").
        Columns("number", "name", "position", "created_at").
        Record(m).
        Exec()

    return err
}

// メンバー取得(個別)
func (m *Member) Load(tx *dbr.Tx, number int64) error {

    return tx.Select("*").
        From("member").
        Where("number = ?", number).
        LoadStruct(m)
}

// メンバー取得(全件)
func (m *Members) Load(tx *dbr.Tx, position string) error {

    var condition dbr.Condition
    if position != "" {
        condition = dbr.Eq("position", position)
    }

    return tx.Select("*").
        From("member").
        Where(condition).
        LoadStruct(m)
}

メンバー取得・保存用の関数では、dbrという外部ライブラリを用いて、dbへアクセスしています。

なお、完全に余談ですが、JSON形式のテキストを、Goの構造体の書式に変換してくれるツールがありました。 mholt.github.io

/db/ddl.sql

echo-sample/ddl.sql at master · eurie-inc/echo-sample · GitHub

役割

  • DBの作成

詳細

最後に残ったddl.sqlは、データベース作成用のSQLが記述されています。

CREATE DATABASE IF NOT EXISTS sample;
USE sample;
DROP TABLE IF EXISTS member;
CREATE TABLE IF NOT EXISTS member (
  number    INT PRIMARY KEY,
  name      VARCHAR(255) NOT NULL,
  position  CHAR(2) NOT NULL,
  created_at BIGINT       NOT NULL
);

参考リンク

github.com

echo.labstack.com