Goブログ
コード生成
普遍的な計算の特性であるチューリング完全性とは、コンピュータープログラムがコンピュータープログラムを作成できることを意味します。これは強力なアイデアであり、頻繁に発生するにもかかわらず、十分に認識されていません。これは、例えばコンパイラの定義の大きな部分を占めています。また、go
test
コマンドの動作方法でもあります。テスト対象のパッケージをスキャンし、パッケージに合わせてカスタマイズされたテストハーネスを含むGoプログラムを出力し、コンパイルして実行します。最新のコンピューターは非常に高速であるため、このコストのかかりそうなシーケンスは数分の一秒で完了します。
プログラムを作成するプログラムには他にも多くの例があります。Yaccなどでは、文法の説明を読み込み、その文法を解析するプログラムを出力します。プロトコルバッファの「コンパイラ」はインターフェース記述を読み込み、構造定義、メソッド、その他のサポートコードを出力します。あらゆる種類の構成ツールも同様に機能し、メタデータまたは環境を調べ、ローカルの状態に合わせてカスタマイズされた足場を出力します。
プログラムを作成するプログラムは、ソフトウェアエンジニアリングにおいて重要な要素ですが、ソースコードを生成するYaccのようなプログラムは、その出力がコンパイルできるようにビルドプロセスに統合する必要があります。Makeなどの外部ビルドツールを使用している場合は、通常簡単に実行できます。しかし、GoツールがGoソースから必要なビルド情報をすべて取得するGoでは、問題が発生します。GoツールだけでYaccを実行するメカニズムは単に存在しません。
少なくとも、今までまでは。
最新のGoリリースであるGo 1.4には、このようなツールの実行を容易にする新しいコマンドが含まれています。これはgo
generate
と呼ばれ、実行する一般的なコマンドを識別するGoソースコード内の特別なコメントをスキャンすることによって機能します。go
generate
はgo
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
generate
がstringer
ツールを実行して、型Pill
のString
メソッドを生成する必要があることを指定します。出力は自動的に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_index
はuint8
(値の空間をカバーするのに十分な最小の整数)の配列(スライスではありません。ヘッダーがもう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
ツールを使用してください。機械に作業させましょう。