はじめに
Goで簡単なCRUD操作ができるWeb APIを作ってみました。悩んだところとか勉強が足りないなと思ったところを備忘録として。しょうもないコードでも実際に手を動かしてみて気付くことが多々ありますね...
GoでWeb APIを作成する方法を解説する記事ではなく、個人的な学習の記録なのであしからず。
所感
設計、構成、パッケージやファイル名、関数の命名、関数の切り分け等、そのへんの知識・経験が皆無。 → 「行き詰まったら考える」を繰り返したせいで完成したコードがぐちゃぐちゃ。
GORMが全然使いこなせない。 → DB操作するのに毎度構造体を作り直したり、値を入れ替えたりして無駄が多すぎる。 DB操作用の関数を切り分けようとしたときにどう分ければいいのか全くわからない。 次はもっと薄めのORMも試してみたい。
全てはアウトプットが足りないということ。
コードはこちら
ディレクトリ構成
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
特には。
package main
import (
"github.com/bschafh/todoapp/db"
"github.com/bschafh/todoapp/router"
)
func init() {
db.Migrate()
}
func main() {
router.Run()
}
router
ルーティング関連のものはここ。
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
ミドルウェア関連のものはここ。
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操作のための構造体定義。
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
コントローラー関連のものはここ。
関数ひとつだけ抜粋します。
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
に合わせてキャストまで行わなければいけません。どうにかしたい。
db
データベース関連のものはここ。
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接続とマイグレート用の関数しかありません。
おわりに
今まで知識をインプットしたり、小さな関数を試すだけのコードを書いたりしかしていなかったので、一つのものを作ろうとしたときに色々とつまずくポイントが有りました。コードを書いたのは少し前なので今でも印象に残っているものだけを記録しましたが当時はもっと悩んでいた気がします。知識として知っているつもりのものでも実際にコードを書くと意識から外れてしまうものもたくさんありました。実際にコードを書くというのはやはり大事ですね。(自戒)
あと、設計やらアーキテクチャやら勉強したい。