Go 言語のコード解析とコード生成でサーバークライアント間でデータ構造や定数を共有するパターン

# はじめに

* 趣味開発でサーバーサイドを Go クライアントサイドを Unity でアプリを作っており [go-langconv](https://github.com/nirasan/go-langconv) という自作のコマンドで通信プロトコルや定数などをサーバークライアント間で共有している
* go-langconv については[前にも書いた](http://qiita.com/nirasan/items/e21bbc8907e70ffb9a98)が、更新をしてみて Go でソースの解析と生成をするときに汎用的に使えそうなパターンがいくつかあったのでまとめてみる

# ディレクトリを指定して再帰的にファイルを処理する

* 対象のディレクトリ以下のすべてのファイルに対して処理を行いたい場合は "path/filepath" の Walk が便利
* `filepath.Walk(string, func(string, os.FileInfo, error) error)` という関数で、第一引数にディレクトリ名、第二引数に各ファイルに対して実行する関数を指定する
* go-langconv での実際の呼び出し箇所は以下のような感じ

```go:main.go
func main() {
	# ...
	filepath.Walk(*dirFlag, walker)
	# ...
}

func walker(path string, info os.FileInfo, err error) error {
	# info にファイル情報が入ってくる
	# ここではディレクトリか .go 以外の場合に処理をしないようにしている
	if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") {
		# error はとりあえず nil を返しておけばいい
		return nil
	}
	# ソースの解析の実行
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
	if err != nil {
		log.Fatal(err.Error())
	}
	for _, decl := range f.Decls {
		tdecl, ok := decl.(*ast.GenDecl)
		if !ok {
			continue
		}
		switch tdecl.Tok {
		case token.CONST:
			if g := NewConstDeclGroup(decl); g != nil {
				constDeclGroupList = append(constDeclGroupList, g)
			}
		case token.TYPE:
			if sd := NewStructDecl(decl); sd != nil {
				structDeclList = append(structDeclList, sd)
			}
		}
	}
	return nil
}
```

# 定義に関連するコメントを取得してパラメータとして処理する

* ソース解析の基本的なところは[こわくない！今日からはじめるGo言語コード生成](http://qiita.com/nirasan/items/bb0a239641028312b4db)にまあまあ書いた
* 型定義や関数定義のすぐ上の行に書いたコメントはその定義と関連付けられるので、ここに任意の文字列を記載してソース解析の対象にしたり生成時のパラメータにしたりできる

## ソース解析時にコメントも取得する

* `parser.ParseFile` で第四引数に `parser.ParseComments` を指定する

```go:main.go
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
	if err != nil {
		log.Fatal(err.Error())
	}
```

## 定義に関連するコメントを確認する

* 定義に関連するコメントは構文木データの定義のデータ内 `Doc` フィールドに入ってくる
* 例えばコメント付きの定数定義とその構文木データは以下のようになる

```go
// +langconv enum:MyEnum
const CONST_SINGLE bool = true
```

```
    73  .  .  1: *ast.GenDecl {
                 // コメント
    74  .  .  .  Doc: *ast.CommentGroup {
    75  .  .  .  .  List: []*ast.Comment (len = 1) {
    76  .  .  .  .  .  0: *ast.Comment {
    77  .  .  .  .  .  .  Slash: 8:1
    78  .  .  .  .  .  .  Text: "// +langconv enum:MyEnum"
    79  .  .  .  .  .  }
    80  .  .  .  .  }
    81  .  .  .  }
    82  .  .  .  TokPos: 9:1
    83  .  .  .  Tok: const
    84  .  .  .  Lparen: -
    85  .  .  .  Specs: []ast.Spec (len = 1) {
    86  .  .  .  .  0: *ast.ValueSpec {
                       // 定数名
    87  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
    88  .  .  .  .  .  .  0: *ast.Ident {
    89  .  .  .  .  .  .  .  NamePos: 9:7
    90  .  .  .  .  .  .  .  Name: "CONST_SINGLE"
    91  .  .  .  .  .  .  .  Obj: *ast.Object {
    92  .  .  .  .  .  .  .  .  Kind: const
    93  .  .  .  .  .  .  .  .  Name: "CONST_SINGLE"
    94  .  .  .  .  .  .  .  .  Decl: *(obj @ 86)
    95  .  .  .  .  .  .  .  .  Data: 0
    96  .  .  .  .  .  .  .  }
    97  .  .  .  .  .  .  }
    98  .  .  .  .  .  }
                       // 型
    99  .  .  .  .  .  Type: *ast.Ident {
   100  .  .  .  .  .  .  NamePos: 9:20
   101  .  .  .  .  .  .  Name: "bool"
   102  .  .  .  .  .  }
                       // 値
   103  .  .  .  .  .  Values: []ast.Expr (len = 1) {
   104  .  .  .  .  .  .  0: *ast.Ident {
   105  .  .  .  .  .  .  .  NamePos: 9:27
   106  .  .  .  .  .  .  .  Name: "true"
   107  .  .  .  .  .  .  }
   108  .  .  .  .  .  }
   109  .  .  .  .  }
   110  .  .  .  }
   111  .  .  .  Rparen: -
   112  .  .  }
```

## 定義に関連するコメントを取得して処理する

```go:ast.go
	// コメントを取得
	comment := tdecl.Doc.Text()
	// コメントに特定の文字列が入っていない場合は解析対象外とする
	if strings.Index(comment, CommentPrefix) == -1 {
		return nil
	}
	// create ConstDeclGroup
	g := &ConstDeclGroup{
		ConstDeclList: []*ConstDecl{},
	}
	// コメントから特定の文字列を取得して生成時のパラメータとして利用する
	// ここでは enum:ENUM_NAME という文字列があったらグループ化した const 定義を列挙体として出力するためのフラグを立てている
	r := regexp.MustCompile(`\senum:(\S+)\s`)
	matched := r.FindAllStringSubmatch(comment, -1)
	if len(matched) > 0 {
		g.IsEnum = true
		g.Name = matched[0][1]
	}
```

# コードを生成する

## コードの文字列を生成する

* 解析したデータをまとめて text/template を使ってクライアントサイドのコードとして生成する

```go:template.go
// 解析結果をまとめたデータ
type TemplateData struct {
	// 生成するコードのクラス名
	ClassName              string
	// 定数定義のリスト
	ConstDeclGroupList     []*ConstDeclGroup
	// 列挙体定義のリスト
	EnumConstDeclGroupList []*ConstDeclGroup
	// クラス定義のリスト
	StructDeclList         []*StructDecl
}

func renderTemplate(data TemplateData, tmpltext string, typemap map[string]string) string {
	tmpl := template.Must(template.New("").Funcs(template.FuncMap{
		// サーバーとクライアントで型名を変換する関数
		"typeconv": func(t string) string {
			v, ok := typemap[t]
			if ok {
				return v
			} else {
				return t
			}
		},
	}).Parse(tmpltext))
	var buf bytes.Buffer
	e := tmpl.Execute(&buf, data)
	if e != nil {
		log.Fatal(e)
	}
	return buf.String()
}
```

* クライアントサイドのコードのデフォルトのテンプレートは埋め込んで定義してしまったが上書きできるようにしてみた

```go:config.go
type Config struct {
	Template string
	Typemap  map[string]string
}

// クライアントサイドのデフォルトのテンプレート
// Unity(C#)向け
var defaultTemplate = `public static class {{ .ClassName }}
{
{{ range .ConstDeclGroupList -}}
{{ range .ConstDeclList -}}
{{ "    " -}} public const {{ typeconv .Type }} {{ .Name }} = {{ .Value }};
{{ end -}}
{{ end -}}

{{ range .EnumConstDeclGroupList -}}
{{ "    public enum " -}} {{ .Name }}
{{ "    " -}} {
{{ range .ConstDeclList -}}
{{ "        " -}} {{ .Name }} = {{ .Value }},
{{ end -}}
{{ "    " -}} }
{{ end -}}

{{ range .StructDeclList -}}
{{ "    public class " -}} {{ .Name }}
{{ "    " -}} {
{{ range .Fields -}}
{{ "        " -}} public {{ typeconv .Type }} {{- if .IsArray -}} [] {{- end }} {{ .Name }};
{{ end -}}
{{ "    " -}} }
{{ end -}}
}
`

// サーバーとクライアント間のデフォルトの型名変換のマップ
// Unity(C#)向け
var defaultTypemap = map[string]string{
	"int":     "int",
	"int32":   "int",
	"int64":   "long",
	"uint":    "uint",
	"uint32":  "uint",
	"uint64":  "ulong",
	"float32": "float",
	"float64": "double",
	"string":  "string",
	"bool":    "bool",
}

// 設定ファイルを読み込んでテンプレートや型変換マップが定義されていたら上書きする
func loadConfig(filename string) Config {
	config := Config{}
	if filename != "" {
		data, err := os.Open(filename)
		if err != nil {
			panic(err)
		}
		buf, err := ioutil.ReadAll(data)
		if err != nil {
			panic(err)
		}
		if err := toml.Unmarshal(buf, &config); err != nil {
			panic(err)
		}
	}
	if config.Template == "" {
		config.Template = defaultTemplate
	}
	if len(config.Typemap) == 0 {
		config.Typemap = defaultTypemap
	}
	return config
}
```

## ファイルを出力する

* 普通に bufio で

```go:main.go
	// write output file
	fp := newFile(*outputFlag)
	defer fp.Close()
	out := renderTemplate(data, config.Template, config.Typemap)
	w := bufio.NewWriter(fp)
	if _, e := w.WriteString(out); e != nil {
		log.Fatal(e)
	}
	w.Flush()
```