LoginSignup
1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-16

はじめに

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

特には。

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内のハンドラを呼び出しているにも関わらず、CustomContextBindValidatorを必要としているのはそのハンドラ、なんといういきあたりばったり。というか普段からコードを書く人はこんなミスはしないでしょう。自分の勉強不足を気付かされました。

もともとmain.go内でまとめて記述していたルーティングとミドルウェアを分割。mainからrouterを呼び出し、routercontrollersそれぞれが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操作を行うために関数を分けようとも考えましたが、BindValidateCreateメソッドが構造体を引数に取るので、どこで構造体を初期化するのか、DB操作を行う関数に構造体をまるごと渡すのか、フィールドの値だけを渡すのか等々考え始めたら着地できなくなったので一旦これで...。

新規登録ならそこまで汚くは見えないのですが、これが更新となると更新前と更新後それぞれの構造体を初期化する必要があります。また、パスパラメータで指定されたIDはstringなのでgorm.Modelに合わせてキャストまで行わなければいけません。どうにかしたい。

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 ドキュメント
GORM ドキュメント
echo.Context を最大限に活用する

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0