徒然なるままに go build と build tag を見ていく
Go #1 Advent Calendar 2021 19日目

本記事は『Go #1 Advent Calendar 2021 19日目 』の記事です。

はじめに
Goには標準で便利なコマンドがたくさんあります。
有名どころで言えば、
generatefmtvet
などがあります。
中でも、Goのコードをコンパイルするためのbuildコマンドは、
みなさんも日頃の開発で使用しているのではないでしょうか。
buildは特に難しいことをしなければ、とてもシンプルに使えるコマンドです。
よって、直感的に「こうすればコンパイルできるんでしょー」くらいの感覚で、
ドキュメントを注視しなくても使い始められるでしょう。
しかし、buildは深く見ていくと意外と奥が深いコマンドです。
今回はそんなbuildについて深ぼっていこうと思います。
buildコマンドとは
まずはbuildとはどういったコマンドなのか見ていきましょう。
helpを使って調べてみます。
$ go help build
Build compiles the packages named by the import paths,
along with their dependencies, but it does not install the results.
<省略>
When compiling packages, build ignores files that end in '_test.go'.
<省略>
When compiling multiple packages or a single non-main package,
build compiles the packages but discards the resulting object,
serving only as a check that the packages can be built.
<省略>
	-tags tag,list
		a comma-separated list of build tags to consider satisfied during the
		build. For more information about build tags, see the description of
		build constraints in the documentation for the go/build package.
		(Earlier versions of Go used a space-separated list, and that form
		is deprecated but still recognized.)
<省略>
buildはコンパイルするやつですよという文章から始まり、
いろいろ書かれていますが、今回は以下の4点について見てみます。
buildはインストールを行わない- コンパイル時に
_test.goファイルは無視する - 複数パッケージor単一の非mainパッケージのコンパイル時は結果のオブジェクトを破棄する
 - build tagについて
 
buildはインストールを行わない
buildはコンパイルだけを行い、インストールは行いません。
「え、そりゃそうでしょ」「インストール?」ってなる人も多いのではないでしょうか。
当たり前と言われればそうですが、ヘルプにて丁寧に説明されています。
まず、インストールとはなにかを理解するために、buildとinsatllの挙動の違いを見てみましょう。
なぜ、installと比較するかと言うと、下記のとおりinstallはコンパイルとインストールを行うコマンドだからです。
$ go help install
usage: go install [build flags] [packages]
Install compiles and installs the packages named by the import paths.
まずはbuildしてみます。
(インストール物はGOBIN配下に配置されます)
$ go build main.go
$ ls -l $GOBIN/
total 33832
-rwxr-xr-x  1 yyh-gl  staff  13989584  8 17 02:05 go
-rwxr-xr-x  1 yyh-gl  staff   3326080  8 17 02:05 gofmt
main.goのコンパイル&インストールしたものがないですね。
次にinstallしてみます。
$ go install main.go
$ ls -l $GOBIN/
total 37488
-rwxr-xr-x  1 yyh-gl  staff  13989584  8 17 02:05 go
-rwxr-xr-x  1 yyh-gl  staff   3326080  8 17 02:05 gofmt
-rwxr-xr-x  1 yyh-gl  staff   1869456 12 17 22:30 main
main.goのコンパイル物であるmainがGOBIN配下に設置されています。
つまり、インストールが行われています。
buildとinstallには上記のような差があります。
両者の使い分けについては、以下の記事などを参考にすると良いのではないでしょうか。
僕は、自プロジェクトの開発物のコンパイルにはbuildを使い、ツールのインストールにはinstallを使うようにしています。
開発物のコンパイル結果はGOBIN配下より、そのプロジェクト配下に入れておいた方がなにかと便利だからです。
コンパイル物を動かす場所がローカルではない場合、そもそもGOBIN配下にある必要もないですしね。
逆にツール系は特に理由がないかぎりは、GOBIN配下にあった方がPATHが通っているので使い勝手がいいですよね。
コンパイル時に_test.goファイルは無視する
これは知っている人も多いと思います。
システムを動かす上でテストコードは不要ですし、無視されるのも納得ですね。
実際に挙動を見てみます。invalid_test.go(Go Playground
)というコンパイルエラーになるテストファイルを対象にbuildを実行してみます。
$ go build invalid_test.go
<正常終了するのでなにも表示されない>
このように正常終了します。
つまり、ビルド対象に含まれていないため、そもそもコンパイルされておらず、エラーが出ません。
同じファイルをinvalid_test2.goという名前に変更し、もう一度buildしてみます。
$ go build invalid_test2.go
# command-line-arguments
./invalid_test2.go:4:1: syntax error: unexpected EOF, expecting name or (
コンパイルエラーが出ましたね。
今度はビルド対象に含まれたようです。
_test.goという命名を基にビルド対象か否かを判定していることが分かります。
複数パッケージor単一の非mainパッケージのコンパイル時は結果のオブジェクトを破棄する
helpで言うと下記の記述に関しての内容です。
When compiling multiple packages or a single non-main package,
build compiles the packages but discards the resulting object,
serving only as a check that the packages can be built.
実際にmainパッケージではないnot_main.go(Go Playground
)に対してbuildを実行してみます。
$ go build not_main.go
$ ls -l
total 8
-rw-r--r--  1 yyh-gl  staff  65 12 17 23:14 not_main.go
このようにビルド自体は正常終了しますが、生成物がありません。
次に、not_main.goにエラーを仕込んでみます。
(Go Playground
)
$ go build not_main.go
# command-line-arguments
./not_main.go:4:2: undefined: fmt
$ ls -l
total 8
-rw-r--r--  1 yyh-gl  staff  51 12 17 23:16 not_main.go
エラーが出ました。
もちろん生成物はありません。
serving only as a check that the packages can be built.
ビルド可能かどうかのチェックだけを行う
ヘルプに記載のあるとおりですね。
複数パッケージに対してビルドをかけてみても、同様に生成物はありませんでした。
(Go Playground
)
$ go build ./...
$ ls -l
total 24
drwxr-xr-x  3 yyh-gl  staff    96 12 18 18:27 foo
-rw-r--r--  1 yyh-gl  staff   204 12 18 10:39 go.mod
-rw-r--r--  1 yyh-gl  staff  1526 12 17 23:41 go.sum
drwxr-xr-x  3 yyh-gl  staff    96 12 18 18:26 hoge
-rw-r--r--  1 yyh-gl  staff   145 12 18 18:28 main.go
hogeとfooはディレクトリだから拡張子がないだけで実行ファイルではありません。
build tagについて
最後にbuild tagについて見て終わろうと思います。
build tag とは
//go:build
↑コードの先頭付近に記載されているこんなやつです。
公式Doc
ビルド対象を切り分けるのに使います。
実際に試してみます。
まずは以下のようなコードを用意します。
Go Playground
main.go
package main
import "github.com/yyh-gl/go-playground/src"
func main() {
	src.Hoge()
}
src/hoge1.go
//go:build hoge
package src
import "fmt"
func Hoge() {
	fmt.Println("hoge1: //go:build hoge")
}
src/hoge2.go
//go:build !hoge
package src
import "fmt"
func Hoge() {
	fmt.Println("hoge2: //go:build !hoge")
}
これらのファイルに対して、
-tagsオプションを使い、ビルド対象を指定した上でbuildを実行してみます。
$ go build -tags hoge main.go
$ ./main
hoge1: //go:build hoge
//go:build hogeを持つhoge1.goの内容が実行されましたね。
逆に//go:build !hogeを持つhoge2.goは無視されました。
今回は-tags hogeを指定したので、build tagとしてhogeを指定したファイルがビルド対象となりました。
逆に//go:build !hogeはhogeが指定されていないときにビルド対象になることを意味するので、
今回は無視されました。(!が否定を意味します)
hoge2.goをビルド対象としたい場合は以下のようにすればできます。
$ go build -tags foo main.go
$ ./main
hoge2: //go:build !hoge
$ go build main.go
$ ./main
hoge2: //go:build !hoge
特筆すべき点は-tagsを指定しない場合もビルド対象になる点です。
AND条件やOR条件も使用できるので、できることはいろいろとありそうですね。
参考
build tag が使われている例
最後に build tag が実際どこで使われているのか紹介して終わります。
Googleが作成したツールで、WireというDIツールがあります。
google/wire
Wireでは、依存関係を定義したGoファイル(wire.go)を見て、
依存関係を解決し、必要なコード郡(wire_gen.go)を生成します。
システムを動かすにあたって必要になるコードはwire_gen.goのみです。wire.goはコード生成時には必要ですが、システムを動かすときには必要ありません。
したがって、各ファイルのbuild tagは以下のようになっています。
wire.go:wireinjectwire_gen.go:!wireinject
すなわち、build tagの指定なしでbuildを実行すると、
wire.goは無視してwire_gen.goだけを見るようになっています。
ちなみに、wireinjectタグはWireによるコード生成時にのみ指定されます。
参考コード
よって、コード生成時には、逆にwire_gen.goは無視してwire.goだけを見るようになっています。
Wire公式チュートリアル にbuild tagに関する言及があるのであわせてご覧ください。
さいごに
今回は、僕が普段何気なく使っていたbuildについて深ぼってみました。
ただ、buildにはまだ他にもオプションがあるので、
一度調べてみると「こんなことできたんだ」という発見に繋がるかもしれません。
みなさんもぜひ一度、Goコマンドについて深ぼってみてはいかかでしょうか。



