Closed8

GoのEnumについて調べた

ぱんだぱんだ

iota使うパターン

1番よく見かけるやつ。iota+generateパターンが結局推奨らしい。

type Currency int

const (
	_ Currency = iota
	JPY
	USD
)

これは結局intの型エイリアスにCurrencyを定義してるだけ。iotaは0から連番ふってくれるだけ。こうなると文字列表示がしたくなるのでString()を定義したくなる。愚直にやるとあほくさくなるのとヒューマンエラーも起こりやすくなるのでみんないろいろ考えてるっぽい。

ぱんだぱんだ

真面目に文字列enumを考える

こちら
https://qiita.com/Koichi/items/f8fe3a14fc94aa75c57b

stringerを使ってString()を自動生成。この記事ではiotaで定義したenumの文字列出力を自動生成しててよさそう。ただ、enumの文字列の値に型の恩恵をプラスするのにもう一つ型を足してる。よく考えられてるけど少しわずわらしさもあり、依然としてenumがiotaだとintの型エイリアスでしかないので不正値が混入する。

ぱんだぱんだ

intやstringよりも厳格に

こちら

https://qiita.com/ikngtty/items/ad135599a2f92e6035fa

なるほど。よく考えられてる。外部から生成できるのがゼロ値のみなのがいい。不正値が混入しない。強いて言うなら手動で実装するのでヒューマンエラーが混入するくらい。

まだGoの自動生成でカバーみたいな文化が馴染んでないので個人的にはこれで良さそうに思う。少なくともサクッと列挙型作りたいときにはこれで良さそう。自動生成が良ければiota+stringerなどを選べばいいと思う。ただ、不正値の混入を完全に防ぐことはできないので1番厳格に書きたければ上記の記事は十分厳格だと思う。

と思ったけどこれだとenumがグローバル変数として定義されるので変更可能になってしまう。

	fmt.Println(enum.Spring.String()) // 春
	enum.Spring = enum.Season{}
	fmt.Println(enum.Spring.String()) // 未定義

まあ、やらないと思うけどenumに不正値代入できてしまうことよりグローバル変数を定義することの方が嫌な人もいるだろう。完全に厳格で安全なenumを作るのは容易ではない。

ぱんだぱんだ

GoにEnumイディオムが欲しいとみんな言ってるようです

こちら
https://zenn.dev/nobonobo/articles/986fea54fdc1c1

簡単に言うとGoの言語仕様にEnumイディオムを入れるのはいろんなところに手を入れなければならないので現実的ではない。標準パッケージとしてもコンパイラ支援がないとそんなに簡単じゃない。現状推奨してるiota+generateパターンで十分要件は満たせる。ベストではなくベター。これ大事。

気になったのは不正値の混入について起こりづらいとされてるが、不正値代入できてしまうということ。これはベストではなくベターと言ってる部分かなと理解した。

type Currency int

const (
	_ Currency = iota
	JPY
	USD
)

var c Currency

c = 100 // これはできちゃうけどこんなことすることはまあない

以下のディスカッションのコメントでiotaって何だよ、覚えられねーよって言われてるの草

https://github.com/golang/go/issues/19814#issuecomment-712788814

ぱんだぱんだ

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で生成したコード
currency_string.go
// 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

https://github.com/dmarkham/enumer

go install github.com/dmarkham/enumer@latest
color.go
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に従いましょう。
このスクラップは2023/11/28にクローズされました