Go の stringer (ツール)でコードの変更を検知してコンパイルエラーを発生させることができる理由

この記事は [N高グループ・N中等部 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nhighschool-njuniorhighschool) 18日目の記事です。N高生です。

Go には、整数型に `fmt.Stringer` インターフェースのコードを生成してくれる [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) というツールがあります。自分で定義した型と定数を使って、列挙型のような使い方ができます。`go generate` した後に、定義側を変更して生成コード側との間で不整合を起こすと、コンパイルエラーが発生します。

## 使い方

1. インストール

   ```plaintext
   go install golang.org/x/tools/cmd/stringer@latest
   ```

1. 型と定数を定義

   `-linecomment` フラグで行コメントを `String()` の返り値として使うようになる。

   ```go
   package main

   import "fmt"

   //go:generate stringer -linecomment -type=Junishi
   type Junishi int

   const (
   	_       Junishi = iota
   	Ne              // 子
   	Ushi            // 丑
   	Tora            // 寅
   	U               // 卯
   	Tatsu           // 辰
   	Mi              // 巳
   	Uma             // 午
   	Hitsuji         // 未
   	Saru            // 申
   	Tori            // 酉
   	Inu             // 戌
   	I               // 亥
   )
   ```

1. コマンド実行

   ```plaintext
   go generate
   ```

   xxx_string ファイルが生成される。

これで、 `String()` メソッドで文字列を返せるようになった。

```go
func junishi(year int) string {
    // 西暦から 3 引いて 12 で割った余り
	return Junishi((year - 3) % 12).String()
}

func main() {
	junishi(2024) // 辰
	junishi(2025) // 巳
}
```

↑ 年から十二支の文字列を返す関数

## 新しい定数を追加してみる

```diff_go
const (
	_     Junishi = iota
	Ne            // 子
	Ushi          // 丑
	Tora          // 寅
	U             // 卯
	Tatsu         // 辰
	_
	Mi      // 巳
	Uma     // 午
	Hitsuji // 未
	Saru    // 申
	Tori    // 酉
	Inu     // 戌
	I       // 亥
)
```

辰年と巳年の間に新しい定数を追加。これにより巳年以降の値が1ずつズレて、コンパイルが通らなくなる。

```plaintext
$ go build .
# junishi
./junishi_string.go:16:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:17:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:18:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:19:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:20:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:21:8: invalid argument: index 1 out of bounds [0:1]
./junishi_string.go:22:8: invalid argument: index 1 out of bounds [0:1]
```

↑ ❗️ コンパイル通らない

## 理由

**junishi_string.go** を確認すると、生成されたコードに [Blank identfier](https://go.dev/ref/spec#Blank_identifier) な関数が定義されている。

```go
func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[Ne-1]
	_ = x[Ushi-2]
	_ = x[Tora-3]
	_ = x[U-4]
	_ = x[Tatsu-5]
	_ = x[Mi-6]
	_ = x[Uma-7]
	_ = x[Hitsuji-8]
	_ = x[Saru-9]
	_ = x[Tori-10]
	_ = x[Inu-11]
	_ = x[I-12]
}
```

↑ 生成されたコードの一部  

関数の中では、長さ **1** の**空 struct** の配列が定義されていて、 現在の定数の値とコード生成時の定数の値を基に index を計算してアクセスし、結果を `_` に代入している。

`_ = x[Ne-1]` (この場合 `Ne` は **1**) 、 `1-1` = **0** なので正常にアクセスできる。  
同じように、 `Ushi` は **2** なので `2-2` = **0** 、 `Tora` は **3-3** = **0** でそれぞれ正常にアクセスすることができる。

定数宣言リストの間に新しい定数を追加すると、それより後ろの定数の値がズレて、

**現在の定数の値** - **コード生成時の定数の値** = **0**

の式が成り立たなくなってしまい、 index が範囲外で [InValidIndex エラー](https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#InvalidIndex) になる。

リストから定数を削除した場合は、削除された定数は未宣言で [UndeclaredName エラー](https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#UndeclaredName)、それより後ろはすべて index が負となってしまいこれも InValidIndex エラー になる。

再度 `go generate` で生成するとコンパイルが通るようになる。

参考

* <https://pkg.go.dev/golang.org/x/tools/cmd/stringer>
* <https://go.dev/ref/spec#Iota>
* <https://go.dev/blog/generate>