はじめに
k.s.ロジャースの西谷です。
今回はGo言語とGinを使ってプロジェクトの開発を行いました。
実務利用は初めてで、このときに感じた便利な機能や躓いた箇所について書いてみようと思います。
もし、間違いやより良い実装があればコメントにて教えて頂けたらと思います。
開発環境
- 言語: Go 1.13.3
- フレームワーク: Gin
便利だと思った所
リクエストのバリデーション
Ginではリクエストオブジェクトの定義にbinding
を書くことでバリデーションを実施できます。
特殊パターン以外はリクエストに直接書くことができるので、直感的だと思います。
以下のように書くと、req
はID
の値は必ず存在し、Age
は0以上となります。
type CreateUser struct {
ID string `json:"id" binding:"required"`
Name string `json:"name"`
Age int `json:"age" binding:"min=0"`
}
func (h *HogeController) Create(c *gin.Context) {
var req request.CreateUser
// このタイミングでバリデーションが実行される
err := c.ShouldBindJSON(&req)
if err != nil {
// リクエストが間違っている時の処理
return
}
}
バリデーションをカスタマイズしたい場合は以下のようにします。
// 日付がyyyy-mm-ddのフォーマットになっているか
func DatetimeValidator(field validator.FieldLevel) bool {
value := field.Field().String()
_, err := time.Parse("2006-01-02", value)
if err != nil {
return false
}
return true
}
func main() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// is_dateのタグでDatetimeValidatorを登録
v.RegisterValidation("is_date", DatetimeValidator)
}
}
type CreateUser struct {
ID string `json:"id" binding:"required"`
Name string `json:"name"`
Age int `json:"age" binding:"min=0"`
Date time.Time `json:"date" binding:"is_date"`
}
詳細はこちらを参照ください。
エラー処理周り
Goでは必ず処理直後にエラー処理を行います。
こちらはエラー処理が煩雑など賛否両論ではありますが、私の意見としては複雑なtry, catch
が乱立するよりはいいかなと思いました。
err := HogeFunc()
if err != nil {
log.Fatal(err)
}
コード品質を気にしているチームであればtry, catch
が乱立することはないかなと思います。
しかし、プロジェクトによってはコードを一切管理しておらず、例外が理解できないほど複雑になることもあるので、Goを利用していれば最悪ケースを防げるかなと思います。
躓いたところ
APIリクエストでのnullの扱い
REST APIのリクエストで未入力と初期値(0や空文字など)を区別したい場合があります。
これを実装する場合は変数をポインタにしてnil
を持たせる必要があります。
実際に起きたバグとしては、PATCH関連のAPIでリクエストオブジェクトを通常の型で定義したところ、未入力フィールドがnullで上書きされる問題でした。
例えば次の定義に対して、ユーザ名だけを変更しようとしてリクエストを行うとAge
が0で上書きされます。
type UpdateUser struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
{
"id": "xxxxxxxxx",
"name": "hogehoge"
}
これに対して、nullが投げられる可能性のあるフィールドをポインタで定義することで解決できます。
type UpdateUser struct {
ID string `json:"id"`
Name *string `json:"name"`
Age *int `json:"age"`
}
これで未入力の場合はnil
となりますので、サーバ側で保存対象を区別できます。
しかし、リクエストでnullで上書きしたい場合は、nullをリクエストで投げても更新されなくなります。
(定義済みのデータを未定義
に更新出来ることが特殊だと思うので、一部の言語が特殊だと考えています)
ジェネリクスがない
型は違うが同じような処理をしたい場合はたまにあります。
現状ですと、interface{}
で頑張るしかないようです。 1
func AddOne(v interface{}) interface{} {
switch v.(type) {
case int:
return v.(int) + 1
case float64:
return v.(float64) + 1.0
default:
return nil
}
}
func main() {
a := AddOne(1.0)
fmt.Printf("%f", a.(float64))
}
おわりに
今回はGoを実際にプロダクトで使ってみて感じたことについて説明しました。
設計や工夫次第で解消できる箇所もあるかなと思うので、これからも色々試していきたいと思います。
間違い等あればご指摘頂けたらと思います。
Wantedlyでもブログ投稿してます
Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers