GolangでMVCなAPIサーバを作るときのディレクトリ構成とプロジェクト生成コマンド

# Goの軽量Webアプリケーションフレームワーク(Echo, Gin)のディレクトリ構成

Golangの軽量Webアプリケーションフレームワークである、EchoやGinを使うとRuby on Railsのようにディレクトリ構成が特に決まっていないため、どのようにすれば効率的に開発できるのか悩みます。

しかし、最近いくつかGinやEchoを使ってAPIサーバを作成し、なんとなくディレクトリ構成が決まってきたので、共有します。

以下のリポジトリは後述する[自動リポジトリ生成コマンド](https://github.com/lon9/go-echo-api-starter)で生成されるプロジェクトの雛形です。
これについて説明をしていきます。

まずディレクトリを木構造で表示すると以下のようになります。

```
├── 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

```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](http://gorm.io/ja_JP/)というORMを利用しているため、このライブラリで利用するデータベースドライバをインポートしています。

コマンドの引数は`e`のみとなっています。ここに`development`や`production`を設定すると`config/environments`以下の対応する設定ファイルが読み込まれる仕組みになっています。

そのあとで`config`と`database`、`server`の初期化を行います。これらについては後述します。

## config
次に`config`です。

`config.go`

```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](https://github.com/spf13/viper)というライブラリを用いてファイルから設定を読み込みます。
ローカルの場合は`config/environments/`以下の引数`e`に対応するyamlファイルを読み込みます。
Dockerを利用する場合は`secret`を利用しコンテナの`/run/secrets/`以下に設定ファイルをマッピングします。
`config`はシングルトンで実装し`GetConfig`で至るところから読み出せるようにします。

## controllers
次にMVCのCの部分のコントローラです。

`health_controller.go`

```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`で定義された以下の構造体を利用します。

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

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

```docker
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`

```docker
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`

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

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

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

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

```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`となる構造体を定義しています。

```go
// 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フレームワークを用いた雛形を生成するコマンドを作成しました。

[go-api-starter](https://github.com/lon9/go-api-starter)

引数でGOPATHからのパスを設定することでGoのテンプレート機能を利用し、プロジェクトファイル群を生成します。

# 追記(2021/03/28)

[go-api-starter](https://github.com/lon9/go-api-starter)の`gorm`がアップデートされていたため、テンプレートをアップデートされたバージョンに更新いたしました。