go1.24アプデ後のgo vetに注意
どうもtakemaruです!
私、元花粉症で20代前半までこの時期は常に鼻を真っ赤にしていました🤧
元というのは、
実は筋トレを始め腸内環境に気を配り食物繊維や発酵食品をよく摂取しているのですが、ある時からピタッと花粉症が治りました😳
なんでも、腸内環境が良くなり腸内細菌が増えると花粉に対して反応していたレセプターが反応しなくなるとかなんとか…
対処療法も大事ですが原因療法で根本を治すことも大事だと思うので食物繊維と発酵食品を日頃摂取することオススメです✨(花粉症解消でQOL爆上がりです)
.
.
.
本題に入ります。
遭遇した事件
go1.24.1出たしそろそろアプデするかぁ
↓
プルリク投げる
↓
いつも通りCIでgo vet
実行される
↓
大量のnon-constant format string in call to fmt.Sprintf
↓
はにゃ?!
これについて解説します
go vetのGo1.24変更点
Go 1.24 がリリースされ、Go ツールチェーンのさまざまな部分に変更や新機能が入りました。
毎回のことではありますが、新バージョンでのビルドやテストで想定外のエラーが発生することもしばしばあります。
Go 1.24 では新しく tests アナライザ が導入されたほか、従来から存在している以下のアナライザにいくつかの変更が加えられました。
- printfアナライザ
- buildtagアナライザ
- copylockアナライザ
これらのうち、本記事で重点的に解説するのは printf アナライザの強化です。
具体的には、非定数の文字列をフォーマットとして渡しながら引数を一つも指定しないPrintf
や Sprintf
などの呼び出しをデフォルトで警告するようになりました。実際にgo vet
を回した際に下記のようなエラー・警告が出た方も多いかもしれません。
non-constant format string in call to fmt.Sprintf
ここでは、その背後にある議論や技術的課題、どのように対応するべきかなどを詳しく解説します。
1. 背景: Go 1.24 と go vet の役割
1.1 Go における静的解析と go vet
Go には品質向上やバグ検出を助けるための静的解析ツールがいくつか存在します。その代表格が go vet です。go vet は言語仕様や一般的な Go コーディング規約、よくあるバグパターンなどをもとにコードをスキャンし、潜在的な問題を早期発見することを目的としています。
参照:
例えば、典型的なチェックとしては「Printf 系関数に指定するフォーマット文字列と引数の型が合わない」「構造体を誤ってコピーしている(ロックの扱いの不備)」「ビルドタグの指定が正しくない」などが挙げられます。このような静的チェックはランタイムエラーを未然に防ぎ、より安全で可読性の高い Go コードを書くうえで非常に有用です。
1.2 Go バージョンアップ時の go vet の強化
Go がマイナーバージョンアップ(1.x が上がるとき)するたびに、go vet
のルールセットやアナライザが追加・拡張されることがあります。新しいアナライザが導入されると、今まで問題なくコンパイルできていたコードに対して突然警告やエラーが出るケースがあり、開発者を驚かせる原因のひとつになります。
たとえば、Go 1.24におけるprintf アナライザ
の変更は 「非定数のフォーマット文字列を引数なしで渡す」というケースに対する検出と警告 を追加したことです。なぜこれが問題視されるのか、次項で詳しく見ていきましょう。
2. 大きな変更点: 4 つのアナライザのアップデート
Go 1.24 のリリースノートで明記されているように、今回のgo vet
では大きく4つのアナライザで変更や新機能が追加されました。
- 新アナライザ
tests
の追加 - 既存の
printf アナライザ
の強化 - 既存の
buildtag アナライザ
の強化 - 既存の
copylock アナライザ
の強化
それぞれの概要を順に見ていきましょう。(printf アナライザのみ参照したい方はここから)
2.1 新アナライザ「tests」の追加
Go 1.24 から、新しくtests アナライザ
が追加されました。
これはテストパッケージに含まれるテスト関数、ファズ(fuzz)関数、ベンチマーク関数、サンプル関数などに対し、命名規則やシグネチャの形式を検査するアナライザです。
具体的には次のような問題を検出します。
- 誤ったテスト関数のシグネチャ
例えばfunc TestXxx(t int) { ... }
のように、テスト関数であるにもかかわらずtesting.T
を引数に取っていない場合など。 - 不適切な名前
TestXxx
やBenchmarkXxx
、ExampleXxx
といった命名規則に反している関数を検知し、「実はテストのつもりだったのにテストとして認識されない」ケースを報告します。 - 存在しない識別子を例示しているサンプルコード
ExampleFoo
として例を示しているのに実際にはFoo
という関数や型が存在しない、といったケースを警告してくれます。
こうした問題は気づきにくく、意図せずテストやサンプルが無視されてしまうことがあるため、非常に便利な検査機能と言えるでしょう。(go1.24でテスト便利機能増えて嬉しい🎉)
なお、このtests アナライザ
はgo test
を実行するときにデフォルトで有効になっています。
そのため、新たにGo1.24にアップデートした後にテストを実行した際、「今まで通っていたはずのテストが動かない?」と困ったときには、まずtests アナライザ
による警告が出ていないかチェックしてみてください。
2.2 printf アナライザの強化 — 非定数フォーマット文字列の警告
こちら本題のため重めに解説します!
2.2.1 何が変わったのか
Go 1.24 で注目されているのが、すでに存在していたprintf アナライザ
の強化です。新たに以下のようなルールが追加されました。
「fmt.Printf(s) のように、非定数(変数)のフォーマット文字列を引数なしで与える呼び出し」に対して診断メッセージを出す
具体的には、fmt.Sprintf(s) も含めて、s が定数でない(つまりリテラルでない)場合は、以下のような診断が表示されるようになりました。
non-constant format string in call to fmt.Sprintf
2.2.2 背景と狙い
この変更の背景は、GoogleのGoコアチームが確認したコードベースの統計や、コミュニティからの多数の指摘にあります。fmt.Printf
やfmt.Sprintf
といった「フォーマット文字列を使う」関数は、通常下記のような形で呼ばれます。
fmt.Printf("%s\n", user.Name)
これは「%s」という定数フォーマット文字列を使い、user.Name
という引数を差し込む想定です。しかし、誤って次のように書いてしまうケースが散見されるといいます。
v := user.Name
fmt.Printf(v)
この書き方は一見すると「文字列をそのまま表示する」意図のように見えますが、実際にはv
を「フォーマット文字列」として扱うため、v
の中身に%
が含まれていた場合に思わぬ挙動となります。さらに、引数リストには追加の値が一切指定されていないため、想定外のフォーマットが行われる可能性が非常に高いのです。
この誤用は実装者の意図と動作が乖離している可能性が高い と考えられるため、「変数のフォーマット文字列を使いながら引数なし」はほぼすべてがバグだろう、という判断に基づいて今回の診断が追加されました。
余談ですがGoogleのGoチームには読みやすさレビューというプロセスがあるんですかね?🤔https://google.github.io/styleguide/go#normative のあたりを目的としたのレビュー専門チームも存在したり?(この辺り詳しい方いらっしゃいましたら教えて欲しいです🙏)
I feel like I see this mistake in Google Go readability reviews at least once a week, and the Google corpus shows up hundreds of violations with what looks like close to 100% precision:
(https://github.com/golang/go/issues/60529#issue-1733343158)
追記:
こちらの記事を@tentennさんにリポストしていただきました!
それがきっかけで、リポストにリプライをしていただいた@fumitoshi_ukaiさんから
Readability mentors teamsがやはり存在するのですね!!
気になっていたのでこの情報共有は非常にありがたかったです、お二人にはとても感謝しています🙏
2.2.3 具体的な例と対策
もし「ただ文字列を表示したいだけ」なのであれば、以下のようにfmt.Print
やfmt.Println
、あるいはfmt.Fprintf
に書き換えるべきです。
// 誤った例
func PrintUserName(u User) {
// v が非定数のフォーマット文字列になっている
v := u.Name
fmt.Printf(v)
}
// 修正例1: ただの出力ならfmt.Print系
func PrintUserName(u User) {
fmt.Print(u.Name)
}
// 修正例2: 改行したいならfmt.Println
func PrintUserName(u User) {
fmt.Println(u.Name)
}
// 修正例3: ファイルやWriterに書き出すならfmt.Fprintf
func PrintUserName(u User, w io.Writer) {
fmt.Fprintf(w, "%s", u.Name)
}
「いや、変数のフォーマット文字列を本当に使いたいんだ」という場合は、通常その変数は複数の引数と組み合わせて使う形になっているはずです。たとえば以下のようなコードです。
func Log(format string, args ...interface{}) {
// 明示的にフォーマット文字列を渡し、引数を展開している
fmt.Printf(format, args...)
}
上記の例では「変数のフォーマット文字列かつ引数あり」という形なので、今回のgo vet
の警告には該当しません。あくまで 「変数のフォーマット文字列かつ引数なし」を誤用とみなし、警告を出すようになった点がポイントです。
2.2.4 この警告が出現する条件
Issue #60529の議論によると、このチェックは「既存のコードに相当数の違反が見つかる」ことが予想されました。そこでGoチームは、Goモジュールのバージョン(go.mod
に記載されるgo directive
、または//go:buildコメント
で指定されるバージョン)が1.24 以上の場合にのみ適用する という運用方針を取りました。
したがって、以下のようなケースでは警告が抑制されます。
-
go.mod
に書かれているバージョンがgo 1.23
以下 -
//go:build go1.23
など、ビルドタグで旧バージョンを指定している
2.3 buildtag アナライザの強化
Go コードにおいてソースファイルの先頭で//go:build ...
や// +build ...
といったビルドタグを使うことで、特定のOSやアーキテクチャ、Goバージョンなどに応じてファイルをコンパイル対象から除外・選択できます。今回のGo 1.24ではこのbuildtag アナライザ
も強化され、無効なGo メジャーバージョンビルドコンストレイントが指定されている場合を警告します。
例えば、次のような build タグは誤りとして検出されます。
//go:build go1.23.1
Go のバージョン指定はメジャー/マイナーバージョンまでであり、パッチバージョン(ここでは.1)はbuildタグとしては正しく扱えません。正しくは以下のように書く必要があります。
//go:build go1.23
実際のところ、Go のパッチバージョンに応じてビルドを切り替えるケースはほとんどありません(そもそも、パッチバージョンで言語仕様が変わることは原則として無いはずです)。そのため、.1や.2といった指定が入っている場合は、ほとんどミスである可能性が高いでしょう。
2.4 copylock アナライザの強化
最後はcopylock アナライザ
です。これはsync.Mutex
などを含む構造体がコピーされるケース を検出し、コピーによるデッドロックや不正な動作を防ぐことを目的としています。
Go 1.22 で「3 つの要素からなる for ループの振る舞い」が変更されました。具体的には、
for i := iter(); done(i); i = next(i) {
// ...
}
のようなループでは、Go 1.22以降 各ループのイテレーション毎に新しい変数が生成され、前イテレーションの値がコピーされるという実装になりました。もしi
がsync.Mutex
のようなロックを含む値だった場合、イテレーションごとにロックがコピーされてしまい安全ではありません。
Go 1.24のcopylockアナライザでは、このように「3-clause for ループ内でロックを含む変数を使っている」場合に警告を出す 機能が強化されています。これは次のようなケースに該当します。
type LockHolder struct {
mu sync.Mutex
}
// ロックを含む値を for ループの変数として使っている
for lh := LockHolder{}; someCondition(lh); lh = nextLockHolder(lh) {
// ...
}
上記のようなコードはロックのコピーが繰り返されるため、意図しない挙動を招きます。ロックを伴う構造体は基本的にポインタを使って管理するのが Go での一般的な流儀です。つまり下記のように書き換えるほうが望ましいでしょう。
for lh := &LockHolder{}; someCondition(lh); lh = nextLockHolderPtr(lh) {
// ...
}
「ループ変数としてコピーしてしまう」というバグは最初は気付きにくいため、このアナライザの強化は多くの人にとってありがたい変更ですね。
3. Go 1.24 アップデート時の注意点と対応策
3.1 CI やビルド環境でのケア
既に述べたように、新しいprintf アナライザによるチェックはGoモジュールのバージョンが1.24以上のときに適用されます。よって
-
go.mod
のgoバージョンを1.24に上げている - あるいは
//go:build go1.24
などで1.24以上を指定している
場合には、CI でgo vet
やgo test
が走った時点で該当のエラーが大量に発生する可能性があります。もし該当する警告が出るようなら、上記で触れたようにコードの書き換えを進めましょう。
一方、プロジェクトがまだ1.23以下をサポートし続けるなどの理由でgo.mod
を更新できない場合、Go 1.24をインストールしていても警告は出ません。ただ、将来的にはバージョンを上げるタイミングで一気にエラーが増えることが予想されるため、余裕があるうちにリファクタリングしておくとスムーズです。
3.2 ライブラリ開発者への影響
もし外部向けにライブラリを公開している場合でも、自身のライブラリコード内にfmt.Printf(v)
のような記述があれば、Go1.24でビルドした利用者から警告報告が上がってくるかもしれません。基本的には問題箇所を修正し、「フォーマット文字列と引数を正しく使う」か「ただ文字列を表示するならfmt.Print系を使う」かのどちらかを明確にしましょう。
特にログやデバッグ系のユーティリティ関数で「任意の文字列をそのまま表示してほしい」場合はfmt.Print
/fmt.Println
のほうが適切です。逆に本当にフォーマット機能を使いたい場合は、引数可変の関数シグネチャに変更して利用者がformat, args...を渡せるようにすると誤用を防止しやすくなります。
3.3 誤っている場合の修正ポイント
実際に引っかかるケースとしては、たとえば以下のようなものがあります。
- 「ログを出力したいだけなのに
Printf
を使っていた」 - 「ユーザ入力など可変の文字列をそのまま表示したいのに
Sprintf
を使って結果を返していた」 - 「フォーマット文字列を切り替えるロジックを書くつもりだったが、引数が抜けていた」
まずは「本当にフォーマット指定が必要なのか?」を判断し、必要ない場合は単純な出力関数に書き換えましょう。もし本当にフォーマットが必要なら、可変引数を正しく渡すコードに修正することが肝要です。
まとめ
本記事では、Go 1.24において強化・追加されたgo vet
のアナライザを中心に解説しました。特に多くの開発者に影響が及ぶと思われるのが、printf アナライザ
の「非定数フォーマット文字列 + 引数なし」に対する新しいチェックです。誤用パターンがコードベースに潜んでいると、Go 1.24 へアップデートした際に次のようなエラーが一斉に報告されるでしょう。
non-constant format string in call to fmt.Sprintf
こうした警告が出た場合には、
- 「本当に文字列をそのまま表示したいだけ」なら
fmt.Print
/fmt.Println
/fmt.Fprint
などを使う - 「複数の引数をフォーマットに当てはめる」のが目的なら
fmt.Printf(format, args...)
の形に正しく修正する
といった対応が必要となります。
また、テストやサンプルコードの命名・シグネチャをチェックするtests アナライザ
の追加、//go:build
でのバージョン指定ミスを検出するbuildtag アナライザ
の強化、コピー禁止のロックを含む値が3-clause forループでコピーされるケースを警告するcopylock アナライザ
の強化も見逃せません。これらは一見マイナーな変更のように思えますが、潜在的なバグを炙り出す効果があります。
最後に、go vet
は味方です!
アップデートして大量の警告が出たらgo vet
に感謝しながら早めにコードを改善しておくのが最善の策でしょう!(めんどくさくてアップデートを見送るようなことがないように)
ここまで読んでいただきありがとうございました!
go vet
と仲良くして素晴らしいプログラミングライフを送ってください👋
参考
Discussion
tip.golang.orgはmasterブランチ(開発版)を元に公開されているサイトでして、各バージョンのリリース前だとリリースノートは以下のURLでも良いんですが、
リリース後は以下のURLを掲載するのが良さそうです。ありがとうございます!!
下記を参照するよう修正します🙏
Xで情報頂いたので、こちらに貼らせてください。
参考:https://ukai-go-talks.appspot.com/2014/gocon.slide#1
ありがとうございます!!非常に気になっていたのでとても嬉しいです!!
なるほど、readability mentors teamが存在するのですね🤔