Golang で enum 。 enum に多様な属性や振る舞いを効率よく実装したい。

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

https://qiita.com/kyoh86/items/18b8bfc6ffe045aaf380


自分だったらどうするかな、というのを書いてみました。２つ目の記事でも指摘されているような Switch 文はたしかに書きたくないので、配列のインデックスアクセスにする方法を採用しています。

個人的には `enum` は固有の情報を色々持てるのが嬉しいので、単に「分かりやすい文字列で識別できればいい」というわけではないんですよね。以下の例でも

- 日本語での表現
- 英語での表現
- 次の季節が何か
- 前の季節は何か
- めぐる季節の順序判定

などの機能が `enum` の値に応じてちゃんと挙動を変えて動いてくれるようになっています。


### 使い方：

```go
	hoge := enum.SeasonValueOf("ほげ")
	fmt.Printf("'ほげ' は '%s' の季節です。\n", hoge.Ja())

	fmt.Println("--------------------")
	w := enum.SeasonValueOf("WINTER")
	fmt.Printf("'WINTER' は日本語だと '%s' です。\n", w.Ja())
	fmt.Printf("'WINTER' は英語ならば '%s' です。\n", w.En())
	fmt.Printf("'WINTER' の次の季節は '%s' です。\n", w.Next().En()+"（"+w.Next().Ja()+"）")

	fmt.Println("--------------------")
	a := enum.SeasonValueOf("秋")
	fmt.Printf("'%s' は '%s' の次の季節でしょうか？： %t 。\n", w.Ja(), a.Ja(), w.IsNextOf(enum.Autumn))
```

### 出力結果：

```sh
$ go run sample.go
'ほげ' は '未定義' の季節です。
--------------------
'WINTER' は日本語だと '冬' です。
'WINTER' は英語ならば 'Winter' です。
'WINTER' の次の季節は 'Spring（春）' です。
--------------------
'冬' は '秋' の次の季節でしょうか？： true 。
```

### 実装：

```go
package enum

import "strings"

type Season int

// enum の本体。実体は iota による int 値なのはよくあるそのまま。 [1]
const (
	Unknown Season = iota
	Spring
	Summer
	Autumn
	Winter
)

func (s Season) Ja() string                 { return s.get().Ja }        // 日本語表記文字列を返す
func (s Season) En() string                 { return s.get().En }        // 英語表記文字列を返す
func (s Season) Next() Season               { return s.get().Next }      // 次の季節を返す
func (s Season) Prev() Season               { return s.get().Prev }      // 前の季節を返す 
func (s Season) IsNextOf(other Season) bool { return s.Prev() == other } // 次の季節かどうかを判定
func (s Season) IsPrevOf(other Season) bool { return s.Next() == other } // 前の季節かどうかを判定
func (s Season) String() string             { return s.Ja() }            // ログ出力を考慮して String() も実装しておく

// 文字列表現などから enum を生成する関数。ウェブ開発などでビュー層からもらった文字列から作りたいときによく使う。
func SeasonValueOf(s string) Season { // hash lookup is more suitable than linear search?
	for _, season := range seasons {
		if strings.EqualFold(s, season.En) || s == season.Ja {
			return season.V
		}
	}
	return Unknown
}

// Season enum の多様な属性（情報）を表現する構造体の型
type attr struct {
	V    Season
	Ja   string
	En   string
	Next Season
	Prev Season
}

// enum の値に応じた属性（情報）を実際に定義している。 [2]
var seasons = [cnt]attr{
	{Unknown, "未定義", "unknown", Unknown, Unknown},
	{Spring, "春", "Spring", Summer, Winter},
	{Summer, "夏", "Summer", Autumn, Spring},
	{Autumn, "秋", "Autumn", Winter, Summer},
	{Winter, "冬", "Winter", Spring, Autumn},
}

// enum のバリエーション数。これを定数化し [2] と [3] 　の両方に使っているのがみそだと思っている。
// enum が拡張されて [1] や [2] のバリエーション数が増えた場合、この cnt も変更され、 [３] の境界条件判定も自動で更新される。
const cnt = 5

// enum 値から属性情報をひっぱってくるメソッド。ここでありえない enum 値を制御できる。 [3]
func (s Season) get() attr {
	if s < 0 || s >= cnt {
		return seasons[0] // or panic("Illegal enum value.")
	}
	return seasons[s]
}
```

### ポイント：

１．
enum の各値における多様な表現や機能を関数として簡単に呼び出せます。

```go
enum.Summer.Ja()   //=> "夏"
enum.Winter.En()   //=> "Winter"
enum.Winter.Prev() //=> enum.Autumn
```

２．
構造体やその配列などの構造はすべて隠蔽されていおり、使う側からは触れません。

３．
enum に新しい情報や機能を持たせる横方向の拡張も enum 自体の値を増やす縦方向の拡張もどちらも容易に書けます。その際下記足りない項目があるとコンパイルエラーになって気付けます。

４．
元記事だと、存在しない整数値を無理やり作れてしまうのでその範囲確認をしていましたが、そこの上限値を定数化（ `cnt` ）し、さらに詳細な情報を持つ構造体の配列の Length 値にも流用しています。こうすることでのちのち enum を拡張して要素を増やした際に `cnt` を修正し忘れているとコンパイルエラーになり気付くことができます。

```go
var seasons = [cnt]attr{
```

```go
const cnt = 5
```

```go
	if s < 0 || s >= cnt {
```



### 感想

順調に Golang を学び、楽しんでおります。