Goのコード生成ツールを作った

September 20, 2020

knifeについて

knifeというツールを細々と作っています。 ざっくり言うとgo listの型情報版です。

-fオプションでテンプレートを指定してあげると表示する情報を変えることができます。 次の例では、net/httpパッケージ内でcontext.Context型のフィールドを持つパッケージスコープの型を表示しています。

$ knife -f '{{- range .Types -}}
    {{- $t := . -}}
    {{- with struct . -}}
        {{- range .Fields -}}
            {{- if identical . (typeof "context.Context") -}}
                {{- $t.Name}} - {{pos .}}{{br}}
            {{- end -}}
        {{- end -}}
    {{- end -}}
{{- end -}}' "net/http"

Request - /usr/local/go/src/net/http/request.go:319:2
http2ServeConnOpts - /usr/local/go/src/net/http/h2_bundle.go:3878:2
http2serverConn - /usr/local/go/src/net/http/h2_bundle.go:4065:2
http2stream - /usr/local/go/src/net/http/h2_bundle.go:4146:2
initALPNRequest - /usr/local/go/src/net/http/server.go:3393:2
timeoutHandler - /usr/local/go/src/net/http/server.go:3241:2
wantConn - /usr/local/go/src/net/http/transport.go:1162:2

hagane

haganeはknifeのサブプロジェクトで、knifeの機能を使ってコード生成を行うためのコマンドラインツールです。

例えば、次のようなコードがあった場合を考えます。

package sample

type DB interface {
	Get(id string) int
	Set(id string, v int)
}

type db struct {}

func (db) Get(id string) int {
	return 0
}

func (db) Set(id string, v int) {}

このコードから次のようなDBインタフェースを実装したモック用のコードを生成したいと思います。

package sample

type MockDB struct {
	GetFunc func(id string) int
	SetFunc func(id string, v int)
}

func (m *MockDB) Get(id string) int {
	return m.GetFunc(id)
}

func (m *MockDB) Set(id string, v int) {
	m.SetFunc(id, v)
}

haganeを使うと次のように、テンプレートと追加データを渡すことで、型情報を元にコード生成を行うことができます。 テンプレートには-dataで追加データが渡せ、テンプレート内で使えるdata関数から取得できます。

$ hagane -template template.go.tmpl -o sample_mock.go -data {"type":"DB"} sample.go

テンプレートは次のように定義されています。

{{with index .Types (data "type")}}{{if interface .}}

package {{(pkg).Name}}

type Mock{{data "type"}} struct {
{{- range $n, $f := methods .}}
	{{$n}}Func {{$f.Signature}}
{{- end}}
}

{{range $n, $f := methods .}}
func (m *Mock{{data "type"}}) {{$n}}({{range $f.Signature.Params}}
	{{- .Name}} {{.Type}},
{{- end}}) ({{range $f.Signature.Results}}
	{{- .Name}} {{.Type}},
{{- end}}) {
	{{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}}
		{{- .Name}},
	{{- end}})
}
{{end}}
{{end}}
{{end}}

テンプレートはGo標準のtext/templateパッケージを用いており、テンプレートに展開されるデータとして型情報として*knife.Package型の値が渡されます。

knife.Package型はtypes.Package型をラップした型で、テンプレートに展開しやすいようにフィールドとして各種情報を保持しています。

型情報とテンプレート内で使える関数を駆使することで、自前で静的解析を行わなくてもコード生成が簡単に行えます。

これから

haganeはさっき(2020年9月25日)に作り始めたツールなので、私の都合でガンガン変更される可能性があります。 もし、こういう用途で使えそう!みたいなアイデアがあったら@tenntennまで教えて下さい。