はじめに
Go1.21連載の2本目です。
取り上げるのは次のgo/ast、go/buildあたりの更新についてです。
- go/ast
- go/build
- 関連してToolの互換性の話も含みます
- go/build/constraint
- go/token
- go/types
go/ast
はGoコードの抽象構文木を表現するパッケージで、go/parser
パッケージでコード解析をすると取得できます。 go/token
とか go/types
は概ね解析した結果の型やトークンを表現するもので比較的セットで利用することが多いかと思います。
go/build
パッケージはGoのビルド周りの情報を集めるパッケージです。今回のアップデートは go/ast
と関連が強かったので、この記事でまとめて紹介します。
バージョンは Go 1.21rc4
であり、リリースまでに多少変更があるかも知れません。ご了承ください。
go/ast の IsGenerated()
go/ast
パッケージに IsGenerated()
というそのコードが自動生成されたかどうかを判定するためのヘルパー関数が追加されました。
func IsGenerated(file *File) bool |
あるGoコードが自動生成されたかどうかは、cmd/goパッケージのドキュメントに記載がある通り、以下の行を含んでいる場合(ファイルの行頭じゃなくてもOK)に、判断できるとあります。 IsGenerated()
でこの行を含んでいるかが判断できるようになりました。
^// Code generated .* DO NOT EDIT\.$ |
go/ast: add func IsGenerated(*File) bool #28089 で議論されており、上記の自動生成の判定条件が現在広く受け入れられているのであれば、公式でAPIを提供したら便利ではという提案でした。
これ以前は起票者が開発した shurcooL/go/generated というパッケージを利用したり、regexp パッケージを用いた正規表現でファイルの判定や、人によってはシェルスクリプトで判定していたようです。
grep -Exq '^// Code generated .* DO NOT EDIT\.$' "$file" |
IsGenerated()
の関数を新規に追加実装する箇所は、 ast.File
や build.Package
なども考えられたかと思いますが、互換性やフィールド追加は神経質に行いたいということで、ヘルパー関数となったようです。次の章で説明するGoVersionは ast.File
へのフィールド追加なので、塩梅は分かるような分からないような。GoVersionは論理式が入って計算されるのでそれなりのロジックだけど、自動生成判定はシンプルなのでヘルパー関数になったんですかね、多分。
追加された IsGenerated()
を試してみます。
package main |
注意としては ParseFile()
の引数に parser.ParseComments
を渡さないと、コメントがフィールドに格納されないため、結果が false
になります。ここは注意です。
- f, err := parser.ParseFile(fset, "", s, parser.ParseComments) |
上手く利用すれば、自動生成されたコードを除去してリントのルールを追加するなどが行いやすくなると感じました。
ちなみに実装は strings.CutPrefix()
、strings.CutSuffix()
で判定しており、正規表現は利用していない愚直なコードでした。
https://github.com/golang/go/blob/go1.21rc4/src/go/ast/ast.go#L1092
go/ast の File.GoVersion
go/ast
パッケージの File
に GoVersion
というフィールドが追加されました。
type File struct { |
GoVersion
が示す意味は、 //go:build
や // +build
のディレクティブで示した最小のGoバージョンが入るそうです。
go/ast, go/build/constraint: support for per-file Go versions using //go:build lines #59033 によると、Goの互換性は以下の方針です。直接的には2の対応のために追加されたようです。
- Go 1.21から go.mod にかかれた
go
のバージョンが、最小バージョン要件となる。例えば、Go 1.21がGo 1.22とかかれたgo.modファイルをビルドできない - Goの最小バージョンが低くても、
//go:build
タグに新しいバージョンを指定することを可能とする。また、逆に//go:build
に古いバージョンを指定することも可能として、新しい機能をロックアウト(無効化)できるようにする。互換性のためダウングレードはGo 1.21以降のgo.mod に適用される
//go:build
ディレクティブはGo1.17から導入された書き方で、従来は// +build
でした。Build Constraintsと呼ばれ、例えば以下のように記載するとWindowsとLinux用のバイナリに対してビルド対象となります。
//go:build windows linux |
OS名(Windows, linux, solaris, darwinなど)やアーキテクチャ名(386
, arm
, ard64
など)だけではなく、バージョンも指定できるようになったようですね。
//go:build
ディレクティブに (linux && go1.20) || (windows && go1.21)
を追加して動かしてみます。実行環境はWindowsなので、 go1.20
が出力されます。
package main |
ディレクティブで指定されたGoバージョンも取得できるのはマニアックに感じます。それなりの規模と歴史を持つ、コードベースを解析するような上級者向けのアップデートかなと思いました。
Tools
Goは下位互換性を重視していて強みの1つとしています。Go 1の互換性ポリシーに従って破壊的変更は抑えられていますが(例えば公開されたインタフェースにメソッドを追加しないなど)、既存の実装がバグであり(既存のコードが古いアルゴリズムやバグのある動作に依存している場合)、それを修正すると、バグである挙動に依存したプログラムは壊れる可能性があるとのこと。対応としては前章にあるような go.mod
などのgoバージョンを読み取るようになったり、GODEBUG環境変数の存在があります。
具体的に互換性でどのような課題が出たんだろうと思いましたが、Proposal: Extended backwards compatibility for Go にいくつか例が書いてありました。Kubernetesチームでは平均で年1回ペースで問題が出たそうです。
例えば以下のような事象があったそうです。
- Go 1.17 では、0127.0000.0000.0001 など、先頭にゼロが付いているアドレスを拒否するように net.ParseIP が変更(BSD派生システムでは8進数として解釈するが、Goでは複数のRFCに従い10進数として解釈)
- Kubernetesクラスタは先頭ゼロのアドレスを保存している可能性があり、この変更によりノードアクセスができなくなる可能性があり、Go 1.17へのアップデートはブロックされた
上記のようなケースでもGODEBUG環境変数で過去の挙動になるように上書きしたり、 go:debug
ディレクティブで個別に指定することを検討中だそうです。互換性にかける熱意が常に素晴らしいなと感じました。
go/build
//go:
で始まるビルドディレクティブを Package構造体の Directives
, TestDirectives
, XTestDirectives
で参照できるようになりました。
go/build/constraint
ビルド時に指定された、Goの最小要件バージョンを返す、GoVersion()
が新しく追加されました。ドキュメントに書かれた例がわかりやすいです。
GoVersion(linux && go1.22) = "go1.22" |
go/token
ファイルの行番号を返す、 File.Lines()
のメソッドが追加されました。
go/token: add a (*File).Lines method #57708で提案された内容です。中身を読むと、すでに SetLines()
メソッドは存在しており、getter が存在しなかったとのこと。
例としては、internal/gcimporter というパッケージで利用したいということで、おそらく goplsとの連携でコード補完などで利用したいのかなと思います。
go/types
Package.GoVersion()
のメソッドが追加されました。中身は go/ast.File.GoVersion()
と同じ。
go/types: add Package.GoVersion method #61175 によると、ここのチェッカーが特定のパッケージに紐づいたGoバージョンを取得できると便利なため、追加する。厳密に言うとGo1.21には不要だが、Go 1.22では必要になるため(おそらくforループの変数の件だと思われる)、ツール作成者のためにも追加するとのこと。
まとめ
- Go 1.21では自動生成したコードかどうかを判定するヘルパー関数が標準で追加されたので、自前で正規表現を書いたり、サードパーティのパッケージをインポートする必要がなくなったよ
- Go 1.21でビルド時のGoバージョンを
//go:build
ディレクティブで指定できるようになりました。 - ディレクティブで指定した内容は、
go/ast
やgo/types
パッケージで合わせて取得できるようにあっているため、コード解析やツール作成者にも優しい変更です
個人的には普段余り利用しない、意識しないパッケージだったので調べていて勉強になりました。最後まで読んでいただきありがとうございました。