Go言語でメタプログラミング
YouTubeのプレミアム会員に入ってしまい、動画を見る時間がますます増えてしまいました。エンジニアの内山です。
最近の業務では、Go言語を書いています。言語の機能については物足りなさを感じることもありますが、それよりもシンプルさが結構気に入っています。
今回は、Go言語のメタプログラミングについて、ご紹介します。
メタプログラミングとは
開発業務では、似たような単純なプログラムを何度も書く必要がある場合があります。ボイラープレートと呼ばれるようなプログラムです。
そのようなプログラムを自動生成するプログラムを書いて、効率化することはよくあると思います。
この 「プログラムを生成するプログラムを書くこと」 を メタプログラミングと呼びます。
筆者が参加しているプロジェクトでは、実際に以下のようなことを行っています。
- AWS LambdaのHandlerとなるプログラムと呼び出し関数を自動生成
- Goプログラムを解析して、AWS SAMの設定ファイルを自動生成(プログラムへの参照情報を自動設定)
何度も単純なプログラムを書いたり、設定ファイルの記述ミスを減らしたりしています。
メタプログラミングは、開発業務を効率化する上で強力な手法になります。
2種類のメタプログラミング
Go言語でメタプログラミングを行う場合、以下の2種類の方法があります。
- reflectパッケージによるメタプログラミング(実行時)
- go/*パッケージによるメタプログラミング(ソースコードの構文解析)
今回は、後者のgo/*パッケージによるメタプログラミング
について、取り上げます。
go/* パッケージによるメタプログラミング
go/*パッケージには、「Goのプログラムを構文解析し、操作を行うAPI」が提供されています。
以下のように、go/parserのParseExpr関数
にGoプログラムを文字列として渡すと、構文解析が行われます。
package main
import (
"fmt"
"go/parser"
)
func main() {
expr, _ := parser.ParseExpr("a * -1")
fmt.Printf("%#v", expr)
}
以下が実行結果です。
*ast.BinaryExpr {
. X: *ast.Ident {
. . NamePos: 1
. . Name: "a"
. . Obj: *ast.Object {
. . . Kind: bad
. . . Name: ""
. . }
. }
. OpPos: 3
. Op: *
. Y: *ast.UnaryExpr {
. . OpPos: 5
. . Op: -
. . X: *ast.BasicLit {
. . . ValuePos: 6
. . . Kind: INT
. . . Value: "1"
. . }
. }
}
「ast.BinaryExpr」「ast.Indent」「Op」「ast.UnaryExpr」「ast.BasicLit」など、
構造体やメンバ変数によって、Goの構文を表しているのが感じ取れると思います。
一番最初に現れるast.BinaryExpr構造体
は、2項演算子を表しています。
今回は実例として__「あるパッケージに含まれているすべての関数のリストを取得する」__ことを考えてみます。
解析対象のプログラム
サンプルとして、以下のプログラムを解析対象とします。
// sample/fuga.go
package sample
import "fmt"
type FugaArg struct {
Message string
}
func Fuga(arg FugaArg) error {
fmt.Println(arg.Message)
return nil
}
// sample/hoge.go
package sample
type HogeArg struct {
Message string
}
func Hoge(arg *HogeArg) (string, error) {
return arg.Message, nil
}
実際には、Fuga関数 と Hoge関数 は別々のファイルで記述されています。
このサンプルプログラムを構文解析した結果を単純に出力してみます。
パッケージ内のプログラムを構文解析するので、parser.ParseDir
関数を使用します。
func main() {
packageName := "sample"
OutputFuncFromPackage(packageName)
}
func OutputFuncFromPackage(packageName string) error {
fset := token.NewFileSet()
packages, err := parser.ParseDir(fset, packageName, nil, parser.Mode(0))
if err != nil {
return err
}
for _, pkg := range packages {
ast.Inspect(pkg, func(n ast.Node) bool {
if t, ok := n.(*ast.FuncDecl); ok {
ast.Print(fset, t)
fmt.Println()
}
return true
})
}
return nil
}
以下が出力結果となります。
*ast.FuncDecl {
. Name: *ast.Ident {
. . NamePos: sample/hoge.go:7:6
. . Name: "Hoge"
. . Obj: *ast.Object {
. . . Kind: func
. . . Name: "Hoge"
. . . Decl: *(obj @ 0)
. . }
. }
. Type: *ast.FuncType {
. . Func: sample/hoge.go:7:1
. . Params: *ast.FieldList {
. . . Opening: sample/hoge.go:7:10
. . . List: []*ast.Field (len = 1) {
. . . . 0: *ast.Field {
. . . . . Names: []*ast.Ident (len = 1) {
. . . . . . 0: *ast.Ident {
. . . . . . . NamePos: sample/hoge.go:7:11
. . . . . . . Name: "arg"
. . . . . . . Obj: *ast.Object {
. . . . . . . . Kind: var
. . . . . . . . Name: "arg"
. . . . . . . . Decl: *(obj @ 15)
. . . . . . . }
. . . . . . }
. . . . . }
. . . . . Type: *ast.StarExpr {
. . . . . . Star: sample/hoge.go:7:15
. . . . . . X: *ast.Ident {
. . . . . . . NamePos: sample/hoge.go:7:16
. . . . . . . Name: "HogeArg"
. . . . . . . Obj: *ast.Object {
. . . . . . . . Kind: type
. . . . . . . . Name: "HogeArg"
. . . . . . . . Decl: *ast.TypeSpec {
. . . . . . . . . Name: *ast.Ident {
. . . . . . . . . . NamePos: sample/hoge.go:3:6
. . . . . . . . . . Name: "HogeArg"
. . . . . . . . . . Obj: *(obj @ 32)
. . . . . . . . . }
. . . . . . . . . Assign: -
. . . . . . . . . Type: *ast.StructType {
. . . . . . . . . . Struct: sample/hoge.go:3:14
. . . . . . . . . . Fields: *ast.FieldList {
. . . . . . . . . . . Opening: sample/hoge.go:3:21
. . . . . . . . . . . List: []*ast.Field (len = 1) {
. . . . . . . . . . . . 0: *ast.Field {
. . . . . . . . . . . . . Names: []*ast.Ident (len = 1) {
. . . . . . . . . . . . . . 0: *ast.Ident {
. . . . . . . . . . . . . . . NamePos: sample/hoge.go:4:2
. . . . . . . . . . . . . . . Name: "Message"
. . . . . . . . . . . . . . . Obj: *ast.Object {
. . . . . . . . . . . . . . . . Kind: var
. . . . . . . . . . . . . . . . Name: "Message"
. . . . . . . . . . . . . . . . Decl: *(obj @ 47)
. . . . . . . . . . . . . . . }
. . . . . . . . . . . . . . }
. . . . . . . . . . . . . }
. . . . . . . . . . . . . Type: *ast.Ident {
. . . . . . . . . . . . . . NamePos: sample/hoge.go:4:10
. . . . . . . . . . . . . . Name: "string"
. . . . . . . . . . . . . }
. . . . . . . . . . . . }
. . . . . . . . . . . }
. . . . . . . . . . . Closing: sample/hoge.go:5:1
. . . . . . . . . . }
. . . . . . . . . . Incomplete: false
. . . . . . . . . }
. . . . . . . . }
. . . . . . . }
. . . . . . }
. . . . . }
. . . . }
. . . }
. . . Closing: sample/hoge.go:7:23
. . }
. . Results: *ast.FieldList {
. . . Opening: sample/hoge.go:7:25
. . . List: []*ast.Field (len = 2) {
. . . . 0: *ast.Field {
. . . . . Type: *ast.Ident {
. . . . . . NamePos: sample/hoge.go:7:26
. . . . . . Name: "string"
. . . . . }
. . . . }
. . . . 1: *ast.Field {
. . . . . Type: *ast.Ident {
. . . . . . NamePos: sample/hoge.go:7:34
. . . . . . Name: "error"
. . . . . }
. . . . }
. . . }
. . . Closing: sample/hoge.go:7:39
. . }
. }
. Body: *ast.BlockStmt {
. . Lbrace: sample/hoge.go:7:41
. . List: []ast.Stmt (len = 1) {
. . . 0: *ast.ReturnStmt {
. . . . Return: sample/hoge.go:8:2
. . . . Results: []ast.Expr (len = 2) {
. . . . . 0: *ast.SelectorExpr {
. . . . . . X: *ast.Ident {
. . . . . . . NamePos: sample/hoge.go:8:9
. . . . . . . Name: "arg"
. . . . . . . Obj: *(obj @ 20)
. . . . . . }
. . . . . . Sel: *ast.Ident {
. . . . . . . NamePos: sample/hoge.go:8:13
. . . . . . . Name: "Message"
. . . . . . }
. . . . . }
. . . . . 1: *ast.Ident {
. . . . . . NamePos: sample/hoge.go:8:22
. . . . . . Name: "nil"
. . . . . }
. . . . }
. . . }
. . }
. . Rbrace: sample/hoge.go:9:1
. }
}
構文解析木を表現している構造体から、必要な情報だけを取る際に、どのような構造になっているのかを把握できるように、上記のように出力してみるのがおすすめです。
関数情報を取り出す処理
あとは、解析結果を出力したものを見ながら、欲しい情報を取得する処理を書いていくのみです。
以下は、関数の一覧を取得する処理を行うプログラムです。
func main() {
packageName := "sample"
funcs, err := GetFuncFromPackage(packageName)
if err != nil {
panic(err)
}
pp.Println(funcs)
}
type GoFunc struct {
FuncName string
Args []*GoFuncArg
Returns []string
}
type GoFuncArg struct {
Name []string
Type string
}
func GetFuncArgTypeStr(t ast.Expr) string {
typeStr := ""
var symbols []string
ast.Inspect(t, func(n ast.Node) bool {
switch n.(type) {
case *ast.SelectorExpr:
sel := n.(*ast.SelectorExpr)
typeStr = fmt.Sprintf("%s.%s", sel.X, sel.Sel.Name)
case *ast.Ident:
if typeStr == "" {
typeStr = n.(*ast.Ident).Name
}
case *ast.InterfaceType:
typeStr = "interface{}"
case *ast.ArrayType:
symbols = append(symbols, "[]")
case *ast.MapType:
m := n.(*ast.MapType)
typeStr = fmt.Sprintf("map[%s]%s", m.Key, m.Value)
case *ast.StarExpr:
symbols = append(symbols, "*")
}
return true
})
return fmt.Sprintf("%s%s", strings.Join(symbols, ""), typeStr)
}
func GetFuncFromPackage(packageName string) ([]*GoFunc, error) {
fset := token.NewFileSet()
packages, err := parser.ParseDir(fset, packageName, nil, parser.Mode(0))
if err != nil {
return nil, err
}
var funcs []*GoFunc
for _, pkg := range packages {
ast.Inspect(pkg, func(n ast.Node) bool {
if t, ok := n.(*ast.FuncDecl); ok {
// メソッド は除外
if t.Recv != nil {
return true
}
// テスト は除外
if strings.HasPrefix(t.Name.Name, "Test") {
return true
}
// 関数の引数情報
var argDecls = make([]*GoFuncArg, len(t.Type.Params.List))
for i, l := range t.Type.Params.List {
// (a, b string, c int) のような引数だった場合、
// [a, b] と [c] のような配列で名前が返ってくる
var names = make([]string, len(l.Names))
for i := range l.Names {
names[i] = l.Names[i].Name
}
// 引数の型情報を取得
typeStr := GetFuncArgTypeStr(l.Type)
// testing.Tなどのテスト関数で定義される引数だった場合は除外する
if strings.HasSuffix(typeStr, ".T") {
return true
}
// 引数情報を簡単に参照できるようにした構造体に保存しておく
argDecls[i] = &GoFuncArg{
Name: names,
Type: typeStr,
}
}
// 関数の返り値情報
retStr := ""
var retDecls []string
if t.Type.Results != nil {
// (int, float, string) のような返り値の型情報リストを取得する
retDecls = make([]string, len(t.Type.Results.List))
for i, l := range t.Type.Results.List {
// 引数の型情報を取得
retDecls[i] = GetFuncArgTypeStr(l.Type)
}
if len(retDecls) != 0 {
retStr = strings.Join(retDecls, ", ")
// 返り値が2つ以上の場合は、 (int, float) のようにカッコを付ける
if len(retDecls) > 1 {
retStr = fmt.Sprintf("(%s)", retStr)
}
}
}
// 関数情報を簡単に参照できるようにした構造体に保存しておく
funcs = append(funcs, &GoFunc{
FuncName: t.Name.Name, // 関数名
Args: argDecls, // 関数の引数情報
Returns: retDecls, // 関数の返り値情報
})
}
return true
})
}
return funcs, nil
}
以下は、出力結果となります。
[]*main.GoFunc{
&main.GoFunc{
FuncName: "Hoge",
Args: []*main.GoFuncArg{
&main.GoFuncArg{
Name: []string{
"arg",
},
Type: "*HogeArg",
},
},
Returns: []string{
"string",
"error",
},
},
&main.GoFunc{
FuncName: "Fuga",
Args: []*main.GoFuncArg{
&main.GoFuncArg{
Name: []string{
"arg",
},
Type: "FugaArg",
},
},
Returns: []string{
"error",
},
},
}
生の解析結果より、関数の情報を取り出しやすい形になっていると思います。
この情報を基に、プログラムを生成する処理を書いていくことができます。
まとめ
Go言語でメタプログラミングをする手法をご紹介しました。
メタプログラミングは「黒魔術」と呼ばれることもあり、取り扱いには注意が必要です。
しかし、うまく行けば開発業務の効率化に繋がります。
ぜひとも挑戦してみてください。
参考リンク
GoのためのGo
https://motemen.github.io/go-for-go-book/