Goで簡単なWeb APIを作ってみた

# はじめに
Goで簡単なCRUD操作ができるWeb APIを作ってみました｡悩んだところとか勉強が足りないなと思ったところを備忘録として｡しょうもないコードでも実際に手を動かしてみて気付くことが多々ありますね...

**GoでWeb APIを作成する方法を解説する記事ではなく､個人的な学習の記録なのであしからず｡**

# 所感
- 設計､構成､パッケージやファイル名､関数の命名､関数の切り分け等､そのへんの知識･経験が皆無｡ → ｢行き詰まったら考える｣を繰り返したせいで完成したコードがぐちゃぐちゃ｡

- GORMが全然使いこなせない｡ → DB操作するのに毎度構造体を作り直したり､値を入れ替えたりして無駄が多すぎる｡ DB操作用の関数を切り分けようとしたときにどう分ければいいのか全くわからない｡ 次はもっと薄めのORMも試してみたい｡

全てはアウトプットが足りないということ｡
[コードはこちら](https://github.com/bschafh/todoapi)

# ディレクトリ構成
```
todoapi
├── README.md
├── controllers
│   └── task_controller.go
├── db
│   └── db.go
├── middleware
│   └── middleware.go
├── models
│   └── task.go
├── router
│   └── router.go
├── main.go
├── go.mod
└── go.sum
```

# main
特には｡

```main.go
package main

import (
	"github.com/bschafh/todoapp/db"
	"github.com/bschafh/todoapp/router"
)

func init() {
	db.Migrate()
}

func main() {
	router.Run()
}
```

# router
ルーティング関連のものはここ｡

 ```router.go
func Run() {
	e := echo.New()
	e.Validator = &mymiddleware.CustomValidator{Validator: validator.New()}

	e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			return next(&mymiddleware.CustomContext{c})
		}
	})
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/tasks", controllers.ShowAllTasks)
	e.GET("/tasks/:id", controllers.ShowTask)
	e.POST("/tasks", controllers.CreateTask)
	e.PUT("/tasks/:id", controllers.UpdateTask)
	e.DELETE("/tasks/:id", controllers.DeleteTask)

	e.Start(":8080")
}
```
後述するミドルウェア`BindValidator`を扱うために`echo.Context`を拡張しています｡注意点として､`echo.Context`を拡張するためのミドルウェアは最初に登録する必要があります｡
echoの`middleware`と自作の`middleware`でパッケージ名が衝突してインポートエラーに悩んだのはご愛嬌｡

# middleware
ミドルウェア関連のものはここ｡

```middleware.go
type CustomValidator struct {
	Validator *validator.Validate
}

func (cv CustomValidator) Validate(i interface{}) error {
	return cv.Validator.Struct(i)
}

type CustomContext struct {
	echo.Context
}

func (cc *CustomContext) BindValidate(i interface{}) error {
	if err := cc.Bind(i); err != nil {
		return cc.String(http.StatusBadRequest, "invalid request:"+err.Error())
	}
	if err := cc.Validate(i); err != nil {
		return cc.String(http.StatusBadRequest, "invalid request:"+err.Error())
	}
	return nil
}
```
構成を考える力が足りないと自覚した｡

最初はドキュメントや解説記事等の局所的なサンプルを真似して､考えなしにミドルウェア関連のものとルーティング関連のものをまとめて`main`パッケージの中に記述しようとしていました｡`main.go`から`controllers`内のハンドラを呼び出しているにも関わらず､`CustomContext`や`BindValidator`を必要としているのはそのハンドラ､なんといういきあたりばったり｡というか普段からコードを書く人はこんなミスはしないでしょう｡自分の勉強不足を気付かされました｡

もともと`main.go`内でまとめて記述していたルーティングとミドルウェアを分割｡`main`から`router`を呼び出し､`router`と`controllers`それぞれが`middleware`に依存するように修正しました｡

# models
モデル関連のものはここ｡
リクエスト/レスポンス､DB操作のための構造体定義｡

```task.go

type Task struct {
	gorm.Model
	Text string `json:"text" gorm:"size:255" validate:"required"`
	Done *bool  `json:"done" gorm:"default:false" validate:"required"`
}

type Tasks []Task
```
今回､リクエストで送られてきたjsonに検証をかけるため､`go-playground/validator`を使用しました｡`validate:"required"`のタグは構造体に値が設定されているかどうかを判定しますが､`bool`をフィールドの型として定義すると`false`が設定されている場合に､上記ミドルウェアの`Validate`メソッドがエラーを返してしまいます｡これを回避するために`Done`の型は`*bool`としています｡

今思えば､構造体を初期化した時点で`Done`には`false`が設定されるので`validate:"required"`は要らなかったような気もする｡そもそもタスクを新規登録する際に｢まだ終わってません｣を送らなきゃいけないという考えもどうなんだろう｡

# controllers
コントローラー関連のものはここ｡
関数ひとつだけ抜粋します｡

```task_controller.go
func CreateTask(c echo.Context) error {
	db := db.Connect()
	defer db.Close()

	cc := c.(*middleware.CustomContext)

	task := new(models.Task)
	now := time.Now()
	task.CreatedAt = now
	task.UpdatedAt = now

	if err := cc.BindValidate(task); err != nil {
		return c.String(http.StatusBadRequest, "invalid request:"+err.Error())
	}

	if err := db.Create(task).Error; err != nil {
		return c.String(http.StatusBadRequest, "invalid request:"+err.Error())
	}

	return c.JSON(http.StatusCreated, task)
}
```
タスクを新規登録するための関数｡

クライアントから受け取れるものは`BindValidate`で構造体への紐付けと検証を行い､作成日時や更新日時に関しては構造体を初期化したときに設定するようにしています｡

DB操作を行うために関数を分けようとも考えましたが､`BindValidate`や`Create`メソッドが構造体を引数に取るので､どこで構造体を初期化するのか､DB操作を行う関数に構造体をまるごと渡すのか､フィールドの値だけを渡すのか等々考え始めたら着地できなくなったので一旦これで...｡

新規登録ならそこまで汚くは見えないのですが､これが更新となると更新前と更新後それぞれの構造体を初期化する必要があります｡また､パスパラメータで指定されたIDは`string`なので`gorm.Model`に合わせてキャストまで行わなければいけません｡[どうにかしたい｡](https://github.com/bschafh/todoapi/blob/master/controllers/task_controller.go#L68)


# db
データベース関連のものはここ｡

``` db.go
func Connect() *gorm.DB {
	db, err := gorm.Open("mysql", "root:password@/todoapp?charset=utf8&parseTime=True")
	if err != nil {
		log.Fatal(err)
	}
	return db
}

func Migrate() {
	db := Connect()
	defer db.Close()
	db.AutoMigrate(&models.Task{})
}
```
実際のDB操作は`controllers`に任せてしまったので､`db`パッケージにはDB接続とマイグレート用の関数しかありません｡

# おわりに
今まで知識をインプットしたり､小さな関数を試すだけのコードを書いたりしかしていなかったので､一つのものを作ろうとしたときに色々とつまずくポイントが有りました｡コードを書いたのは少し前なので今でも印象に残っているものだけを記録しましたが当時はもっと悩んでいた気がします｡知識として知っているつもりのものでも実際にコードを書くと意識から外れてしまうものもたくさんありました｡実際にコードを書くというのはやはり大事ですね｡(自戒)

あと､設計やらアーキテクチャやら勉強したい｡

# 参考
[echo ドキュメント](https://echo.labstack.com/)
[GORM ドキュメント](http://gorm.io/ja_JP/docs/)
[echo.Context を最大限に活用する](https://codehex.hateblo.jp/entry/echo-context)