Goブログ

コード生成

Rob Pike
2014年12月22日

普遍的な計算の特性であるチューリング完全性とは、コンピュータープログラムがコンピュータープログラムを作成できることを意味します。これは強力なアイデアであり、頻繁に発生するにもかかわらず、十分に認識されていません。これは、例えばコンパイラの定義の大きな部分を占めています。また、go testコマンドの動作方法でもあります。テスト対象のパッケージをスキャンし、パッケージに合わせてカスタマイズされたテストハーネスを含むGoプログラムを出力し、コンパイルして実行します。最新のコンピューターは非常に高速であるため、このコストのかかりそうなシーケンスは数分の一秒で完了します。

プログラムを作成するプログラムには他にも多くの例があります。Yaccなどでは、文法の説明を読み込み、その文法を解析するプログラムを出力します。プロトコルバッファの「コンパイラ」はインターフェース記述を読み込み、構造定義、メソッド、その他のサポートコードを出力します。あらゆる種類の構成ツールも同様に機能し、メタデータまたは環境を調べ、ローカルの状態に合わせてカスタマイズされた足場を出力します。

プログラムを作成するプログラムは、ソフトウェアエンジニアリングにおいて重要な要素ですが、ソースコードを生成するYaccのようなプログラムは、その出力がコンパイルできるようにビルドプロセスに統合する必要があります。Makeなどの外部ビルドツールを使用している場合は、通常簡単に実行できます。しかし、GoツールがGoソースから必要なビルド情報をすべて取得するGoでは、問題が発生します。GoツールだけでYaccを実行するメカニズムは単に存在しません。

少なくとも、今までまでは。

最新のGoリリースであるGo 1.4には、このようなツールの実行を容易にする新しいコマンドが含まれています。これはgo generateと呼ばれ、実行する一般的なコマンドを識別するGoソースコード内の特別なコメントをスキャンすることによって機能します。go generatego buildの一部ではないことを理解することが重要です。依存関係分析は含まれておらず、go buildを実行する前に明示的に実行する必要があります。Goパッケージの作成者によって使用されることを意図しており、クライアントによって使用されることを意図していません。

go generateコマンドは使いやすいです。ウォーミングアップとして、Yacc文法を生成する方法を次に示します。

まず、GoのYaccツールをインストールします

go get golang.org/x/tools/cmd/goyacc

新しい言語の文法を定義するgopher.yというYacc入力ファイルがあるとします。Goソースファイルを作成するには、通常、次のようにコマンドを呼び出します

goyacc -o gopher.go -p parser gopher.y

-oオプションは出力ファイルの名前を指定し、-pはパッケージ名を指定します。

go generateにプロセスを実行させるには、同じディレクトリ内の通常の(生成されていない).goファイルのいずれかに、次のコメントをファイル内の任意の場所に追加します

//go:generate goyacc -o gopher.go -p parser gopher.y

このテキストは、上記のgo generateによって認識される特別なコメントを前に付けたコマンドです。コメントは行の先頭から始まり、//go:generateの間にスペースがあってはなりません。そのマーカーの後、行の残りの部分がgo generateが実行するコマンドを指定します。

実行します。ソースディレクトリに変更し、go generate、次にgo buildなどを実行します

$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test

以上です。エラーがないと仮定すると、go generateコマンドはyaccを呼び出してgopher.goを作成します。その時点で、ディレクトリにはGoソースファイルの完全なセットが含まれるため、通常どおりビルド、テスト、作業を行うことができます。gopher.yが変更されるたびに、go generateを再実行してパーサーを再生成するだけです。

オプション、環境変数など、go generateの動作の詳細については、設計ドキュメントを参照してください。

Go generateは、Makeやその他のビルドメカニズムでは実行できないことは何もありませんが、goツールに付属しています(追加のインストールは不要)Goエコシステムにうまく適合します。呼び出すプログラムがターゲットマシンで使用できないという理由だけで、パッケージの作者用であり、クライアント用ではないことに注意してください。また、含まれるパッケージがgo getによってインポートされることを意図している場合、ファイルが生成(およびテスト)されたら、クライアントが使用できるようにソースコードリポジトリにチェックインする必要があります。

これで使用できるようになったので、新しいものに使用してみましょう。go generateが役立つ方法のまったく異なる例として、golang.org/x/toolsリポジトリで使用できる新しいプログラムstringerがあります。これは、整数の定数のセットに対して文字列メソッドを自動的に記述します。リリースされたディストリビューションの一部ではありませんが、簡単にインストールできます

$ go get golang.org/x/tools/cmd/stringer

stringerのドキュメントの例を次に示します。さまざまな種類の薬を定義する整数の定数のセットを含むコードがあるとします

package painkiller

type Pill int

const (
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
    Acetaminophen = Paracetamol
)

デバッグのために、これらの定数を自分自身で美しく印刷したいと考えています。つまり、次のようなシグネチャのメソッドが必要になります。

func (p Pill) String() string

手動で記述するのは簡単です。おそらく次のようになります

func (p Pill) String() string {
    switch p {
    case Placebo:
        return "Placebo"
    case Aspirin:
        return "Aspirin"
    case Ibuprofen:
        return "Ibuprofen"
    case Paracetamol: // == Acetaminophen
        return "Paracetamol"
    }
    return fmt.Sprintf("Pill(%d)", p)
}

もちろん、この関数を記述する方法は他にもあります。Pillをインデックスとする文字列のスライス、またはマップ、またはその他のテクニックを使用できます。何をするにしても、薬のセットを変更した場合に維持する必要があり、それが正しいことを確認する必要があります。(パラセタモールの2つの名前により、これはそれ以外の場合よりも難しくなります。)さらに、どのアプローチを採用するかは、型と値によって異なります。符号付きまたは符号なし、密集型またはスパース型、0ベースまたは非0ベースなどです。

stringerプログラムは、これらの詳細をすべて処理します。独立して実行できますが、go generateによって駆動されることを意図しています。使用するには、ソースに生成コメントを追加します。おそらく型定義の近くにあります

//go:generate stringer -type=Pill

このルールは、go generatestringerツールを実行して、型PillStringメソッドを生成する必要があることを指定します。出力は自動的にpill_string.go-outputフラグでオーバーライドできるデフォルト)に書き込まれます。

実行してみましょう

$ go generate
$ cat pill_string.go
// Code generated by stringer -type Pill pill.go; DO NOT EDIT.

package painkiller

import "fmt"

const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"

var _Pill_index = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
    if i < 0 || i+1 >= Pill(len(_Pill_index)) {
        return fmt.Sprintf("Pill(%d)", i)
    }
    return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
$

Pillの定義または定数を変更するたびに、実行する必要があるのは

$ go generate

Stringメソッドを更新することだけです。そしてもちろん、同じパッケージに複数の型をこのように設定している場合、その単一のコマンドは、単一のコマンドですべてのStringメソッドを更新します。

生成されたメソッドが醜いことは間違いありません。ただし、人間が作業する必要がないため、問題ありません。機械生成コードは多くの場合、醜いです。効率的に機能するために努力しています。すべての名前は1つの文字列にまとめられており、メモリを節約します(無数の名前があっても、すべての名前に対して1つの文字列ヘッダーのみ)。次に、配列_Pill_indexは、シンプルで効率的なテクニックによって値から名前へのマッピングを行います。_Pill_indexuint8(値の空間をカバーするのに十分な最小の整数)の配列(スライスではありません。ヘッダーがもう1つ削除されます)であることにも注意してください。値がもっと多かったり、負の値があったりすると、_Pill_indexの生成された型はuint16またはint8に変更される可能性があります。最適なものが使用されます。

stringerによって印刷されるメソッドで使用されるアプローチは、定数のセットのプロパティによって異なります。たとえば、定数がスパースな場合、マップを使用する場合があります。2の累乗を表す定数のセットに基づく些細な例を次に示します

const _Power_name = "p0p1p2p3p4p5..."

var _Power_map = map[Power]string{
    1:    _Power_name[0:2],
    2:    _Power_name[2:4],
    4:    _Power_name[4:6],
    8:    _Power_name[6:8],
    16:   _Power_name[8:10],
    32:   _Power_name[10:12],
    ...,
}

func (i Power) String() string {
    if str, ok := _Power_map[i]; ok {
        return str
    }
    return fmt.Sprintf("Power(%d)", i)
}

要するに、メソッドを自動的に生成することで、人間が行うよりも良い仕事ができるようになります。

Goツリーに既にインストールされているgo generateの他の用途はたくさんあります。例としては、unicodeパッケージのUnicodeテーブルの生成、encoding/gobでの配列のエンコードとデコードのための効率的なメソッドの作成、timeパッケージでのタイムゾーンデータの生成などがあります。

創造的にgo generateを使用してください。実験を促進するためにあります。

そして、使用しない場合でも、整数の定数のStringメソッドを記述するために新しいstringerツールを使用してください。機械に作業させましょう。

次の記事:Gopher Galaは世界初のGoハッカソンです
前の記事:Go 1.4がリリースされました
ブログインデックス