Goの軽量Webアプリケーションフレームワーク(Echo, Gin)のディレクトリ構成
Golangの軽量Webアプリケーションフレームワークである、EchoやGinを使うとRuby on Railsのようにディレクトリ構成が特に決まっていないため、どのようにすれば効率的に開発できるのか悩みます。
しかし、最近いくつかGinやEchoを使ってAPIサーバを作成し、なんとなくディレクトリ構成が決まってきたので、共有します。
以下のリポジトリは後述する自動リポジトリ生成コマンドで生成されるプロジェクトの雛形です。
これについて説明をしていきます。
まずディレクトリを木構造で表示すると以下のようになります。
├── README.md
├── config
│ ├── config.go
│ └── environments
│ ├── development.yml
│ └── test.yml
├── controllers
│ ├── common.go
│ ├── health_controller.go
│ └── health_controller_test.go
├── database
│ ├── database.go
│ └── db
├── dockerfiles
│ ├── prod
│ │ └── Dockerfile
│ └── test
│ └── Dockerfile
├── forms
├── main.go
├── middleware
├── models
├── server
│ ├── router.go
│ └── server.go
└── views
main.go
package main
import (
"flag"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"{{ .ProjectPath }}/config"
"{{ .ProjectPath }}/database"
"{{ .ProjectPath }}/server"
)
func main() {
env := flag.String("e", "development", "")
flag.Parse()
config.Init(*env)
database.Init(false)
defer database.Close()
if err := server.Init(); err != nil {
panic(err)
}
}
まずmain.go
ですが、これは非常にシンプルになっています。gormというORMを利用しているため、このライブラリで利用するデータベースドライバをインポートしています。
コマンドの引数はe
のみとなっています。ここにdevelopment
やproduction
を設定するとconfig/environments
以下の対応する設定ファイルが読み込まれる仕組みになっています。
そのあとでconfig
とdatabase
、server
の初期化を行います。これらについては後述します。
config
次にconfig
です。
config.go
package config
import (
"github.com/spf13/viper"
)
var c *viper.Viper
// Init initializes config
func Init(env string) {
c = viper.New()
c.SetConfigFile("yaml")
c.SetConfigName(env)
c.AddConfigPath("config/environments/")
c.AddConfigPath("/run/secrets/")
if err := c.ReadInConfig(); err != nil {
panic(err)
}
}
// GetConfig returns config
func GetConfig() *viper.Viper {
return c
}
viperというライブラリを用いてファイルから設定を読み込みます。
ローカルの場合はconfig/environments/
以下の引数e
に対応するyamlファイルを読み込みます。
Dockerを利用する場合はsecret
を利用しコンテナの/run/secrets/
以下に設定ファイルをマッピングします。
config
はシングルトンで実装しGetConfig
で至るところから読み出せるようにします。
controllers
次にMVCのCの部分のコントローラです。
health_controller.go
package controllers
import (
"net/http"
"github.com/labstack/echo"
)
// HealthController controller for health request
type HealthController struct{}
// NewHealthController is constructer for HealthController
func NewHealthController() *HealthController {
return new(HealthController)
}
// Index is index route for health
func (hc *HealthController) Index(c echo.Context) error {
return c.JSON(http.StatusOK, newResponse(
http.StatusOK,
http.StatusText(http.StatusOK),
"OK",
))
}
controllers
ではGinやEchoなどのハンドラを定義します。そのため後述するserver/router.go
でインスタンス化されフレームワークの各ルートと各メソッドがマッピングされます。
データベースのUser
、Article
などそれぞれのリソースごとにuser_controller.go
、article_controller.go
と構造体ごとにファイルを作ります。そして構造体のメソッドにハンドラを定義します。
ハンドラは比較的自由ですが私はIndex
,Get
,Create
,Update
,Delete
などを作成します。1つのハンドラが大きくなりすぎるならば分けてもいいと思います。
返り値はcommon.go
で定義された以下の構造体を利用します。
type response struct {
Status int `json:"status"`
Message string `json:"message"`
Result interface{} `json:"result"`
}
func newResponse(status int, message string, result interface{}) *response {
return &response{status, message, result}
}
Status
にはステータスコードをMessage
にはメッセージを(http.StatusText()
の文字列をよく使う)入れ、Result
にリソースを代入します。
リスポンスを統一することでフロントエンドから扱いやすくなります。
テストはuser_controller_test.go
のように同じディレクトリに作成します。
database
データベースを管理するモジュールはdatabaseディレクトリに作成します。
database.go
package database
import (
"github.com/jinzhu/gorm"
"{{ .ProjectPath }}/config"
)
var d *gorm.DB
// Init initializes database
func Init(isReset bool, models ...interface{}) {
c := config.GetConfig()
var err error
d, err = gorm.Open(c.GetString("db.provider"), c.GetString("db.url"))
if err != nil {
panic(err)
}
if isReset {
d.DropTableIfExists(models)
}
d.AutoMigrate(models...)
}
// GetDB returns database connection
func GetDB() *gorm.DB {
return d
}
// Close closes database
func Close() {
d.Close()
}
こちらもconfig
同様シングルトンで定義しています。ドキュメント志向データベースなどのRDB以外を利用する場合はこのままでは利用できませんが抽象化されているためこのモジュールを書き換えることで対応できます。
dockerfiles
Dockerfileはこの中に入れましょう。ディレクトリごとに本番環境用やテスト環境用としてDockerfileを分けます。
各Dockerfileは以下のとおりです。
prod
FROM golang:alpine as build-env
ARG GITHUB_ACCESS_TOKEN
RUN apk add --no-cache git gcc musl-dev
RUN git config --global url."https://${GITHUB_ACCESS_TOKEN}:x-oauth-basic@github.com/".insteadOf "https://github.com/"
RUN go get {{ .ProjectPath }}
WORKDIR /go/src/{{ .ProjectPath }}
RUN go build -o /usr/bin/app
FROM alpine
COPY --from=build-env /usr/bin/app /usr/bin/app
マルチステージビルドでイメージの容量を削減します。
test
FROM golang:alpine
RUN apk add --no-cache git gcc musl-dev
ADD . /go/src/{{ .ProjectPath }}
WORKDIR /go/src/{{ .ProjectPath }}
RUN go get
forms
ここではPOSTのBodyなどを構造体で定義します。モデルにそのままマッピングしても良いのですが、モデルとの間にはさむことで処理を抽象化できます。フォームのメソッドでモデルのメソッドを呼び出し間接的にデータベースを操作します。
認可もここで行います。
user_form.go
// UserForm is form for user
type UserForm struct {
Name string `json:"name"`
Description string `json:"description"`
}
// Update updates a user with UserForm
func (uf *UserForm) Update(id uint, idToken *auth.Token) (ret *models.User, err error) {
user := new(models.User)
err = user.FindByID(id)
if err != nil {
return nil, err
}
// authorization
if idToken.UID != user.UID {
return nil, errors.New("Unauthorized")
}
user.Name = uf.Name
user.Description = uf.Description
err = user.Update()
return user, err
}
middleware
ここではそれぞれのフレームワークで利用できるミドルウェアを定義します。
例えば認証用のミドルウェアを定義します。
以下はfirebase authの認証を行うミドルウェア。
firebase.go
package middleware
import (
"context"
"log"
"net/http"
"strings"
firebase "firebase.google.com/go"
"firebase.google.com/go/auth"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"google.golang.org/api/option"
)
const valName = "FIREBASE_ID_TOKEN"
// FirebaseAuthMiddleware contains methods verifying JWT token
type FirebaseAuthMiddleware struct {
fbase *firebase.App
skipper middleware.Skipper
}
// NewFireBaseAuthMiddleware is middleware authentication with firebase
func NewFireBaseAuthMiddleware(credFilePath string, skipper middleware.Skipper) (*FirebaseAuthMiddleware, error) {
opt := option.WithCredentialsFile(credFilePath)
app, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
return nil, err
}
if skipper == nil {
skipper = middleware.DefaultSkipper
}
return &FirebaseAuthMiddleware{
fbase: app,
skipper: skipper,
}, nil
}
// Verify verifies token
func (f *FirebaseAuthMiddleware) Verify(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if f.skipper(c) {
return next(c)
}
r := c.Request()
token := strings.Replace(r.Header.Get(echo.HeaderAuthorization), "Bearer ", "", 1)
if token == "" {
return c.String(http.StatusUnauthorized, "Bad token")
}
client, err := f.fbase.Auth(context.Background())
if err != nil {
log.Println(err)
return c.String(http.StatusUnauthorized, "Bad token")
}
authToken, err := client.VerifyIDToken(context.Background(), token)
if err != nil {
log.Println(err)
return c.String(http.StatusUnauthorized, "Bad token")
}
c.Set(valName, authToken)
return next(c)
}
}
// ExtractClaims extracts claims
func ExtractClaims(c echo.Context) *auth.Token {
idToken := c.Get(valName)
if idToken == nil {
return new(auth.Token)
}
return idToken.(*auth.Token)
}
models
MVCのM、models
ではデータベースの操作を行います。
以下のようにデータベースのモデル定義を構造体として定義し、メソッドはデータベースを操作するものやcontrollers
やforms
でリソースに関して共通に呼び出されるものを定義します。
user.go
// User is struct of user
type User struct {
gorm.Model
UID string `json:"uid" gorm:"unique;not null"`
Name string `json:"name" gorm:"unique;not null"`
Applications []Application `json:"applications"`
Description string `json:"description"`
}
// Create creates a user
func (u *User) Create() (err error) {
db := database.GetDB()
return db.Create(u).Error
}
// FindByID finds a user by id
func (u *User) FindByID(id uint) (err error) {
db := database.GetDB()
return db.Where("id = ?", id).First(u).Error
}
server
serverでは各フレームワークのAPIを取り扱ったり、ルートを定義します。
server.go
package server
import "{{ .ProjectPath }}/config"
// Init initialize server
func Init() error {
c := config.GetConfig()
r, err := NewRouter()
if err != nil {
return err
}
r.Logger.Fatal(r.Start(":" + c.GetString("server.port")))
return nil
}
非常にシンプルですが後述するrouter.go
でルートを定義し、config
からポート番号を取得しフレームワークのサーバを起動します。
router.go
package server
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"{{ .ProjectPath }}/config"
"{{ .ProjectPath }}/controllers"
)
// NewRouter is constructor for router
func NewRouter() (*echo.Echo, error) {
c := config.GetConfig()
router := echo.New()
router.Use(middleware.Logger())
router.Use(middleware.Recover())
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: c.GetStringSlice("server.cors"),
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
version := router.Group("/" + c.GetString("server.version"))
healthController := controllers.NewHealthController()
version.GET("/health", healthController.Index)
return router, nil
}
router.go
ではフレームワークの初期化やミドルウェアを設定したり、controllers
をインスタンス化してメソッドに対応するルートを定義します。
views
最後にMVCのV、views
です。ここはレスポンスのresult
となる構造体を定義しています。
// UserView is view for user
type UserView struct {
gorm.Model
UID string `json:"uid"`
Name string `json:"name"`
Applications []*ApplicationView `json:"applications"`
Description string `json:"description"`
}
// NewUserView is constructor for view of users
func NewUserView(user *models.User) *UserView {
appViews := make([]*ApplicationView, len(user.Applications))
for i, app := range user.Applications {
appViews[i] = NewApplicationView(&app)
}
view := &UserView{
UID: user.UID,
Name: user.Name,
Applications: appViews,
Description: user.Description,
}
view.Model = user.Model
return view
}
モデルにはレスポンスとして返したくないアクセストークンなどのフィールドがあったりユーザごとに違うレスポンスを返さなければならない場合などに対応するためにこのviewを通し、必要なフィールドを持った構造体に変換します。
以上が最近良く利用しているディレクトリ構成です。改善点などございましたらお教えいただけると幸いです。
プロジェクト生成コマンド
上記のディレクトリ構成のEcho,ginフレームワークを用いた雛形を生成するコマンドを作成しました。
引数でGOPATHからのパスを設定することでGoのテンプレート機能を利用し、プロジェクトファイル群を生成します。
追記(2021/03/28)
go-api-starterのgorm
がアップデートされていたため、テンプレートをアップデートされたバージョンに更新いたしました。