Go言語で名前を指定して構造体のインスタンスを取得する

前回のエントリーはこのエントリーへの伏線であります。
実はタイトルにもあるようにGo言語でも「名前を指定して構造体のインスタンスを取得する」ということをしたかったのです。
(Go言語でリフレクションを使いまくるのはどうこう・・・という話はおいておいてください)
C#でいうところの、

object obj = Activator.CreateInstance(Type.GetType("クラス名"));

Javaでいうところの

Class<?> clazz = Class.forName("クラス名");

のようなことをGo言語でもやってみようかと。
Go言語でもリフレクションはあるので、簡単にできるだろうと思っていたのですが、そんなに簡単ではありませんでした。


先人たちの知恵を探してみたところ、How I can get instance of struct by it name : golang-nutsに分かりやすい回答がありました。

The Go compiler does not retain the names of types in the compiled code, so there is no way for reflection to get at that information.

ということで、コンパイル時に未使用の型や、型の名前は削除されてしまうようです。


では、解決方法はないのかというとそうでもなく、上記リンク先にもあるようにmapなどで型名と型情報を保持しておけばよいと。

var structCache = make(map[string]reflect.Type)

init関数や、main関数でmapに名前と型情報を登録してあげればよさそうです。
使用時はこのような形でしょうか。

t := structCache["構造体の名前"]
v := reflect.New(t).Interface()

ただ取得したインスタンスはinterface{}型なので、使い勝手がいいとはいえませんが。


確かにこのような形で「名前を指定して構造体のインスタンスを取得する」ことはできますが、mapへいちいち手動で登録するの面倒じゃないですか、というか登録、変更あるいは削除のし忘れとか絶対に起きそうです。


それなら、あるルールのもと、ソースコードに定義されている構造体を自動的に調べてmapに登録するプログラムを書けばいいじゃないかと思いました。


早速Go言語で書かれたソースコードの解析です。ここで前回のエントリーに繋がります。
今回は指定したディレクトリ以下のファイルを解析するので、parser.ParseDir関数を使用します。

fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, path, nil, 0)

パッケージごとにast.Fileが取得できるので、ファイルごとに構造体を取得します。

func parseFile(f *ast.File) []string {
    ss := make([]string, 0)
    for _, decl := range f.Decls {
        gd, ok := decl.(*ast.GenDecl)
        if !ok {
            continue
        }

        for _, spec := range gd.Specs {
            ts, ok1 := spec.(*ast.TypeSpec)
            if !ok1 {
                continue
            }

            _, ok2 := ts.Type.(*ast.StructType)
            if !ok2 {
                continue
            }

            ss = append(ss, ts.Name.Name)
        }
    }

    return ss
}

最後にテンプレートを使用してマップに登録するソースコードを出力すれば終わりです。
以下の様なディレクトリ構成で、

sample/
    action/
        subaction/
            my1action.go
        my1action.go
        my2action.go
    main.go

それぞれのファイルには以下の構造体が含まれているとします。

action/my1action.go
    type My1Action struct{}
    type My2Action struct{}

action/my2action.go
    type My11Action struct{}
    type My21Action struct{}

action/subaction/my1action.go
    type My1Action struct{}
    type My2Action struct{}

以下のようにプロジェクトのルートディレクトリを指定して実行すると

$ go run generator.go -pd=./sample

sample/mystructs/structs.goというファイルが出力されます。中身は以下のようになっており、

package mystructs

import (
    //"fmt"
    "reflect"
)

import (
    pkg1 "github.com/taknb2nch/go-structbyname/sample/action"
    pkg2 "github.com/taknb2nch/go-structbyname/sample/action/subaction"
)

var structs = make(map[string]reflect.Type)

func init() {
    // action
    register(pkg1.My1Action{})
    register(pkg1.My2Action{})
    register(pkg1.My11Action{})
    register(pkg1.My21Action{})
    // action/subaction
    register(pkg2.My1Action{})
    register(pkg2.My2Action{})
}

func register(x interface{}) {
    t := reflect.TypeOf(x)
    n := t.PkgPath() + "." + t.Name()
    //fmt.Printf("Registered > %v\n", n)
    structs[n] = t
}

func New(name string) (interface{}, bool) {
    t, ok := structs[name]
    if !ok {
        return nil, false
    }
    v := reflect.New(t)
    return v.Interface(), true
}

mainプログラムで以下のようにこのファイルをインポートすれば利用できます。

package main

import (
    "errors"
    "fmt"
    "reflect"

    "./mystructs"
)

func main() {
    execute("github.com/taknb2nch/go-structbyname/sample/action.My1Action", "Index")
    execute("github.com/taknb2nch/go-structbyname/sample/action/subaction.My1Action", "Index")
}

func execute(structName, methodName string) error {
    a1, ok := mystructs.New(structName)
    if !ok {
        return errors.New(structName + " is not found.")
    }
    ...


ソースコード一式はこちらに置いています。

まとめ

最初にも書きましたがGo言語でリフレクションを使いまくるのはどうなの?という議論はあるとは思いますが、このようなこともできるということを試してみたかったということにしておいてください。議論するつもりもありませんのでご了承ください。
ちなみに以前のGolangCafeでDockerのソースコードを読みましたが、Dockerでは関数を名前で登録して使用しているようです。