Menu

Go言語の列挙型(enum):constとiotaで作る方法

Goにenumキーワードはありません。このページでは、名前付きの型とiotaを使ったconstブロックというGoらしい代替手段と、String()、検証、パース、ビットフラグ、JSON対応の追加方法を紹介します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Goに enum キーワードはありません。列挙型は、名前付きの型と、iota で番号を振ったその型の値の const ブロックという2つの部品から作ります。

Sunday は0で、その後の行は1つずつ増えます。これを単なる数値のリストではなく列挙型にしているのは、名前付きの型 Weekday です。isWeekend は何を期待しているかをシグネチャで示せ、型にはメソッドを追加できます。出力が 1 5 6 になるのは、Weekday をテキストとしてどう表示するかをまだGoに教えていないからです。それは後で扱います。

iotaの仕組み

iota は、const ブロックの中でコンパイラが提供するカウンタです。これを使ったあらゆるテクニックは、2つのルールで説明できます。

  1. iota はブロック内での現在の行のインデックスに等しく、0から始まり、新しい const ブロックごとに0に戻る。
  2. = 式 のない行は、直前の行の式と型を繰り返し、新しい iota で評価する。

つまり上の Monday は、iota が1になった状態での Monday Weekday = iota の省略形です。式が繰り返されるので、iota だけでなくどんな定数式でも使えます。

iota が数えるのは名前ではなく行です。XY が示すとおり、同じ行にある2つの定数は1つの iota の値を共有します。

1から始める方法と、そうしない理由

誰も設定していない列挙型の変数は、ゼロ値の0を保持しています。0が Sunday のような本物の値だと、「ユーザーが日曜日を選んだ」のか「フィールドが一度も埋められていない」のかを区別できません。よくある解決策は3つです。

本番のコードで最もよく使われるのは1つ目の方法で、protobufが生成するGoの列挙型もこれに従っています(..._UNSPECIFIED = 0)。こうするとゼロ値が正直な意味を持ちます。

値を読み飛ばす

ブランク識別子 _ は、名前を作らずに iota の値を1つ消費します。プロトコルで定められた番号に合わせるときや、残りの番号を振り直さずに値を廃止するときなど、間を空けるのに使います。

type Opcode byte

const (
	OpContinue Opcode = iota // 0
	OpText                   // 1
	OpBinary                 // 2
	_                        // 3, reserved
	_                        // 4, reserved
	_                        // 5, reserved
	_                        // 6, reserved
	_                        // 7, reserved
	OpClose                  // 8
	OpPing                   // 9
	OpPong                   // 10
)

このWebSocketのオペコードのように、番号が外部の仕様で決まっている場合は、ブランクを数えるより明示的に書く(OpClose Opcode = 8)ほうがたいてい明快です。iota は、正確な番号を気にしない値のためのものです。

データベースやファイルに保存したり、ネットワークで送ったりする番号を持つ iota のリストは、決して並べ替えたり途中に挿入したりしないでください。 途中に1行加えると、それ以降のすべての値がずれます。新しい値は末尾に追加するか、番号を明示的に割り当てます。

Stringメソッドを追加する

型に String() string メソッドを付けると、fmt%v%sPrintln でそれを使います。

このメソッドで重要な点が2つあります。

  • 範囲チェック。これがないと Weekday(9).String() はインデックス範囲外でpanicします。呼び出し側が Weekday(9) を作るのを止めるものは何もないので、いつかは起きます。
  • Sprintf の中の int(d)d そのものを %d で整形するのは問題ありませんが、%v で整形すると再び String() が呼ばれ、スタックがあふれるまで再帰します。

%d はそのまま数値を表示するので、Wednesday is day 3 のように両方の形が使えます。

stringerでStringを生成する

リストが長いときは、stringer ツールがメソッドを書いてくれます。

//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...

これは簡潔な String() の実装を持つ weekday_string.go を作り、さらに定数が変わったのに再生成していない場合にビルドを失敗させるコンパイル時のチェックも入れます。-linecomment フラグを使うと行末のコメントが名前として使われ、空白を含む名前に便利です。

値を検証する

Goの列挙型は閉じていません。基底型のどんな値でも変換でき、型なし定数は暗黙に変換されます。

var d Weekday = 42     // compiles
d = Weekday(userInput) // compiles

ですから、自分のコードの外(JSON、データベース、フラグ、他のパッケージ)から来る値はチェックします。

ブロックの末尾にある非公開の番兵 colorCount は常に最後の本物の値の1つ後ろにあるので、新しい色を追加しても IsValid が正しいままになります。

列挙型でswitchする

列挙型はたいてい switch で使われます。Goはswitchがすべての値を網羅しているかをチェックしないので、想定外の値を扱う default を加えておきます。

func (c Color) Hex() string {
	switch c {
	case Red:
		return "#ff0000"
	case Green:
		return "#00ff00"
	case Blue:
		return "#0000ff"
	default:
		return "#000000"
	}
}

サードパーティの exhaustive リンター(golangci-lint に含まれています)は、列挙型に対するswitchでケースが漏れているものを報告します。他の言語の網羅性チェックで得られるものの大半がこれで手に入ります。

ビットフラグの列挙型

権限のように値を組み合わせる場合は、1 << iota で値ごとに1ビットを使います。

| でフラグを組み合わせ、& で調べ、&^(GoのAND NOT演算子)で消します。ここでは符号なしの基底型が適しています。uint8 なら8個、uint64 なら64個のフラグを持てます。

文字列の列挙型

値をどのみちテキストとして保存・送信するなら、文字列ベースの型にすれば変換の層が要りません。

type Env string

const (
	EnvDev     Env = "dev"
	EnvStaging Env = "staging"
	EnvProd    Env = "prod"
)

String() メソッドなしで読みやすく表示・シリアライズでき、データベースのカラムには宣言順に依存する数値ではなく "prod" が入ります。トレードオフは、比較が文字列の比較になること、ビットフラグが使えないこと、そして Env("banana") もコンパイルできるので検証は依然として自分の仕事だということです。

列挙型とJSON

整数の列挙型は、デフォルトでは数値としてシリアライズされます。代わりに名前で読み書きするには、encoding.TextMarshalerencoding.TextUnmarshaler を実装します。encoding/json は値とマップのキーの両方でそれらを使います。

MarshalText は値レシーバーなので、Level でも *Level でも動きます。UnmarshalText は値を変更するのでポインタレシーバーが必要です。同じ2つのメソッドで、flag パッケージの TextVar やほとんどの設定ライブラリでもこの型が使えるようになります。

落とし穴

  • リテラルの暗黙の変換。 Weekday を受け取る関数は、型なし定数の 42 も受け付けます。拒否されるのは、別の型を持つ型付きの値だけです。
  • 最初の行の型を書き忘れる。 const ( Red = iota; Green; Blue ) では3つとも Color の値ではなく型なしの整数定数になり、Color のメソッドは使えません。繰り返される式が型を持つように、Red Color = iota と書きます。
  • 保存済みの列挙型を並べ替える。 iota ブロックの途中に値を挿入すると、すでに他の場所に保存されている番号が黙って変わります。
  • Stringの中での再帰。 String() の中でレシーバーを %v%s で整形してはいけません。先に基底型に変換します。

よくある質問

Goに列挙型(enum)はありますか?

言語機能としてはありません。enum キーワードはありません。Goらしい代替手段は、名前付きの型と、その型の定数のブロックで、通常は iota で番号を振ります。

type Color int

const (
	Red Color = iota
	Green
	Blue
)

型があることでシグネチャが読みやすくなり、String() のようなメソッドを付ける場所もできます。ただし Color(42) と書くことは防げないので、外部から来る値は検証してください。

Goのiotaとは何ですか?

iota は事前に宣言された識別子で、const ブロックの中で現在の行(定数仕様)のインデックスに等しく、0から始まります。新しい const ブロックごとに0に戻ります。式を省略した行では、Goは直前の式を次の iota で繰り返します。Red = iota; Green; Blue が0、1、2になるのはこのためです。

iotaを1から始めるには?

最初の行に First Kind = iota + 1 と書くか、ブランク識別子でゼロを飛ばします:_ = iota の次に First。ただ多くのGoプログラマーは0を残して UnknownInvalid と名付けます。そうすれば、初期化されていない変数(ゼロ値は0)が本当の選択肢ではないことがはっきりします。

Goで列挙型を文字列として表示するには?

型に String() string メソッドを付けます。fmt%v%sPrintln でそれを呼ぶので、fmt.Println(Green)1 ではなく Green と表示します。メソッドは switch や配列を使って手で書くことも、go run golang.org/x/tools/cmd/stringer@latest -type=Color で生成することもできます。

Goで文字列を列挙型に変換するには?

文字列を検索するパース関数を書きます。通常は map[string]Colorswitch を使い、不明な入力にはエラーを返します:func ParseColor(s string) (Color, error)。同じ処理で UnmarshalText を実装すると、JSON、フラグ、設定の読み込みが自動でそれを使うようになります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める