metagoを作った話

Source: metagoを作った話

Files changes: 1

  • go/metago/README.md (+113, -0)

vvakame vvakame -2019-08-23 16:44:55

リポジトリ

metagoはGo言語向けのメタプログラミングライブラリです。
考え方のベースとしてwireのシグニチャを定義し実装は機械的に生成する考え方と、VのReflection via codegenのホスト言語の構文でメタ構造を書く、というのを使っています。

本ライブラリはある程度動きますが、現時点ではトップレベルの定義を生成したり動的な名前のメソッドを生成したりすることはできません。
テストケースも圧倒的に不足しているため実用レベルに達しているかといわれると疑問があります。
でも面白いよ!

モチベーション

Goは言語自体の持つ型の表現力が弱く、ボイラープレートなコードをたくさん書くことになりがちです。
そこで、何らかのデータを元にGoのコードを自動生成しよう!というアイディアに至るのに時間はかかりません。

一番最初に思いつくであろう、GoのコードをASTで組み立てる戦略は破滅的にめんどくさいです。
これは、生成したいと思っているGoのコードとASTを組み立てるコードにまったくもって相似ではないからです。

次のアイディアとして、Goのコードをテキストとして組み立てるものがあります。
jwgでは Printf を使ってソースコードを生成しています。
gqlgenでは text/template を使ってソースコードを生成しています。
これは、ASTを使って組み立てるのに比べるとだいぶ仕上がりが想像しやすいです。
一方でIDEからの支援が得にくく、出力後のコードがvalidなコードかというのは出力してみるまでわかりません。

新しいアプローチとして、本ライブラリ metago を考え、実装しました。
metagoでは生成するべきGoのコードをGoのコードで書きます。
鋳型になるGoコードのASTをこねこねして、ほしいGoコードに変換するイメージです。
これならば、IDEの支援を今までに比べると圧倒的に楽に実装することができます。

metago について

metagoでは、実際のソースコード中にマーカーを仕込んでいき最終的なソースコードを生成します。

あるオブジェクトのフィールド名と値を出力するテンプレートは次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//+build metago

package main

import (
"fmt"

"github.com/vvakame/metago"
)

type Foo struct {
ID int64
Name string
}

func main() {
obj := &Foo{1, "vvakame"}
mv := metago.ValueOf(obj)
for _, mf := range mv.Fields() {
fmt.Println(mf.Name(), mf.Value())
}
}

これをmetagoで処理すると次のコードが得られます。
実際にプログラムとして動作させるのはこちらの生成されたコードです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Code generated by metago. DO NOT EDIT.

//+build !metago

package main

import (
"fmt"
)

type Foo struct {
ID int64
Name string
}

func main() {
obj := &Foo{1, "vvakame"}

{
fmt.Println("ID", obj.ID)
}
{
fmt.Println("Name", obj.Name)
}
}

reflectパッケージに少し似ています。
どういったことができるかというのはtestbedディレクトリを見てみてください。

主なfeatureとして…

  • mv := metago.ValueOf(obj) によって metago.Value な値を取得
  • for _, mf := range mv.Fields() によって各フィールドに対する処理を展開・記述
  • mf.Name() によってフィールド名の取得
  • mf.Value() によってフィールドの値の取得
  • mf.StructTagGet("json") などでstructのタグの取得
  • mf.Value().(time.Time) といった型アサートとif文の組み合わせによるフィールドの型毎の処理の振り分け
    • type switchもサポート
  • インラインテンプレート(第一引数が mv metago.Value の関数)の利用

などに対応しています。

metago のインストールと実行

1
2
$ go get -u github.com/vvakame/metago/cmd/metago
$ metago -v .