【Golang】"go install" でアプリのバージョン情報に自動対応させる【Go 1.16+】

:::note warn
この記事は Go 1.16 より上のバージョン向けです。
:::

<h1>Go アプリのバージョン表示 with <code>go install</code></h1>

> Go 言語（以下 Golang）の**俺様アプリをバージョン情報表示に対応させたい**。

自家製 Golang アプリでバージョン情報を表示するには、一般的にソースにバージョン情報を埋め込むか、ビルド時にオプションで渡す必要があります。

ソースにバージョン情報を埋め込む場合、更新するのが面倒だったり更新忘れがあったりします。そのため、`git tag` で打ったタグを、ビルド用スクリプトなどで読み込んでビルド時の引数に渡すなどの工夫をします。

しかし、ビルド時に引数でバージョンを渡すと、今度は <strong>`go install` でインストールされた場合は反映されないのが問題</strong>となります。

つまり、**`go install` でインストールされたとしても、アプリのバージョン情報が `git tag` に追随するようにしたい**のです。

ちなみに、`go install` でインストールされた Golang アプリを一括アップデートしたい場合は `gup` コマンドを使うと便利です。

- [github.com/nao1215/gup](https://github.com/nao1215/gup/blob/main/doc/ja/README.md) @ GitHub
    - [gup: go installしたバイナリの一括アップデートコマンド（仕様と今後）](https://zenn.dev/nao1215/articles/aef3fe318848d6) @ Zenn

## TL; DR <sub><sup>（今北産業）</sup></sub>

1. `debug.ReadBuildInfo` で、ビルド時の `git` タグを取得できる。
1. サンプル<br>このサンプルは、`git tag` などでバージョンをタグ付けしておけば、アプリを `go install` されてもリポジトリのバージョンに追随します。（`go build` 除く）

	```go:main.go
	package main

	import (
		"flag"
		"fmt"
		"runtime/debug"
	)

    // Version はビルド時に `-ldflags` 経由で渡されるアプリのバージョン情報です。
	var Version string

	func main() {
		var versionFlag = flag.Bool("version", false, "")

		flag.Parse()

		if *versionFlag {
			// "go build" された時用のバージョン対応（従来の手法。要 LDFLAGS）
			if Version != "" {
				fmt.Println(Version)

				return
			}

			// "go install" された時用のバージョン対応
			if buildInfo, ok := debug.ReadBuildInfo(); ok {
				// clone された VCS の最終タグから取得するので、ローカルの場合は
				// デフォルトの "(devel)" がセットされる。
				fmt.Println(buildInfo.Main.Version)

				return
			}

			fmt.Println("(unknown)")

			return
		}

		fmt.Println("Hello, world!")
	}
	```
	- サンプルリポジトリ: https://github.com/KEINOS/go-version-opt-example/ @ GitHub
	- [オンラインで上記を <code>go install</code> してバージョン表示する結果を見てみる](https://paiza.io/projects/ISR5FRyKf_Vj5svKcRhe8A) @ paiza.IO

1. 必須条件
    - `git` などの [VCS](https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0) で、バージョン用のタグを ["vX.Y.Z" 形式](https://semver.org/lang/ja/)で付けておくこと。
    - ソースをモジュールモードに対応させておくこと。（`go.mod` と `go.sum` を置くこと）
    - `go install` でパッケージがインストールされた場合のみ反映される。<br><sub><sup>そのため、リリース用のバイナリ作成やテストなど、`go build` する場合は反映されない。`go build` では、従来通り `LDFLAGS` オプションを指定（`go build -ldflags="-X main.Version=$(git describe --tags)"`）してビルドする基本は変わらない。（[Go 1.18 で改善される可能性がある](https://github.com/golang/go/milestone/201)そうです）</sup></sub>
    - 対応 Go バージョン
        - `go.mod` で `go 1.16` 以上に設定したパッケージで使う方がいい。
        - `go.mod` が `go 1.17` なら確実なのでオススメ。

:::note info
<sub><sup>🐒&nbsp;&nbsp;この仕組みのポイントは Go 1.12 で実装された <code><a href="https://pkg.go.dev/runtime/debug#ReadBuildInfo" target="_blank">debug.readBuildInfo()</a></code> を使うところです。<code>readBuildInfo()</code>は、ビルド時に埋め込まれたデバッグ情報のオブジェクトを返すのですが、<code>obj.Main.Version</code> フィールドに、<code>main.go</code> に付いていた最終タグが入っています（デフォルトは <code>(devel)</code>）。サンプルでは、これを <code>Version</code> 変数が空の場合に利用しています。<br>しかし、実際には Go のモジュール・モードと <code>go install</code> に依存します。そのため、<code>go get -u</code> でパッケージをバイナリとしてインストールする習慣がまだある（<code>go install</code> を使わない）ユーザのことを考えると、 <code>go.mod</code> のバージョンを <code>go 1.17</code> 以上に設定している場合に、この方法を使うのが確実だと思います。（Go 1.17 は <code>go install</code> が必須になったため）</sup></sub>
:::

### TIPS

- `git` タグによるバージョン情報の取得コマンド

    ```bash
    git describe --tag
    ```
- `git` のコミット ID の取得コマンド

    ```bash
    git rev-parse --short HEAD
    ```

- バージョン情報をビルド時に渡す例

    ```bash
    TAG_APP=$(git describe --tag)
    REV_APP=$(git rev-parse --short HEAD)
    VER_APP="${TAG_APP}+${REV_APP}"
    
    go build -ldflags="-s -w -extldflags \"-static\" -X 'main.versionApp=${VER_APP}'"
    ```

<!--

## TS; DR <sub><sup><sub><sup>（たかがアプリのバージョン情報を表示させたいだけなのに学んだコマケーこと）</sup></sub></sup></sub>

### アプリのバージョン情報のベストプラクティスを求めて

Go 言語（以下 Golang）初心者の登竜門の 1 つに「アプリのバージョン情報の表示機能の実装」がある気がします。

先に「バージョン情報の管理方法のベスト」を言ってしまうと **`git` などの、バージョン管理アプリを利用して付けた「バージョンのタグを利用する」**なのですが、問題はその表示方法です。Go のバージョンによって変わるためです。

Golang は新しい言語であるためマイナー・バージョンがポンポン変わります。しかも、言語仕様が変わってもメジャーバージョンは上がりません。

そのため、初心者だと、本を買って試しても、ネット記事を漁っても微妙に動かなかったりすることが多くあります。

しかし、だんだん慣れてくると「Golang 自体のソースコードを参照して機能を把握する」ということが習慣づけられてきます。


Python や PHP などのスクリプト言語と違い、Golang の実装自体も Golang で書かれています。つまり、一度 Golang の読み方を覚えれば、どのように実装されているかも読み取れるのです。
（Delphi と似ていて良いなと思います）

実際に使う時にはソースコードを必要としないことがあります。自分のアプリで使いたい場合は別ですが、パッケージをコンパイルしてバイナリ（単体アプリ）として使いたい場合などは、ビルド済みのものを配布すればいいからです。

そのため、「アプリのバージョン情報を、どのようにバイナリに埋め込むか」が悩みどころです。
アプリを使う時にはコンパイル／ビルドしてバイナリとして使われることが大半なので、
簡単で確実なのは、ソースコードにハードコーディングする（直接書いちゃう）方法です。


Go v1.17 から、Golang パッケージのビルド & インストールが `go get -u` でインストールできなくなりました。

つまり、今後は `go get` と `go install` の役割が明確になったということです。

1. 俺様アプリ内で使うためパッケージをゲットする時は `go get`
1. パッケージをコンパイルしてアプリとして使う場合は `go install` （旧 `go get -u`）

## 従来のアプリ・バージョンの埋め込み方法

`-ldflags="-X main.Version=$(git describe --tags)"`

- Go v1.12 以降では、ビルド時の `LDFLAGS` オプションで指定していた。
    - `LDFLAGS` オプションで指定された変数の値が、ソースコード内の同名の変数の初期値として置き換えるため、ビルド後のバイナリにアプリのバージョンを埋め込めることができた。
    - `go build -ldflags="-X main.Version=$(git describe --tags)"`

-->

## 参考文献

- [ReadBuildInfo()](https://pkg.go.dev/runtime/debug#ReadBuildInfo) | debug | runtime @ pkg.go.dev
- #29228: [runtime/debug: document BuildInfo.Main.Version == "(devel)"](https://github.com/golang/go/issues/29228) | Issue | golang @ GitHub
- #29814: [cmd/go: use version control to discover the main module's version?](https://github.com/golang/go/issues/29814) | Issue | golang @ GitHub
- #172: [Add go1.12 versioning support](https://github.com/golang-migrate/migrate/pull/172) | Pull Request | golang-migrate @ GitHub