Zenn
🚨

go1.24アプデ後のgo vetに注意

2025/03/17に公開
5
5

どうも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 ツールチェーンのさまざまな部分に変更や新機能が入りました。

http://go.dev/doc/go1.24

毎回のことではありますが、新バージョンでのビルドやテストで想定外のエラーが発生することもしばしばあります。

Go 1.24 では新しく tests アナライザ が導入されたほか、従来から存在している以下のアナライザにいくつかの変更が加えられました。

  • printfアナライザ
  • buildtagアナライザ
  • copylockアナライザ

これらのうち、本記事で重点的に解説するのは printf アナライザの強化です。
具体的には、非定数の文字列をフォーマットとして渡しながら引数を一つも指定しないPrintfSprintfなどの呼び出しをデフォルトで警告するようになりました。実際に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つのアナライザで変更や新機能が追加されました。

  1. 新アナライザtestsの追加
  2. 既存のprintf アナライザの強化
  3. 既存のbuildtag アナライザの強化
  4. 既存のcopylock アナライザの強化

それぞれの概要を順に見ていきましょう。(printf アナライザのみ参照したい方はここから)

2.1 新アナライザ「tests」の追加

Go 1.24 から、新しくtests アナライザが追加されました。
これはテストパッケージに含まれるテスト関数、ファズ(fuzz)関数、ベンチマーク関数、サンプル関数などに対し、命名規則やシグネチャの形式を検査するアナライザです。

具体的には次のような問題を検出します。

  • 誤ったテスト関数のシグネチャ
    例えばfunc TestXxx(t int) { ... }のように、テスト関数であるにもかかわらずtesting.Tを引数に取っていない場合など。
  • 不適切な名前
    TestXxxBenchmarkXxxExampleXxxといった命名規則に反している関数を検知し、「実はテストのつもりだったのにテストとして認識されない」ケースを報告します。
  • 存在しない識別子を例示しているサンプルコード
    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.Printffmt.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.Printfmt.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の警告には該当しません。あくまで 「変数のフォーマット文字列かつ引数なし」を誤用とみなし、警告を出すようになった点がポイントです。
https://github.com/golang/go/issues/60529#issuecomment-2045792601

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以降 各ループのイテレーション毎に新しい変数が生成され、前イテレーションの値がコピーされるという実装になりました。もしisync.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 vetgo 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

こうした警告が出た場合には、

  1. 「本当に文字列をそのまま表示したいだけ」ならfmt.Print/fmt.Println/fmt.Fprintなどを使う
  2. 「複数の引数をフォーマットに当てはめる」のが目的ならfmt.Printf(format, args...)の形に正しく修正する
    といった対応が必要となります。

また、テストやサンプルコードの命名・シグネチャをチェックするtests アナライザの追加、//go:buildでのバージョン指定ミスを検出するbuildtag アナライザの強化、コピー禁止のロックを含む値が3-clause forループでコピーされるケースを警告するcopylock アナライザの強化も見逃せません。これらは一見マイナーな変更のように思えますが、潜在的なバグを炙り出す効果があります。

最後に、go vetは味方です!
アップデートして大量の警告が出たらgo vetに感謝しながら早めにコードを改善しておくのが最善の策でしょう!(めんどくさくてアップデートを見送るようなことがないように)

ここまで読んでいただきありがとうございました!
go vetと仲良くして素晴らしいプログラミングライフを送ってください👋

参考

https://tip.golang.org/doc/go1.24#vet
https://github.com/golang/go/issues/60529
https://github.com/golang/go/issues/64127
https://github.com/golang/go/issues/66387

5

Discussion

take0take0

ありがとうございます!!非常に気になっていたのでとても嬉しいです!!

なるほど、readability mentors teamが存在するのですね🤔

ログインするとコメントできます