LoginSignup
4

More than 5 years have passed since last update.

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

Posted at

はじめに

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

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

  • 対象のディレクトリ以下のすべてのファイルに対して処理を行いたい場合は "path/filepath" の Walk が便利
  • filepath.Walk(string, func(string, os.FileInfo, error) error) という関数で、第一引数にディレクトリ名、第二引数に各ファイルに対して実行する関数を指定する
  • go-langconv での実際の呼び出し箇所は以下のような感じ
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言語コード生成にまあまあ書いた
  • 型定義や関数定義のすぐ上の行に書いたコメントはその定義と関連付けられるので、ここに任意の文字列を記載してソース解析の対象にしたり生成時のパラメータにしたりできる

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

  • parser.ParseFile で第四引数に parser.ParseComments を指定する
main.go
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err.Error())
    }

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

  • 定義に関連するコメントは構文木データの定義のデータ内 Doc フィールドに入ってくる
  • 例えばコメント付きの定数定義とその構文木データは以下のようになる
// +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  .  .  }

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

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 を使ってクライアントサイドのコードとして生成する
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()
}
  • クライアントサイドのコードのデフォルトのテンプレートは埋め込んで定義してしまったが上書きできるようにしてみた
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 で
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()

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4