GoのEnumについて調べた
GoのEnumについて今更ながら調べた。
iota使うパターン
1番よく見かけるやつ。iota+generateパターンが結局推奨らしい。
type Currency int
const (
_ Currency = iota
JPY
USD
)
これは結局intの型エイリアスにCurrency
を定義してるだけ。iota
は0から連番ふってくれるだけ。こうなると文字列表示がしたくなるのでString()
を定義したくなる。愚直にやるとあほくさくなるのとヒューマンエラーも起こりやすくなるのでみんないろいろ考えてるっぽい。
iotaとか使わないでもう文字列でいいじゃん
ってやつ。
たぶんこれで困らないんだけど型に厳格な人だと不正値が混入するのがたぶん気になるはず。
で結局こうやってバリデーションロジックを書くはめになる。
真面目に文字列enumを考える
こちら
stringer
を使ってString()
を自動生成。この記事ではiotaで定義したenumの文字列出力を自動生成しててよさそう。ただ、enumの文字列の値に型の恩恵をプラスするのにもう一つ型を足してる。よく考えられてるけど少しわずわらしさもあり、依然としてenumがiotaだとintの型エイリアスでしかないので不正値が混入する。
intやstringよりも厳格に
こちら
なるほど。よく考えられてる。外部から生成できるのがゼロ値のみなのがいい。不正値が混入しない。強いて言うなら手動で実装するのでヒューマンエラーが混入するくらい。
まだGoの自動生成でカバーみたいな文化が馴染んでないので個人的にはこれで良さそうに思う。少なくともサクッと列挙型作りたいときにはこれで良さそう。自動生成が良ければiota+stringerなどを選べばいいと思う。ただ、不正値の混入を完全に防ぐことはできないので1番厳格に書きたければ上記の記事は十分厳格だと思う。
と思ったけどこれだとenumがグローバル変数として定義されるので変更可能になってしまう。
fmt.Println(enum.Spring.String()) // 春
enum.Spring = enum.Season{}
fmt.Println(enum.Spring.String()) // 未定義
まあ、やらないと思うけどenumに不正値代入できてしまうことよりグローバル変数を定義することの方が嫌な人もいるだろう。完全に厳格で安全なenumを作るのは容易ではない。
GoにEnumイディオムが欲しいとみんな言ってるようです
こちら
簡単に言うとGoの言語仕様にEnumイディオムを入れるのはいろんなところに手を入れなければならないので現実的ではない。標準パッケージとしてもコンパイラ支援がないとそんなに簡単じゃない。現状推奨してるiota+generateパターンで十分要件は満たせる。ベストではなくベター。これ大事。
気になったのは不正値の混入について起こりづらいとされてるが、不正値代入できてしまうということ。これはベストではなくベターと言ってる部分かなと理解した。
type Currency int
const (
_ Currency = iota
JPY
USD
)
var c Currency
c = 100 // これはできちゃうけどこんなことすることはまあない
以下のディスカッションのコメントでiotaって何だよ、覚えられねーよって言われてるの草
enum支援モジュール使ってみる
stringer
go install golang.org/x/tools/cmd/stringer@latest
package money
//go:generate stringer -type=Currency
type Currency int
const (
_ Currency = iota
JPY
USD
)
go generate ./...
stringerで生成したコード
// Code generated by "stringer -type=Currency"; DO NOT EDIT.
package money
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[JPY-1]
_ = x[USD-2]
}
const _Currency_name = "JPYUSD"
var _Currency_index = [...]uint8{0, 3, 6}
func (i Currency) String() string {
i -= 1
if i < 0 || i >= Currency(len(_Currency_index)-1) {
return "Currency(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _Currency_name[_Currency_index[i]:_Currency_index[i+1]]
}
func main() {
fmt.Println(money.JPY.String()) // JPY
fmt.Println(money.JPY) // JPY
}
これができちゃうけど普通にやらないよね
func main() {
fmt.Println(money.JPY.String())
fmt.Println(money.JPY)
a := money.Currency(100)
fmt.Println(a)
}
enumer
go install github.com/dmarkham/enumer@latest
package color
//go:generate enumer -type=Color
type Color int
const (
_ Color = iota
Red
Yellow
Blue
)
go generate ./...
color_enumer.go
// Code generated by "enumer -type=Color"; DO NOT EDIT.
package color
import (
"fmt"
"strings"
)
const _ColorName = "RedYellowBlue"
var _ColorIndex = [...]uint8{0, 3, 9, 13}
const _ColorLowerName = "redyellowblue"
func (i Color) String() string {
i -= 1
if i < 0 || i >= Color(len(_ColorIndex)-1) {
return fmt.Sprintf("Color(%d)", i+1)
}
return _ColorName[_ColorIndex[i]:_ColorIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _ColorNoOp() {
var x [1]struct{}
_ = x[Red-(1)]
_ = x[Yellow-(2)]
_ = x[Blue-(3)]
}
var _ColorValues = []Color{Red, Yellow, Blue}
var _ColorNameToValueMap = map[string]Color{
_ColorName[0:3]: Red,
_ColorLowerName[0:3]: Red,
_ColorName[3:9]: Yellow,
_ColorLowerName[3:9]: Yellow,
_ColorName[9:13]: Blue,
_ColorLowerName[9:13]: Blue,
}
var _ColorNames = []string{
_ColorName[0:3],
_ColorName[3:9],
_ColorName[9:13],
}
// ColorString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ColorString(s string) (Color, error) {
if val, ok := _ColorNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _ColorNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Color values", s)
}
// ColorValues returns all values of the enum
func ColorValues() []Color {
return _ColorValues
}
// ColorStrings returns a slice of all String values of the enum
func ColorStrings() []string {
strs := make([]string, len(_ColorNames))
copy(strs, _ColorNames)
return strs
}
// IsAColor returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Color) IsAColor() bool {
for _, v := range _ColorValues {
if i == v {
return true
}
}
return false
}
fmt.Println(color.Red, color.Red.String(), color.Red.IsAColor()) // Red Red true
enumerだと以下のように文字列もしくは数値からenumを取得しやすい。
func ValueOf(str string) (Color, error) {
for _, color := range ColorValues() {
if color.String() == str {
return color, nil
}
}
return 0, fmt.Errorf("invalid color %s", str)
}
感想
-
enumer
がいい感じだった。 - 何をどうしても完全に安全で厳格なenumをGoで作るのは容易ではない
- なのでサクッとやるならiotaもしくはstringで不正値の代入には目をつぶる。
- ちゃんとやるならiota+stringer or iota+enumerで自動生成する。どっちにしろ不正値の代入には目をつぶる。
- 他の言語から来た人は気になるだろうがこれがGoです。Goに従いましょう。