Go と reflect と generate

この記事は [Go Advent Calendar 2015](http://qiita.com/advent-calendar/2015/go) 5 日目の記事です。

# はじめに

Go にはジェネリクスがありませんが、任意の型に対して共通の処理を提供したいことがあります。
例えば ORM ライブラリで `User` テーブルと `Blog` テーブルを struct で表す場合を考えてみます。

```go
type User struct {
	Id    int64
	Name  string
	Email string
}

type Blog struct {
	Id    int64
	Title string
	Body  string
}
```

この 2 つの struct に対して共通の処理、例えば id で検索して結果を返す `FindById` というメソッドを提供したいとします。

Go は静的型付き言語なので、複数の型を引数に取ったり、複数の型を返すといったメソッドを定義することはできません。
よって、それぞれの型に対してメソッドを定義することになります。

```go
package main

var db = func() *sql.DB {
	db, err := sql.Open("mysql", "hoge")
	if err != nil {
		panic(err)
	}
	return db
}()

func (u User) FindById(id int64) (*User, error) {
	return &u, db.QueryRow("SELECT Id, Name, Email FROM User WHERE Id = ?", id).Scan(&u.Id, &u.Name, &u.Email)
}

func (b Blog) FindById(id int64) (*Blog, error) {
	return &b, db.QueryRow("SELECT Id, Title, Body FROM Blog WHERE Id = ?", id).Scan(&b.Id, &b.Title, &b.Body)
}

func main() {
	u, err := User{}.FindById(1)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", u)
	b, err := Blog{}.FindById(1)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b)
}
```

これでは ORM ライブラリは事前に全ての struct を知った上で全ての struct に対して `FindById` メソッドを定義しないといけません。
今回はこの問題に対する 3 つの方法を紹介します。

# reflect を使う方法

[reflect][] とは ~~闇~~ 実行時に型情報や名前を取得したり、値を書き換えたりするためのパッケージです。実行時リフレクション (run-time reflection) と言ったほうがわかりやすいかもしれません。
これと `interface{}` を使うことによって先ほどの問題を解決できます。

拙作の ORM ライブラリ [genmai](https://github.com/naoina/genmai) もこの方法を使っています。

```go
package main

type ORM struct{}

func (o *ORM) FindById(out interface{}, id int64) error {
	rv := reflect.ValueOf(out).Elem()
	rt := rv.Type()
	var names []string
	var args []interface{}
	for i := 0; i < rv.NumField(); i++ {
		names = append(names, rt.Field(i).Name)
		args = append(args, rv.Field(i).Addr().Interface())
	}
	columns := strings.Join(names, ",")
	return db.QueryRow(fmt.Sprintf("SELECT %s FROM %s WHERE Id = ?", columns, rt.Name()), id).Scan(args...)
}

func main() {
	orm := &ORM{}
	u := User{}
	if err := orm.FindById(&u, 1); err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", u)
	b := Blog{}
	if err := orm.FindById(&b, 1); err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b)
}
```

## Pros:

- お手軽（簡単とは言ってない）
- ライブラリだけで完結する

## Cons:

- 実行時に panic する可能性がある
- `interface{}` を使うので静的型付きが有名無実化する

1 つ目は頑張ればなんとかできるとしても、2 つ目は静的型付き言語を使う以上できれば避けたいものです。

# コード生成を使う方法

`go:generate` もとい、コード生成です。
はじめに書いた、全ての struct に対してメソッドを定義するというのをプログラムで行います。
対象となる struct の情報は、ソースファイルを AST に変換して解析するという方法が使われることが多いようです。
この際に出力するコードのテンプレートとしてよく使われるのが [text/template][] で、[gen](https://clipperhouse.github.io/gen/) や [argen](https://github.com/monochromegane/argen) でも使われています。
なお下記のサンプルコードは簡略化のため struct の情報を直接書いています。

```go:main.go
package main

import (
	"fmt"
	"go/build"
	"os"
	"strings"
	"text/template"
)

var tmpl = template.Must(template.New("").Parse(`package {{.Package}}

func (o {{.Name}}) FindById(id int64) (*{{.Name}}, error) {
	return &o, db.QueryRow("SELECT {{.Columns}} FROM {{.Name}} WHERE Id = ?", id).Scan({{.Fields}})
}
`))

func Generate(name string, columns ...string) error {
	pkg, err := build.Default.ImportDir(".", 0)
	if err != nil {
		panic(err)
	}
	fields := make([]string, 0, len(columns))
	for _, c := range columns {
		fields = append(fields, fmt.Sprintf("&o.%s", c))
	}
	f, err := os.Create(fmt.Sprintf("%s_gen.go", strings.ToLower(name)))
	if err != nil {
		return err
	}
	defer f.Close()
	return tmpl.Execute(f, map[string]interface{}{
		"Package": pkg.Name,
		"Name":    name,
		"Columns": strings.Join(columns, ", "),
		"Fields":  strings.Join(fields, ", "),
	})
}

func main() {
	if err := Generate("User", "Id", "Name", "Email"); err != nil {
		panic(err)
	}
	if err := Generate("Blog", "Id", "Title", "Body"); err != nil {
		panic(err)
	}
}
```

生成されるファイルは下記です。

```go:user_gen.go
package main

func (o *User) FindById(id int64) error {
	return db.QueryRow("SELECT Id, Name, Email FROM User WHERE Id = ?", id).Scan(&o.Id, &o.Name, &o.Email)
}
```

```go:blog_gen.go
package main

func (o *Blog) FindById(id int64) error {
	return db.QueryRow("SELECT Id, Title, Body FROM Blog WHERE Id = ?", id).Scan(&o.Id, &o.Title, &o.Body)
}
```

## Pros:

- 実行時に panic しない
- 静的型付きのメリットを享受できる

## Cons:

- 変更があるたびにコードを生成しなおさなければいけない
- 生成されるコードを事前にテストできない
- テンプレートは文字列なのでコード補完やシンタックスチェッカーが使えない

2 つめは、コードを生成した後のコードにテストを書けば良さそうですが、

*テンプレート変更 -> コード生成 -> テスト*

という手順を踏む必要があり煩雑です。

# AST からコード生成する方法

文字列テンプレートからのコード生成の問題を解決するために、実際に動くソースコードそれ自体をテンプレートにします。
実際に動くソースコード自体をテンプレートにするにはどうすればいいかというと、ソースコードを文字列として読み込んでプレースホルダーとなる識別子を [strings.Replace](https://golang.org/pkg/strings/#Replace)、あるいは [regexp](https://golang.org/pkg/regexp/) でガリガリ書き換えていくという方法もあるにはありますが、文字列の中は書き換えたくないとか、コメントの中は書き換えたくないとか、あると思うので、AST を使います。

幸いにも Go には AST ライブラリ [go/ast][] が標準で備わっていますので、これを使って頑張ることができます。

```go:template.go
package main

func (o PlaceHolder) FindById(id int64) (*PlaceHolder, error) {
	return &o, db.QueryRow("SELECT "+o.columns()+" FROM User WHERE Id = ?", id).Scan(o.fields(&o)...)
}

func (o PlaceHolder) columns() string {
	return "Id, Name, Email"
}

func (o PlaceHolder) fields(p *PlaceHolder) []interface{} {
	return []interface{}{&o.Id, &o.Name, &o.Email}
}
```

```go:main.go
package main

import (
	"fmt"
	"go/ast"
	"go/build"
	"go/format"
	"go/parser"
	"go/token"
	"os"
	"strconv"
	"strings"
)

func GenerateFromAST(name string, columns ...string) error {
	pkg, err := build.Default.ImportDir(".", 0)
	if err != nil {
		panic(err)
	}
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "template.go", nil, parser.ParseComments)
	f.Name.Name = pkg.Name
	rewriteASTNode(f, name, columns)
	file, err := os.Create(fmt.Sprintf("%s_gen.go", strings.ToLower(name)))
	if err != nil {
		return err
	}
	defer file.Close()
	return format.Node(file, fset, f)
}

func rewriteASTNode(f ast.Node, structName string, columns []string) {
	ast.Inspect(f, func(n ast.Node) bool {
		switch aType := n.(type) {
		case *ast.Ident:
			if strings.Contains(aType.Name, "PlaceHolder") {
				aType.Name = strings.Replace(aType.Name, "PlaceHolder", structName, 1)
			}
		case *ast.FuncDecl:
			if aType.Recv == nil {
				break
			}
			switch aType.Name.Name {
			case "columns":
				aType.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.BasicLit).Value = strconv.Quote(strings.Join(columns, ", "))
			case "fields":
				recvName := aType.Recv.List[0].Names[0].Name
				clit := aType.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.CompositeLit)
				clit.Elts = clit.Elts[:0]
				for _, c := range columns {
					clit.Elts = append(clit.Elts, &ast.UnaryExpr{
						Op: token.AND,
						X: &ast.SelectorExpr{
							X:   ast.NewIdent(recvName),
							Sel: ast.NewIdent(c),
						},
					})
				}
			}
		}
		return true
	})
}

func main() {
	if err := GenerateFromAST("User", "Id", "Name", "Email"); err != nil {
		panic(err)
	}
	if err := GenerateFromAST("Blog", "Id", "Title", "Body"); err != nil {
		panic(err)
	}
}
```

生成されるファイルは下記です。

```go:user_gen.go
package main

func (o User) FindById(id int64) (*User, error) {
	return &o, db.QueryRow("SELECT "+o.columns()+" FROM User WHERE Id = ?", id).Scan(o.fields(&o)...)
}

func (o User) columns() string {
	return "Id, Name, Email"
}

func (o User) fields(p *User) []interface{} {
	return []interface{}{&o.Id, &o.Name, &o.Email}
}
```

```go:blog_gen.go
package main

func (o Blog) FindById(id int64) (*Blog, error) {
	return &o, db.QueryRow("SELECT "+o.columns()+" FROM User WHERE Id = ?", id).Scan(o.fields(&o)...)
}

func (o Blog) columns() string {
	return "Id, Title, Body"
}

func (o Blog) fields(p *Blog) []interface{} {
	return []interface{}{&o.Id, &o.Title, &o.Body}
}
```

## Pros:

- 実行時に panic しない
- 静的型付きのメリットを享受できる
- 生成されるコードを事前にテストすることができる
- テンプレートを書くときにコード補完やシンタックスチェッカーを使える

## Cons:

- ご覧の通り全て AST で組み立てる必要があるため複雑
- 書き換えられる部分の構造を少しでも変えると動かなくなる

# まとめ

任意の型に対して共通の処理を提供する方法として [reflect][] を使った方法、 [text/template][] を使ったコード生成、AST からコード生成する方法の 3 つを紹介しました。
どれも一長一短ありますが、個人的には事前にテストができるのと、コード補完が効くという理由で AST からコード生成する方法が好きです。

これらの他にもより良い方法があればぜひお知らせください。

[reflect]: https://golang.org/pkg/reflect/
[text/template]: https://golang.org/pkg/text/template/
[go/ast]: https://golang.org/pkg/go/ast/