Goで手軽にコード生成したいときはgomplateを使うと楽

仕事で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の機能がないため、似たようなことを実現するには typeconst を組み合わせて実現することが多いと思います。

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としてはこれくらいでも十分なのではないでしょうか。

この記事で紹介したコードは以下のリポジトリにも置いてあります。

github.com

*1:2021年3月現在、2022年初頭リリース予定のGo1.18での追加が予定されています。https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md