Go言語で文字列ベースのenumを書く時には必ずバリデーションしたい : Tipsメモ

こんにちは。てぃろです。

今回はGo言語のコーディングのメモです。

最近APIのリクエストパラメータの定義としてenumを使おうとしていたのですが、Go言語には言語仕様としてのenumがありません。

しかしenumっぽい書き方はできるので、それを利用することにしました。

ただし、 iota は今回使わずに文字列ベースのenumを定義して使用としたところある問題に気付きました。

それはenumとして定数定義していない文字列も変数に入れてしまえる、ということです。

つまりこういうこと。

package main

import "fmt"

type Color string

const (
	Red    Color = "Red"
	Blue   Color = "Blue"
	Yellow Color = "Yellow"
)

func main() {
	var c Color
	c = "Black"
	fmt.Println(c) // Black
}

プログラムの内容を見れば当たり前のように見えるのですが、ここで定義しているColorをenumのように使いたい!と思うと意図としては”Red”か”Blue”か”Yellow”の3つ以外は c には代入されてほしくないわけです。

他の言語でenum定義されている場合には、当たり前にこのような定義の場合 “Black” は入らないのでそれだけでバリデーションチェックができてしまうのですが、上記のようにGo言語ではenumという言語仕様がないので値を定義通りにチェックするのはこれではできません。

そこで、以下のようにバリデーションチェックの関数を作ることにします。

package main

import "fmt"

type Color string

const (
	Red    Color = "Red"
	Blue   Color = "Blue"
	Yellow Color = "Yellow"
)

func (c Color) Valid() error {
	switch c {
	case Red, Blue, Yellow:
		return nil
	}
	return fmt.Errorf("invalid Color: %s", c)
}

func main() {
	var c Color
	c = "Black"
	if err := c.Valid(); err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Println("valid")
	}
}

これでenum定義を補強するバリデーションチェックができるようになります。

より厳密にしようとすると、 c.Set() のようなJavaでいうgetterやsetterを作るようにするといいのかもしれませんが、書く量も増えるし規約も増えてしまうのであまり好みではありません。

その代わり c.Valid() をしっかり呼ばないといけないという規約があるので、好みの問題かなと思います。

今後文字列ベースで作るときには c.Valid() だけは作っていきたいなと思っています。

参考

追記(2022/12/3)

この記事のツイートに引用で以下のような情報を頂きました!

ここでご紹介いただいたツールでは、getter、setterみたいなイメージでmershalの中でバリデーション入れる形で考えているようですね。後出しではありますが、これは一度は考えていたことでしたし、これが作れるツールというのは非常に魅力的だと思います。

ただ、本記事でそもそも気にしていたのは、enum の定義になっているある一つの要素に値を自由に入れてしまえることだったので少しやりたかったこととは違いました。

あとはiota + stringer でどこまで今回気にしていた enum 定義の値にデータを自由に入れられないようにするバリデーションを実現できるのか?が気になります。

私が不勉強なだけで先駆者が何かしらやっていたり、うまくやる方法があったりする気がするので、もうちょっと調べたり試したりしたいと思います。

のぼのぼさん、情報いただきありがとうございました!