Goでひたすら運用を楽にするためのコード生成をする

kaneshin
kaneshin
Dec 25, 2020 · 11 min read

この記事は Go 2 Advent Calendar 2020 の25日目の記事です。

こんにちは、こんばんは。久しぶりにGoの記事を書いている kaneshin です。今年はフロントエンドをコーディングする機会が多かったためGoに触れる機会があまりありませんでしたが、人間がやるべきでない運用タスクを見掛けるとすぐにGoを用いてコード生成をしたくなります。

今までの経験では筋の悪いコード生成もふんだんに負債として残してきましたが、無かったら困ることも多かっただろうとポジティブに考えることにしています。

さて、今回はそのコード生成について難しく考えないでとりあえず書いてみようと思えるレベルで紹介しようと思います。

TL;DR

  • 記事中盤にGo公式のコード生成を一例にした画像があるので参照してください
  • 記事後半に簡易的なサンプルコードを作成したリンクを貼っているので参照してください

この記事のモチベーション

Goでコード生成を行うのはほとんど text/template パッケージを使ってファイルを書き込むだけだという当たり前のことをただ当たり前にやるだけです。しかし、あまりメリットもないのかほとんど記事を見かけない気がするので、もっと身近に感じてもらえるようGoでのコード生成を噛み砕いて紹介したいと思って記事を書いています。

上のページは「GoでのGoファイルコード生成」における処理について書かれていますが、これだけだとピンとこないと思いますし、Goファイルコード生成のみじゃなくてなんでも生成していいのです。

go generateの公式コードを読んでみる

ひとまず、Go公式だとどのようにGoのコード生成をしているのか、実際のソースコードの参考例として sort パッケージを見てみましょう。

src/sort/zfuncversion.go が生成される流れ

(1) src/sort/sort.go
コード生成をするときの go generate コマンドで検出されるディレクティブである//go:generate go run genzfunc.go が記述されています。そのため、このファイルが zfuncversion.go を生成する起点となっており、この例では go run genzfunc.go を実行しています。

(2) src/sort/genzfunc.go
src/sort/zfuncversion.go を生成する実行ファイルです。このGoファイルは基本的なコード生成の例としてはわかりやすいと思います。また、この実行ファイルで何をするのかはコメントに記載があります。

// It copies sort.go to zfuncversion.go, only retaining funcs which
// take a “data Interface” parameter, and renaming each to have a
// “_func” suffix and taking a “data lessSwap” instead. It then rewrites
// each internal function call to the appropriate _func variants.

抄訳:sort.goを基にして、 data Interface を引数に持つ関数(レシーバーを持たないかつ、外部パッケージ参照不可)の引数の data Interfacedata lessSwap に変更して、関数名に _func を付け加える。
※一部、コード読まないとわからないことも記載しています

(3) src/sort/zfuncversion.go
コード生成によって生成されたファイルです。何をして生成されたかは上の(2)に抄訳として記載した通りです。

処理のメインとなるのは (2) のコード生成をしているところなので、genzfunc.go では何をしているのかを似非フローチャートでお送りします。(Miroの図形テンプレートが少ないので形はあまり気にしないでください)

//go:generate go run genzfunc.go

自分の頭の中では3つのフェーズを持って、処理を切り分けして考えています。

  1. Definition phase: コード生成が実行されるまでに必要な処理
  2. Generate phase: コード生成をするにあたり必要なデータを処理
  3. Output phase: コードを書き出すにあたり必要な整形処理

1と3のフェーズは基本的にどれも似たような記述になり、2のフェーズがメタ的にコードを生成させるロジック処理です。

2のフェーズで、genzfunc.go ではAST処理をしてファイルの操作を行っていますが、自分でコード生成をするときはとあるメタファイル(JSONやYaml)からコード生成をすることが非常に多いです。

Goを活用し、コード生成をして運用レスへ

もちろん、GoからGoを生成するのにASTを使ったりなど便利な面が多いですが、Goの text/template パッケージは簡易的にテンプレートエンジンとして使用し、ファイルの書き出しが行い易いのでどのようなファイルを生成してもいいのです。

実際にコード生成を使う場面によりますが、事業を運営している企業ならば何かしらの運用タスクを減らすために自動化をすることが多いと踏んでいるので、そのときはメタファイルを運用者に準備してもらい、そこからコード生成を行うということが経験上非常に多いです。

つまり、この記事で言いたかったことは運用を減らすためならなんでもコード生成するということです。言い過ぎかもしれませんが、別にGoファイルを生成するだけじゃなくて、メタ情報を持ったJSONファイルから改めてJSONファイルを書き出してもいいのです。

シンプルに物事を考えて問題解決をする

割と毎年書いていますが、Go言語の思想は別にサーバーアプリケーションに適した言語として設計されたわけではないです。Go at Google から一部抜粋するならば;

Moreover, the scale has changed: today’s server programs comprise tens of millions of lines of code, are worked on by hundreds or even thousands of programmers, and are updated literally every day. To make matters worse, build times, even on large compilation clusters, have stretched to many minutes, even hours.

Go was designed and developed to make working in this environment more productive. Besides its better-known aspects such as built-in concurrency and garbage collection, Go’s design considerations include rigorous dependency management, the adaptability of software architecture as systems grow, and robustness across the boundaries between components.

少し誇大解釈していなくもないですが、シンプルに考えることができるのがGo言語の素晴らしいところなので、運用面でのコストを最小化していくツールとしても活躍させていけるのがGoの魅力です。

Goの思想については去年のアドベントカレンダーで書いたので、もし知りたい方がいらっしゃれば年末年始にでも読んでみてください。

簡単な書き方のサンプル

コード生成のサンプルプロジェクトを作っておきました。

  1. Definition phase: main.goを参照してください
  2. Generate phase: cli/gen-meta/main.goを参照してください
  3. Output phase: cli/gen-meta/main.goを参照してください

これによって出力されるファイルは meta_gen.go ファイルです。Generate phaseで入力するメタ情報は package.json を用意したので、そちらをメタ情報として利用しています。

生成ファイルと判断できるコメントを組み込むこと

人が目視判断ができることと、マシンが正規表現で引っかかるようにコメントを入れるのがお決まりになっています。

// Code generated by gen-meta/main.go; DO NOT EDIT.

正規表現としては ^// Code generated .* DO NOT EDIT\.$ で引っかかればよいので、ワイルドカードのところは各自カスタマイズしてください。

go1.16からのgo:embed連携もできる

text/templateでパースする文字列が長くなったりすると同じファイルに記述しておくと管理しにくい場合もあるかもしれません。

ioutil.ReadFile をするのもありですが、go1.16からは変数にファイルを展開することもできるようになるのでファイルを分けておいて embed させるのもありかもしれません。

import _ "embed"//go:embed meta.txt
var metaTxt string

//go:generate command arguments

ここはほとんどが //go:generate go run oneof.go のようになると思いますが、実行可能ファイルであれば基本的に何でも動くので

//go:generate echo foo

も実行することができます。小ネタです。

//go:generate gofmtは使わないでgo/formatをする

できることならディレクティブで gofmtを使用しないことをおすすめします。なぜかと言うと、依存関係の話で、呼び出し側(ディレクティブ定義側)が実行ファイルで何が生成されるのかについて関心を持たせる必要が生じてしまうからです。

例えば、実行ファイルが a.go と foo/b.go の二つのファイルを生成したときにこの両方を呼び出し側で書く必要が生じるのと、もしも生成ファイルがリネームされたとしたらこれも呼び出し側を変更する必要が生じます。

呼び出し側は純粋にコード生成を実行させるだけに専念させて、コード生成させる実行ファイルの方でコードの整形は行いうことで依存関係を少なくすることができます。

gofmt をディレクティブとして記述するのが多いですが、これは go/format.Source を実行ファイルで処理しましょう。

おわりに

コード生成は簡単なんだと少しでも思えるようになってもらえたら嬉しいです。

なんかよくわからんと思って触らないよりは、一旦何か生成してみるのをおすすめします。運用タスクを無くしていくにはかなり効果が出ると経験上感じています。

ただ、コード生成は課題を構造化するスキルが十分に必要でもあるので、データ構造への理解や公式のコードを読むなどして熟練度を上げていくのをおすすめします。

それでは、みなさんメリークリスマス!🎄

Eureka Engineering

Learn about Eureka’s engineering efforts, product…

Eureka Engineering

Learn about Eureka’s engineering efforts, product developments and more.

kaneshin

Written by

kaneshin

Hi, I’m kaneshin. I’m currently working as a software engineer based in Tokyo, Japan.

Eureka Engineering

Learn about Eureka’s engineering efforts, product developments and more.