仕事でGo言語をよく書くのですが、Goは言語機能が多くはないためボイラープレートやコピペが増えがちです。
特に他の言語でいうところのgenericsがあれば解決するのに、と思うことがよくあるのですが、現在のGoにはそのような機能は存在しない*1ため、go generate
を活用したコード生成で代用することが多いと思います。
一方で、目的のコードを生成できるツールが存在しない場合、ツールをゼロから自分で実装するのは手間なので、本来はコード生成したい場合でも、実際にはコピペで済ませてしまうことがよくあります。
何か良い手はないかと探していたのですが、gomplateを使うと手軽に目的を達成できたので紹介します。
gomplate とは
gomplateはGoで書かれたテンプレートレンダラーで、さまざまなデータソースからデータの入力を受け付け、それをGoのtemplateにレンダリングするコマンドラインツールです。
例えば以下のようにしてテンプレートをレンダリングできます。
$ gomplate -i 'Hello, {{ .Env.USER }}' Hello, maku693
gomplateではGoのtemplateの標準的な機能に加えて、文字列を加工する関数や、YAMLや環境変数などをデータソースとして受け取る仕組みが提供されており、コード生成を含むさまざまな場面で活用できます。
例えば、enum風のconstを定義する時に使えます。
Goにはいわゆるenumの機能がないため、似たようなことを実現するには type
と const
を組み合わせて実現することが多いと思います。
package main import ( "fmt" "strconv" ) type Animal int const ( Dog Animal = iota Cat Bird ) func (a Animal) Validate() error { switch a { case Dog, Cat, Bird: return nil } return fmt.Errorf("invalid animal: %d", a) } func (a Animal) String() string { switch a { case Dog: return "Dog" case Cat: return "Cat" case Bird: return "Bird" } return "%!Animal(" + strconv.Itoa(int(a)) + ")" }
これくらいなら手で書けそうですが、enumが欲しくなるたびにこれらを毎回定義しなければならないとなると面倒です。
また、String()
メソッドだけならgolang.org/x/tools/cmd/stringerを使えば十分ですが、ほかのメソッドも必要だったり、例示した以外の関数やメソッドが各enumに対して必要になったとき、それぞれの型に対していちいち実装を追加するのは手間です。
これらの問題を解消するには、ゼロからコード生成ツールを作ってもいいのですが、gomplateを使うと、テンプレートとデータソースを用意するだけで目的が達成できます。
まず、enumを定義するためのテンプレートを用意します。
{{- $source := datasource "source" -}} // Code generated by go generate; DO NOT EDIT. package {{ $source.package }} import ( "fmt" "strconv" ) type {{ $source.type }} int const ( {{- range $i, $value := $source.values }} {{ $value }} {{ $source.type }} = {{$i}} {{- end }} ) func (v {{$source.type}}) Validate() error { switch v { {{- range $value := $source.values }} case {{ $value }}: return nil {{- end }} } return fmt.Errorf("invalid {{ $source.type | strings.ToLower }}: %d", v) } func (v {{$source.type}}) String() string { switch v { {{- range $value := $source.values }} case {{ $value }}: return "{{ $value }}" {{- end }} } return "%!{{$source.type}}(" + strconv.Itoa(int(v)) + ")" }
次に、このテンプレートに渡すデータソースを用意します。今回はYAMLを使います。
package: main type: Animal values: - Dog - Cat - Bird
最後に、これらをgomplateを使ってレンダリングするファイルを用意し、go generate
コマンドでレンダリングします。
package main //go:generate go run github.com/hairyhenderson/gomplate/v3/cmd/gomplate --datasource source=animal.yml --file enum.go.tmpl --out animal_gen.go
$ go generate .
これで以下のようなファイルが生成されます。
// Code generated by go generate; DO NOT EDIT. package main import ( "fmt" "strconv" ) type Animal int const ( Dog Animal = 0 Cat Animal = 1 Bird Animal = 2 ) func (v Animal) Validate() error { switch v { case Dog: return nil case Cat: return nil case Bird: return nil } return fmt.Errorf("invalid animal: %d", v) } func (v Animal) String() string { switch v { case Dog: return "Dog" case Cat: return "Cat" case Bird: return "Bird" } return "%!Animal(" + strconv.Itoa(int(v)) + ")" }
最初に示したコードとは若干異なりますが、実用上は変わりありません。
他のenumを生成したければ、あらたなYAMLを用意して、go:generateコメントを追加するだけですし、実装を追加したい場合は、テンプレートを変更して再度コードを生成しなおすだけなので簡単です。
この例以外にも、テンプレートとデータソースを工夫すれば、structの特定のフィールドをキーにしてソートする関数を生成したり、あるstructのsliceを別のstructのsliceに詰め替える関数を生成したりもできます。
gomplateにはGoのコード生成専用の高度な機能は存在しないので、リフレクションを使いたいなど要件が複雑な場合は自前でコード生成ツールを書いた方がよいかもしれませんが、貧者のgenericsとしてはこれくらいでも十分なのではないでしょうか。
この記事で紹介したコードは以下のリポジトリにも置いてあります。
*1:2021年3月現在、2022年初頭リリース予定のGo1.18での追加が予定されています。https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md