🍊

Goにおける列挙子と網羅性: コードジェネレータ作りました

2023/08/02に公開

はじめに

https://www.youtube.com/watch?v=TOUkp_Dxb9w
↑こちらの短い発表に大変刺激を受け、紹介されているパターンの実装を支援するコード生成ツールを作成しました…!

https://github.com/daichitakahashi/go-enum

Goの列挙子

Goでは言語機能としてenumというものがなく、constiotaを使った定数が代わりに使用されることが多いです。

https://github.com/golang/go/blob/64c2072a94281fe5b19f9349b522881751347726/src/unicode/letter.go#L69-L75

慣れてしまえばこのスタイルで問題なくコードをかけるのですが、業務でコードを書いていると↓のように思うことは多いはず。

  • 網羅性チェックしたい
    const (
        patternA = iota
        patternB
        patternC
    )
    
    switch c {
    case patternA:
        // ...
    case patternB:
        // ...
    // case patternC: // チェック漏れを検知することが難しい
    default:
        panic("unreachable code") // できればこれは書きたくない
    }
    
  • 列挙子による条件分岐+その値に応じた型アサーションをもっとイケてる方法で書きたい(TypeScriptでいうdiscriminated unionのような)
    // var v any
    switch c {
    case patternA:
        v.(typeA).DoWithA() // patternAとtypeAの結びつきが型レベルでは表現されていない
    case patternB:
        v.(typeB).DoWithB()
    }
    

チーム開発をやっていると、より安全な方法でコードを書きたくなるものですね。

はじめに紹介した発表では、これらを解決するための列挙型実装のパターンが紹介されています。
その他の困りどころや具体的な解決方法については動画を見ていただくのが一番なので、続いてツールの紹介をしていきます。

github.com/daichitakahashi/go-enum

このパッケージを利用して、列挙型を作ってみましょう。

列挙型とメンバーの定義

列挙型のインターフェースとそのメンバーとなる型は、自分で定義する必要があります。
次のようなコードを準備しましょう。
https://github.com/daichitakahashi/go-enum/blob/72136835dda22addbf8249ae337c6716c933da57/example/fruits/fruits.go#L1-L13

Fruits型が列挙型のインターフェース、Apple, Orange, Grapeがそのメンバーとなる型です。
メンバーはenum.MemberOf[Fruits]をベースにしたDefined typeです。enum.MemberOfはこのgo-enumパッケージが公開する唯一の型となっています。

コード生成

上記のコードにはgo:generateディレクティブが含まれていますので、あとはgo generateを実行するだけです。
すると、以下のようなコードが生成されます。
https://github.com/daichitakahashi/go-enum/blob/72136835dda22addbf8249ae337c6716c933da57/example/fruits/enum.gen.go#L1-L26

めでたく、列挙型が完成しました!

enum.MemberOf

この型は何の実装も持たない空の構造体となっており、コード生成の対象となる列挙型とそのメンバーをマークするためにあります。enumgenコマンドはワーキングディレクトリのGoコードからマークされた型定義を収集し、コード生成の対象とします。

設定ファイルやコマンドライン引数ではなく型定義を使う方法が自分でも気に入っています[1]

以下のように、メンバーとなる構造体に埋め込むことでもマーク可能です。
https://github.com/daichitakahashi/go-enum/blob/72136835dda22addbf8249ae337c6716c933da57/example/event/event.go#L11-L30

Visitor型とAcceptメソッドの名前をカスタマイズする

enumgenコマンドの--visitorオプションと--acceptオプションを仕様することで、型名とメソッド名をカスタマイズすることが可能です。

https://github.com/daichitakahashi/go-enum/blob/72136835dda22addbf8249ae337c6716c933da57/example/event/event.go#L9

https://github.com/daichitakahashi/go-enum/blob/72136835dda22addbf8249ae337c6716c933da57/example/event/enum.gen.go#L1-L26

まとめ

動画を見てください!それが一番大事です。
ちょっとしたツールですが github.com/daichitakahashi/go-enum もぜひ使ってみてください。

コード生成ツールは作るのも使うのも楽しいです。

脚注
  1. 型がimportされることでgo.modでのバージョン管理対象になるため、コード生成につかうコマンドもばっちり管理対象となるのも良いところ。ということは、例のgo:generateディレクティブで書いているバージョン指定(@latest)は不要ですね…! ↩︎

Discussion