Echo(Golang web framework)のディレクトリ構成の参考情報 - OSS「echo-sample」のコードリーディング
TL;DR
echo-sample
は、こんな構成だった
きっかけ
業務で、Golang
とそのWebフレームワークのEcho
で、APIサーバーを作る機会があったのですが、初めて触るのでディレクトリ構成で悩みました。
必要なのは、Clean Architectureなどでガッツリ分離する必要も無いくらいのシンプルな機能のみなので、OSSで程よい構成のお手本を探しました。
いくつかEchoを利用したリポジトリがある中で、echo-sample
が簡潔な構成で参考になりそうでした。
そこで、それぞれのファイルがどういった役割を担っているのか調査したので、結果をまとめました。
echo-sampleの概要
名前の通り、Echoを用いたアプリケーションのサンプルです。
POST
で新しいメンバーの登録、GET
でメンバーのデータを取得(個別or全件)出来るという非常に簡素なAPIです。
ディレクトリ構成
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
、.gitignore
とLICENSE
は自明なので飛ばします
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{})
}
route
echo-sample/route/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}, }))
また後述する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/middleware/transaction.go at master · eurie-inc/echo-sample · GitHub
役割
- DBのトランザクション 制御
詳細
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
というパッケージを利用しています。
handler
echo-sample/handler/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, }) } }
db
echo-sample/db/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/conf/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/api/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/model/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
- 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/db/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 );