Goでの列挙型を文字列ベースで作ってみる?

この記事は [弁護士ドットコム Advent Calendar 2022](https://qiita.com/advent-calendar/2022/bengo4com) の3日目の記事になります。

# Goでの列挙型どうしてますか?
こんにちは。
私は[弁護士ドットコム](https://www.bengo4.com/)株式会社の[クラウドサイン](https://www.cloudsign.jp/)事業本部で、Goを使ったバックエンドの開発に携わっています。


ところで、Goでの[列挙型](https://ja.wikipedia.org/wiki/%E5%88%97%E6%8C%99%E5%9E%8B)が欲しくなったとき、皆さんはどうしてますか?
私的には [iota](https://go.dev/ref/spec#Iota) が便利なんでよく使ってきました。


# 例題:フルーツバスケットを作ろう
列挙型を使う例題として、フルーツの種類を列挙型でコード化して、それを集めてバスケットに詰めるということをやってみたいと思います。

今回使ったGoのバージョンは以下のものです。
```console
% go version    
go version go1.19.2 darwin/amd64
```


ファイルを以下のような配置にします。
```
.
├── fruit
│   └── code.go
├── go.mod
└── main.go
```

モジュール名は、今回サンプルということでローカルのみで使用する `fruitbasket` にしてみます。
```go.mod
module fruitbasket

go 1.19
```

fruit/code.go でフルーツの種類を列挙型として定義します。
```fruit/code.go
package fruit

// Code はフルーツの種類を表す型
type Code int

const (
	Apple Code = iota + 1
	Banana
	Cherry
)

// Basket は Code を格納するバスケット
type Basket []Code
```
`Apple`などの定義には [iota](https://go.dev/ref/spec#Iota) を使うが、ゼロ値は未使用という形にするところも完璧ですね。

フルーツの定数定義ができたので　`main`　でバスケットを作り、中に入っているものをプリントします。

```go
package main

import (
	"fmt"
	"fruitbasket/fruit"
)

func main() {
	basket := fruit.Basket{
		fruit.Apple,
		fruit.Banana,
		fruit.Cherry,
	}
	for _, c := range basket {
		fmt.Println(c, "in the basket.")
	}
}
```

実行します。
```console
% go run .
1 in the basket.
2 in the basket.
3 in the basket.
```

以上でフルーツの種類を列挙型で定義するという目的は一応果たせました。

# iotaを使う場合の困りどころ
このように [iota](https://go.dev/ref/spec#Iota) はコード化して列挙したいものを簡単に定義できて便利なものです.
ところが使っていると困ったと感じることも出てきました。

その1つは、上記の例の `fruit.Code` の値のように定義された値が整数値なので、それをそのままログに出したりDBに保存したものを見た場合に、何を表すのかぱっとわからないことがあるということです。

もう1つは、一度 [iota](https://go.dev/ref/spec#Iota) で定義した定数は、その定義の順序を変えることができないということです。定義順を変えてしまうと、それぞれの定数の値が変わってしまってプログラム全体に影響を及ぼしてしまうので、それは避けたいところです。
例えば上の例に `Avocado` を追加したいと思ったら、一番最後に追加せざるを得ません。さもなくばプログラム全体の書き替えになってしまいます。
```
const (
	Apple Code = iota + 1
	Banana
	Cherry
	Avocado
)
```
定義順はアルファベット順にしておきたいと思っても、致し方なしということもあるわけです。

# fruit.Code を文字列ベースにしてみよう
`fruit.Code`　の値が意味するところをわかりやすくして、定義順も自由にしたいと思ったら、文字列ベースの型に変更するというアイデアが一つあります。

ではやってみます。
```fruit/code.go
...
// Code はフルーツの種類を表す型
type Code string

const (
	Apple  = Code("Apple")
	Banana = Code("Banana")
	Cherry = Code("Cherry")
)
...
```
```console
% go run .
Apple in the basket.
Banana in the basket.
Cherry in the basket.
```

いいですね。プリントの結果もわかりやすくなりました。

ちなみに以下のような定義の書き方をしてしまうと `Apple` は `Code` 型ですが、 **`Banana` と `Cherry` は `string` 型になってしまいます。**
注意が必要なところです。
```fruit/code.go
...
// Code はフルーツの種類を表す型
type Code string

const (
	Apple  Code = "Apple"  // Code
	Banana      = "Banana" // string
	Cherry      = "Cherry" // string
)
...
```

# ここでAvocado を追加してみよう
さて、`fruit.Code`型の定数が[iota](https://go.dev/ref/spec#Iota)を使わない定義になったので、　`Apple` の行をコピペして修正すれば好きなところに `Abocado` を追加定義できますね。

```fruit/code.go
...
const (
	Apple  = Code("Apple")
	Avocado = Code("Apple")
	Banana = Code("Banana")
	Cherry = Code("Cherry")
)
```
```main.go
....
func main() {
	basket := fruit.Basket{
		fruit.Apple,
		fruit.Avocado,
		fruit.Banana,
		fruit.Cherry,
	}
	for _, c := range basket {
		fmt.Println(c, "in the basket.")
	}
}
```
```console
% go run .
Apple in the basket.
Apple in the basket.
Banana in the basket.
Cherry in the basket.
```
あれあれ?　`Apple` が2つ。

実はわざと `Avocado` の定義の右辺を、コピペしたまま修正忘れという状態にしていました。

# 列挙型のベースに文字列型を使った場合の問題
列挙型のベースに文字列型を使うと自由度が増すのですが、その分、値を一つずつ手作業で定義する必要が出てきます。
また、その手作業のため typo の可能性があり、これはGoの文法チェックでは防げません。

# そこで: stringer を使ってみる
[stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) は整数定数に、その定数名と同じ文字列を返す String メソッドを定義するファイルを生成してくれるコマンドです。
これならば定義値となる文字列の typo を防げるのでは?

ということで[stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) を使うような改造を施していきます。

`fruit.Code`型の定数は元の[iota](https://go.dev/ref/spec#Iota)の定義に戻し、 [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer)　コマンドで　`fruit.Code.String`　メソッドの定義ファイルを生成します。

[stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) の実行には、`go install golang.org/x/tools/cmd/stringer` でコマンドをインストールしておいてそれを実行すればいいのですが、今回は `go generate` 経由で、またコマンドのインストールをやらずにやってみます。

そのため `fruit/code.go` に `//go:generate` の行を差し込みます。
また、`fruit.Backet`が保持する型を `string` に変更しておきます。
```fruit/code.go
package fruit

// Code はフルーツの種類を表す型
type Code int

//go:generate /usr/bin/env bash -c "echo generating strings for fruit.Code 1>&2"
//go:generate go run golang.org/x/tools/cmd/stringer -type=Code

const (
	Apple Code = iota + 1
	Banana
	Cherry
)

// Basket はフルーツの種類を表す文字列を格納するバスケット
type Basket []string
```

もとのディレクトリで `go generate ./...` などとやると、 `//go:generate` で指定されたコマンドが実行されます。
`//go:generate /usr/bin/env bash -c "echo ..."` の行はファイル生成のためには不要ですが、何やってるかわかるようにメッセージを出すためだけのものです。

ではやってみます。

```console
% go generate ./...
generating strings for fruit.Code #<-- echo による出力
no required module provides package golang.org/x/tools/cmd/stringer; to add it:
	go get golang.org/x/tools/cmd/stringer
fruit/code.go:7: running "go": exit status 1
```

エラーになりました。　`golang.org/x/tools/cmd/stringer` のパッケージがないと言われてます。
メッセージ通り　`go get golang.org/x/tools/cmd/stringer`　をすればいいんですが、今回はそうせずに、[stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer)への参照を含む `tools.go` を置いておくことにします。

```tools.go
//go:build tools

package tools

import (
	_ "golang.org/x/tools/cmd/stringer"
)
```

`//go:build tools` は tools.go ファイルをビルド対象に含めないためのタグの指定です。 `tools` 部分は何でもよくてビルド時に使わないタグを指定することでビルド対象から除外しておきます。

ここまでファイル構成がこうなりました。
```
.
├── fruit
│   └── code.go
├── go.mod
├── go.sum
├── main.go
└── tools.go　<- New
```

tools.go の設置が終わったらパッケージの依存情報を更新するため `go mod tidy` を実行します。
```console
% go mod tidy
go: finding module for package golang.org/x/tools/cmd/stringer
go: found golang.org/x/tools/cmd/stringer in golang.org/x/tools v0.3.0
```
なにやら[stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer)が取り込まれたようです。

では、再度 `go generate` をやってみます。

```console
% go generate ./...
generating strings for fruit.Code
```
これで `fruit/code_string.go` というファイルができてます。

```
.
├── fruit
│   ├── code.go
│   └── code_string.go <- New
├── go.mod
├── go.sum
├── main.go
└── tools.go
```

ここまで準備が整ったところで　`main` を修正します。　`fruit.Basket` には `fruit.Code.String` が返す文字列を追加します。
ちなみに、`fruit.Code.String`の定義は先ほど生成された`fruit/code_string.go`の中にあります。

```main.go
package main

import (
	"fmt"
	"fruitbasket/fruit"
)

func main() {
	basket := fruit.Basket{
		fruit.Apple.String(),
		fruit.Banana.String(),
		fruit.Cherry.String(),
	}
	for _, c := range basket {
		fmt.Println(c, "in the basket.")
	}
}
```
実行します。
```console
% go run .
Apple in the basket.
Banana in the basket.
Cherry in the basket.
```
できてます。

ここで`Avocado`の定数を好きな行に追加して、
```fruit/code.go
...
const (
	Apple Code = iota + 1
	Avocado
	Banana
	Cherry
)
...
```
```console
% go generate ./...
generating strings for fruit.Code
```

`main`でも`fruit.Avocado.String()`を追加します。
```main.go
func main() {
	basket := fruit.Basket{
		fruit.Apple.String(),
		fruit.Avocado.String(),
		fruit.Banana.String(),
		fruit.Cherry.String(),
	}
	for _, c := range basket {
		fmt.Println(c, "in the basket.")
	}
}
```
```console
% go run .
Apple in the basket.
Avocado in the basket.
Banana in the basket.
Cherry in the basket.
```
うまくいきました。

# 文字列ベースの新しい列挙型を作る
さて文字列型を使った定数定義がなんとかできたところですが、 `fruit.Backet`　の定義をもう一度見てみましょう。
```fruit/code.go
// Basket はフルーツの種類を表す文字列を格納するバスケット
type Basket []string
```
`fruit.Basket`には文字列がなんでも入ってしまうので、この定義だと `fruit.Code` という型を定義したことでの型チェックの恩恵を受けてないように思います。

そこで文字列ベースの列挙型として `fruit.Type` を定義して、　`fruit.Backet` の定義を `fruit.Type` を集めたものに修正します。
そして `fruit.Code`　にも`fruit.Type`型の値を返す `Type` というメソッドを追加します。

そんなこんなで、ちょっとした工夫も追加して最終的にできたものは以下になります。

```fruit/code.go
package fruit

import "fmt"

// Type はフルーツの種類を表す型
type Type string

// Code はフルーツの種類に対応したコード
type Code int

//go:generate /usr/bin/env bash -c "echo generating strings for fruit.Code 1>&2"
//go:generate go run golang.org/x/tools/cmd/stringer -type=Code

const (
	invalid Code = iota
	Apple
	Avocado
	Banana
	Cherry
	endOfCode
)

// Type は c を Type に変換したものを返す
func (c Code) Type() Type {
	if c <= invalid || c >= endOfCode {
		return Type(fmt.Sprintf("BadCode(%d)", c))
	}

	return Type(c.String())
}

// Basket は Type を格納するバスケット
type Basket []Type
```

```main.go
package main

import (
	"fmt"
	"fruitbasket/fruit"
)

func main() {
	basket := fruit.Basket{
		fruit.Apple.Type(),
		fruit.Avocado.Type(),
		fruit.Banana.Type(),
		fruit.Cherry.Type(),
	}
	for _, c := range basket {
		fmt.Println(c, "in the basket.")
	}
}
```

```console
% go generate ./...
generating strings for fruit.Code
% go run .         
Apple in the basket.
Avocado in the basket.
Banana in the basket.
Cherry in the basket.
```

# おわりに
文字列の列挙型を作るべく、 [const](https://go.dev/ref/spec#Constant_declarations) と [iota](https://go.dev/ref/spec#Iota) を使うことで重複のない定数を定義し、それを [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) コマンドの結果で文字列化したものを、列挙用の型に変換して使うという試みをやってみました。
上記の例では `fruit.Code` 型の定数を直接使わず、かならず `fruit.Code.Type` メソッドの結果を使うという約束事を守る注意点はあります。

それにしても [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) が生成するコードはコンパクトで素晴らしいと思います。さらにファイル生成後に元ファイルだけを変更してしまうとコンパイルエラーになり、これによってファイルの不整合が防がれる仕組みになっていて、こんな事ができるんだと本当に感心します。

説明がくどくなっちゃったかな、と思いつつ、ここまで長々とお付き合いいただきましてありがとうございました。こうしたほうがいいよとかありましたら教えていただけると嬉しいです。

明日は @Sympatisk_Hedgehog3 さんです。お楽しみに。