nodeエンジニアがGoでつくるREST APIサーバー【ベストプラクティス!?】ʕ ◔ϖ◔ʔ

# はじめに

今回は競馬予想 [**siva**](http://siva-ai.com/) の改修に伴い、バックエンドをnode.jsで作っていたものをGoで実装した際のメモになります。
Go経験ゼロから最低限必要な機能をサクッと実装するまでの記録です。

## 〇 ここでやること
- 依存ライブラリの導入説明
- echoでRESTサーバを立てたときの設定
- 個人的にやってしまったミス

## × ここでやらないこと
- goのインストール説明
- goの基本構文等の説明
- テストに関すること


# 環境

- Windows 10
- Go 1.10.1 


# 1. 導入

まずは以下のコマンドをインストールします。

- 依存関係管理([dep](https://github.com/golang/dep))
- xormツール([xorm](https://github.com/go-xorm/cmd))
- Swaggerコード生成([swag](https://qiita.com/pei0804/items/3a0b481d1e47e5a72078))
- タスクランナー([godo](https://github.com/go-godo/godo))

## 依存関係管理(dep)
node.jsでいうnpmのようなパッケージ管理ツールです。

### インストール
```
go get -u github.com/golang/dep/cmd/dep
```

### 使い方
プロジェクトのディレクトリでコマンドを実行します。

初期化(初回一回のみ実行)

```
dep init
```

ライブラリの追加

```
dep ensure -add XXXX
```

定義済みのGopkg.tomlに従いダウンロード

```toml:Gopkg.toml
[prune]
  go-tests = true
  unused-packages = true

[[constraint]]
  branch = "master"
  name = "github.com/comail/colog"

[[constraint]]
  name = "github.com/labstack/echo"
  version = "3.3.5"

[[constraint]]
  name = "github.com/spf13/viper"
  version = "1.0.2"

[[constraint]]
  name = "github.com/go-xorm/xorm"
  version = "0.7.0"

[[constraint]]
  branch = "master"
  name = "github.com/mitchellh/mapstructure"

[[constraint]]
  branch = "master"
  name = "github.com/swaggo/echo-swagger"

[[constraint]]
  name = "github.com/thoas/go-funk"
  version = "0.2.0"

[[constraint]]
  name = "github.com/go-resty/resty"
  version = "1.5.0"

[[constraint]]
  name = "github.com/antonholmquist/jason"
  version = "1.0.0"

[[constraint]]
  name = "github.com/fatih/structs"
  version = "1.0.0"

[[constraint]]
  name = "github.com/miiton/kanaconv"
  version = "1.0.0"
```

```
dep ensure
```

## xormツール
DBに関するツールで今回はリバースでテーブル用の構造体を作成するのに使用しました。

### インストール
```
go get github.com/go-xorm/cmd/xorm
```

### 使い方
**コードを出力する前にテンプレートを作成する必要があります。**
今回は[goxorm](https://github.com/go-xorm/cmd/tree/master/xorm/templates/goxorm)をダウンロードして使用しています。

DBからcode生成

```
xorm reverse mysql user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8 templates/goxorm
```

## Swaggerコード生成(swag)
Godocを解析してSwagger UIを出力してくれます。

### インストール
```
go get -u github.com/swaggo/swag/cmd/swag
```

### 使い方
[設定できる項目](https://github.com/swaggo/swag#general-api-info)を参考にコードにSwagger UI用のコメントを書いていきます。
コメントを書いたら生成用のコマンドを実行します。

```
swag init
```

正常に完了したら「create docs.go at  docs/docs.go」のメッセージが出力されます。
サーバを起動後、[http://localhost:1323/swagger/index.html](http://localhost:1323/swagger/index.html)にアクセスすることでSwagger UIにアクセスできます。  


## タスクランナー(godo)
**Windowsでは動作しないため不要であれば読み飛ばしてください。**

### インストール
```
go get -u gopkg.in/godo.v2/cmd/godo
```

### 使い方
今回はソースが修正されたのを検知してサーバをリスタートするタスクを使用しました。
godoが参照するディレクトリ名は固定で**Gododir**になり、その配下のmain.goのタスクが実行されます。

```golang:Gododir/main.go
package main

import (
	do "gopkg.in/godo.v2"
)

func tasks(p *do.Project) {
	p.Task("server", nil, func(c *do.Context) {
		c.Start("main.go", do.M{"$in": "./"})
	}).Src("*.go", "**/*.go").Debounce(3000)
}

func main() {
	do.Godo(tasks)
}
```

以下のコマンドでサーバが起動します。

```
godo server --watch
```


# 2. 開発

## プロジェクト構造
新しく開発者がJOINしたときにもすぐ開発できるように作業対象となるディレクトリを明確にしたいをコンセプトに構築しました。
基本的には3ディレクトリが開発対象となります。

- dto
- handlers
- resources

```
backend-template
|   .gitignore
|   Gopkg.toml
|   main.go
|   README.md
|   
+---application             ・・・アプリコア部分
|   |   application.go
|   |   context.go
|   |   validator.go
|   |   
|   +---configadapter
|   |       configadapter.go
|   |       
|   +---constants
|   |       constants.go
|   |       
|   +---dbadapter
|   |       dbadapter.go
|   |       
|   \---kvsadapter
|           kvsadapter.go
|           
+---config                ・・・アプリケーション設定ファイル
|       default.json
|       production.json
|       
+---docs                  ・・・swagコマンド出力結果(Swagger UI)
|   |   docs.go
|   |   
|   \---swagger
|           swagger.json
|           swagger.yaml
|           
+---dto                   ・・・ 構造体格納先
|   +---request
|   |       params.go
|   |       sandbox.go
|   |       
|   \---response
|           sandbox.go
|           
+---Gododir               ・・・ godo用
|       main.go
|       
+---handlers              ・・・ ハンドラ(コントローラー)
|       horses.go         ・・・ 出走馬に関するAPIグループ
|       routes.go         ・・・ ルーティング設定メイン部分
|       sandbox.go        ・・・ テスト用APIグループ
|       
+---models                ・・・ xorm tools出力先
|       m_horse.go
|       
+---resources      
|   \---sql               ・・・ 生のSQLクエリを実行するときのファイル格納(ハンドラごとにディレクトリを作成する)
|       +---horses
|       |       test.sql
|       \---sandbox 
|               test.sql
|               
\---templates
    \---goxorm            ・・・ xormツールテンプレート格納先
            config
            struct.go.tpl
```

## main
あまり太りすぎないように初期化とMiddlewareの設定のみを行うようにしました。
echo.Contextのラップは[**他のMiddleware設定よりも先に行わなければならない**](https://echo.labstack.com/guide/context)ので注意です。

```go:main.go
// Copyright 2018 GAUSS All Rights Reserved.
// API サーバメイン
// @title SIVA API
// @version 1.0.0
// @description SIVA, バックエンドAPIサーバ
// @contact.name GAUSS
// @contact.url https://gauss-ai.jp
// @contact.email support@gauss-ai.jp
// @host localhost:1323
// @BasePath /
// @securityDefinitions.apikey Bearer XXXXXXXXXXXXXXXX
// @in header
// @name Authorization

package main

import (
	"fmt"
	"log"
	"strings"

	"gauss/backend-template/application"
	"gauss/backend-template/handlers"

	"github.com/comail/colog"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	//===================================
	// Initialize
	//===================================
	colog.SetFormatter(&colog.StdFormatter{
		Flag: log.Ldate | log.Ltime | log.Lshortfile,
	})
	colog.Register()
	e := echo.New()
	e.Validator = application.GetValidator()

	// アプリケーション設定
	config := application.GetInstance().GetConfig()

	// Router初期化
	handlers.Router(e)

	//===================================
	// Middleware
	//===================================
	// echo.Context をラップして扱うために middleware として登録する
	e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			return h(&application.AppContext{c})
		}
	})
	e.Use(middleware.Recover())
	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
		Format: "${host} [${time_rfc3339}] \"${method} ${uri}\" ${status} ${bytes_in} ${bytes_out}\n",
	}))
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE},
		AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
	}))
	e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
		Skipper: func(c echo.Context) bool {
			if strings.Index(c.Path(), "/swagger/") > -1 {
				return true
			}
			return false
		},
		Validator: func(key string, c echo.Context) (bool, error) {
			return key == config.GetString("server.validKey"), nil
		},
	}))

	//===================================
	// server start
	//===================================
	host := config.Get("server.host")
	port := config.Get("server.port")
	e.Start(fmt.Sprintf("%v:%v", host, port))
}
```

## ルーター
各ハンドラグループを読み込むための設定用ルーターを用意しました。
このへんはリフレクションで自動で設定するほうがシンプルな気がしているのですが、現状は数も少ないですしリフレクションを多用すべきではないということから手で追加する方法としています。

```go:handlers/routes.go
package handlers

import (
	"os"

	"sample/backend-template/application"
	_ "sample/backend-template/docs"

	"github.com/swaggo/echo-swagger"
)

var app = application.GetInstance()

// Router ルーティング設定
func Router(e *echo.Echo) {

  // ハンドラを作成するたびにここに追加していく
	HorsesRouter(e)

	key, ok := os.LookupEnv("GO_ENV")
	if !ok || key != "production" {
		SandBoxRouter(e)

		// swagger
		e.GET("/swagger/*", echoSwagger.WrapHandler)
	}

}
```

## context
echo.Contextをラップしてリクエストパラメータのバインドとバリデーションを行います。
この辺は[参考元](https://codehex.hateblo.jp/entry/echo-context)をそのまま利用させていただきました。

```go:application/context.go
package application

import (
	"fmt"
	"log"
	"net/http"

	"github.com/labstack/echo"
)

// AppContext echo.Context をラップ
type AppContext struct {
	echo.Context
}

// BindValidate BindとValidateを合わせたメソッド
func (c *AppContext) BindValidate(i interface{}) error {
	if err := c.Bind(i); err != nil {
		return c.String(http.StatusBadRequest, "Request is failed: "+err.Error())
	}
	if err := c.Validate(i); err != nil {
		return c.String(http.StatusBadRequest, "Validate is failed: "+err.Error())
	}
	return nil
}

// callFunc
type callFunc func(c *AppContext) error

// AppHandler Router登録用
func AppHandler(h callFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		log.Printf("debug: start: %v %v", c.Request().Method, c.Request().RequestURI)
		return h(c.(*AppContext))
	}
}
```

## validator
[validator](https://github.com/go-playground/validator)を利用してリクエストパラメータの入力チェックを行っています。
型定義がstringで、必須ではない値の桁数チェックや数値変換可能な文字列のチェックを行いたかったためカスタムのチェックを追加しています。

```go:application/validator.go
package application

import (
	"strconv"

	"gopkg.in/go-playground/validator.v9"
)

var valIns = newValidator()

// Validator Validator
type Validator struct {
	validator *validator.Validate
}

// Validate Validate
func (v *Validator) Validate(i interface{}) error {
	return v.validator.Struct(i)
}

// New コンストラクタ
func newValidator() *Validator {
	ins := new(Validator)
	ins.validator = validator.New()
	ins.validator.RegisterValidation("optlen", optionLenValid)
	ins.validator.RegisterValidation("atoi", atoiValid)

	return ins
}

// GetValidator GetValidator.
func GetValidator() *Validator {
	return valIns
}

// 任意指定桁数チェック
func optionLenValid(fl validator.FieldLevel) bool {
	if fl.Field().Len() < 1 {
		return true
	}
	if l, err := strconv.Atoi(fl.Param()); err == nil && len(fl.Field().String()) == l {
		return true
	}

	return false
}

// 数値変換チェック
func atoiValid(fl validator.FieldLevel) bool {
	if fl.Field().Len() < 1 {
		return true
	}
	if _, err := strconv.Atoi(fl.Field().String()); err == nil {
		return true
	}
	return false
}
```

## dto
リクエストとレスポンスで設定するタグが異なっているためディレクトリから分けて格納することにしました。
リクエストの構造体は[バリデーション用のタグ](https://godoc.org/gopkg.in/go-playground/validator.v9)と[リクエストクエリパラメータのタグ](https://echo.labstack.com/guide/request)を書いています。
また、レスポンスの構造体はmapstructure.WeakDecode()するとき用に[mapstructureのタグ](https://godoc.org/github.com/mitchellh/mapstructure)を書いています。

```go:dto/request/sandbox.go
package request

// SandBoxParam サンプルリクエストパラメータ
type BaseRaceParam struct {
	HoldingYear    string `json:"holding_year" query:"holding_year" validate:"required,len=4"`               // 開催年
	HoldingDate    string `json:"holding_date" query:"holding_date" validate:"optlen=4,atoi"`                // 開催月日
	RaceCourseCode string `json:"race_course_code" query:"race_course_code" validate:"required,min=1,max=2"` // 競馬場コード
	RaceNo         int    `json:"race_no" query:"race_no" validate:"required,min=0,max=99"`                  // レース番号
	Hits           int    `json:"hits" query:"hits"`                                                         // 取得数
	Offset         int    `json:"offset" query:"offset"`                                                     // オフセット
}
```

```go:dto/response/sandbox.go
package response

// SandBoxList test struct.
type SandBoxList struct {
	RecordTypeID    string `json:"record_type_id" mapstructure:"record_type_id"`
	DataCreatedDate string `json:"data_created_date" mapstructure:"data_created_date"`
	RaceNo          int    `json:"race_no" mapstructure:"race_no"`
}
```

## handlers
各グループ単位でファイルを設けてエンドポイントを作成しました。
XXXRouterに[Routing](https://echo.labstack.com/guide/routing)を書くことでハンドラの追加・削除がシンプルになったと思います。

```go:handlers/sandbox.go
package handlers

import (
	"net/http"

	"gauss/backend-template/application"
	"gauss/backend-template/dto/request"
	"gauss/backend-template/utils/iterator"

	"github.com/labstack/echo"
)

// SandBoxRouter APIテスト
func SandBoxRouter(e *echo.Echo) {
	g := e.Group("/sandbox")

	g.GET("/bbs", application.AppHandler(getBbs))
	g.POST("/bbs", application.AppHandler(postBbs))

}

// getBbs godoc
// @Summary 掲示板投稿内容一覧取得
// @Description 掲示板投稿内容一覧を取得する。
// @Security ApiKeyAuth
// @Accept  json
// @Produce  json
// @Param Authorization header string true "Authentication header"
// @Param holding_year query string false "開催年"
// @Param holding_date query string false "開催月日"
// @Param race_course_code query string false "競馬場コード"
// @Param race_no query int false "レース番号"
// @Param hits query int false "取得数"
// @Param offset query int false "オフセット"
// @Success 200 {object} models.TBbs
// @Router /bbs [get]
func getBbs(c *application.AppContext) error {
	param := new(request.BbsGet)
	if err := c.BindValidate(param); err != nil {
		return err
	}
	findRes := make([]*models.TBbs, 0)
	var count int64
	var err error
	db := app.GetDb().GetDBInstance()
	if param.Hits > 0 {
		count, err = db.OrderBy("post_date").Limit(param.Hits, param.Offset).FindAndCount(&findRes, param)
	} else {
		count, err = db.OrderBy("post_date").FindAndCount(&findRes, param)
	}
	if err != nil {
		return err
	}
	result := &response.BbsList{TotalCount: count, Posts: findRes}
	copier.Copy(&result, &param)

	return c.JSON(http.StatusOK, result)
}

// postBbs godoc
// @Summary 掲示板投稿
// @Description 掲示板投稿する。
// @Security ApiKeyAuth
// @Accept  json
// @Produce  json
// @Param Authorization header string true "Authentication header"
// @Param param body request.BbsPost true "掲示板投稿内容"
// @Success 201 {object} models.TBbs
// @Router /bbs [post]
func postBbs(c *application.AppContext) error {
	param := new(request.BbsPost)
	if err := c.BindValidate(param); err != nil {
		return err
	}

	guid := xid.New()
	dec := &models.TBbs{PostDate: time.Now(), DeleteFlg: "0", Xid: guid.String()}
	copier.Copy(&dec, &param)

	res, err := app.GetDb().Insert(dec)
	if err != nil {
		return err
	}
	fmt.Printf("%+v", res)

	return c.JSON(http.StatusCreated, res)
}
```


# 3. ちょっとはまったところ

## $GOPATH
ワークスペースとしてユーザ任意の場所を指定することができるのですがディレクトリ構造が固定されてしまうところがあまりイケてるとは思いませんでした。(importの解決とかで使用されるので仕方ないのでしょうが。。。)
依存関係管理(dep)はこのGOPATH配下でしか利用できないため、ちょっと拾いものを一時ディレクトリに展開して動かしてみるというやりかたはできません。
なお、複数指定することができるのですがあまりおすすめされていないようです。

## import相対指定
これもまた[ドキュメント](https://golang.org/cmd/go/#hdr-Relative_import_paths)に書かれていますが、ワークスペース($GOPATH)配下では相対パス指定ができません。  
自前のパッケージをimportする場合はパスを明記するようにします。

例：handlersからみたパスの指定方法
OK）import "gauss/backend-template/application"
NG）import "../application"

## 構造体の初期化
構造体を初期化するには3パターンあります。
関数呼び出し時のパラメータなど多くの場合にアドレスが求められるため、1か2を使うことが多かったです。
さらにxormで使用する場合は1を、その他の場合は1行で初期化までできるため2が個人的には好みでした。

1.new()を使う場合

```go
s := new(SampleStruct)
```

2.アドレス演算子付き

```go
s := &SampleStruct{}
```

3.アドレス演算子なし

```go
s := SampleStruct{}
```


# まとめ

ささっと作りたいなと思ったときに外部ライブラリを頼りたくなるのですが、あまりstarがついていなかったりメンテされてるのかわからないものが多い印象を受けました。シンプルゆえに物足りないと思う部分もあり、そのへん頑張って書く必要があるのでお手軽感が若干かけるかなぁと。

静的型付け全般の話でGoに限ったことではないですが、javascriptでガシガシとオブジェクトにデータを突っ込んでいた頃に比べるといちいち構造体作ったりするのめんどくさいなぁと思っていましたが、設計書から構造体を出力できるツールをつくったり、[gojson](https://github.com/ChimeraCoder/gojson)を活用することで多少ストレスは解消されました。

キャリアとして元々C言語からスタートしていたのでポインタはすんなりでしたが、LL言語スタートだとこの辺はちょっと理解するのに時間が必要なのかもしれません。
ただ、アドレスを意識する言語を覚えておくことは凄く有益だと思いますし、ビルドも実行速度も速いのでGoは今後社内でも広めていきたいと思いました。

折を見て今回作ったライブラリもgithubに公開していきます。

# 参考文献
- [echo.Context を最大限に活用する](https://codehex.hateblo.jp/entry/echo-context)
- [[swaggo]GoのGoDocを書いたら、Swaggerを出せるやばいやつ](https://qiita.com/pei0804/items/3a0b481d1e47e5a72078)