フューチャー技術ブログ

Go 1.27の go fix アップデート

The Go gopher was designed by Renee French.

Go 1.27 リリース連載の9本目です。

はじめに

TIG真野です。

1.26時の前作に続いて、Go 1.27における go fix の機能追加や変更点、背景を解説します。

検証は go1.27rc2 linux/amd64 で行いました。

サマリ

リリースノートには3点書かれています。整理すると次の通りです。

  1. モダナイザーの新規追加: リリースノート上は atomictypesembedlitslicesbackwardunsafefuncsの4つが追加されました。ただ、 go tool fix help を実行すると errorsastype も含まれ、実際は5つ追加されているようです。従来分と合わせると登録analyzer数は1.26の22個 → 26個1になりました
  2. fmtappendfの削除: 既存のモダナイザーでしたがスタイル上の懸念が理由
  3. waitgroupがリネームされた: go vet 側に先住していた同名のバグ検知analyzerと紛らわしいため、waitgroupgo にリネーム

1.26ではイマドキの書き方への置換中心だったのに対し、1.27の新顔は並行処理・unsafeといった「間違えると事故る」内容も含まれています。ツールとしてのレベルアップを感じます。

モダナイザーの新規追加

新しいモダナイザー5つについて、go fix -diff ./... の書き換え結果と、背景を見ていきます。

書き換え結果は視認性優先で、package main やimport文の増減(slices の追加など、go fix が自動で面倒を見てくれます)を省略しています。あしからず。

atomictypes: sync/atomic関数を型付きatomicに

Issueは #77352です。

before/after
-var counter int64
+var counter atomic.Int64

func increment() int64 {
- atomic.AddInt64(&counter, 1)
- return atomic.LoadInt64(&counter)
+ counter.Add(1)
+ return counter.Load()
}

atomic.AddInt64(&x, 1) のような関数スタイルを、Go 1.19で追加された型付きatomic(atomic.Int64 など。提案は #50860)へ書き換えます。

従来の関数スタイルには2つの困りごとがありました。

  1. 非atomicアクセスを止められない: 変数自体はただの int64 なので、うっかり x++x == 0 と書けてしまい、データ競合の温床でした。atomicに触るべき変数かどうかは、宣言を見ても分かりません
  2. 32bit環境でのアライメント問題: 64bit系のatomic関数は対象変数が64bitアラインされていることを要求し、structフィールドの並び順次第で実行時パニックする罠が有名だったようです。atomic.Int64 型は自動で正しくアラインされ、この問題ごと消えます

atomictypesは他のモダナイザーと毛色が違い、式の書き換えではなく変数宣言そのものを atomic.Int64 に変えて、全利用箇所を追随させるという大掛きな変換をします。

その分だけ発動条件は厳格で、手元の検証では、対象変数に非atomicなアクセスが1箇所でもあると(例:&counterunsafe.Pointer に渡していた)書き換え全体が抑止されました。「全使用箇所がatomic操作である場合のみ書き換える。さもないとデータ競合を隠してしまう」とソースコメントにも明記されています。

embedlit: 埋め込みフィールドのネストしたリテラルを直接初期化に

Issueは#77965です。

before/after
 // type User struct { Base; Email string }
// type Base struct { ID int; Name string }
return User{
- Base: Base{ID: 1, Name: "mano"},
+ ID: 1, Name: "mano",
Email: "mano@example.com",
}

この文法の拡張についてはGo 1.27の構造体リテラルのキー拡張で、埋め込みフィールドの初期化が楽になった を確認ください。

embedlitはこの新記法への書き換えで、実は2パターンあります。

書き換えパターン
// パターン1: ネスト除去
t := T{U: U{x: 1}} → t := T{x: 1}

// パターン2: リテラル直後の代入の取り込み
t := T{}
t.x = 1
// ↓
t := T{x: 1}

新しい言語機能と同時にモダナイザーを同梱してリリースする、素敵な対応です。

slicesbackward: 手書きの逆順ループを slices.Backward に

Issueは#78484です。

before/after
 func printReverse(items []string) {
- for i := len(items) - 1; i >= 0; i-- {
- fmt.Println(items[i])
+ for _, item := range slices.Backward(items) {
+ fmt.Println(item)
}
}

slices.Backward() は初見でした。便利そうです。

この手書きの逆順ループ、len(s) - 1>= 0i-- の3点セットを毎回正しく書く必要があり、> 0 と書いて先頭要素を取りこぼす、などのミスが定番でした。Go 1.23のイテレータ導入(#61899)で slices.Backward が入り、宣言的に書けるようになっていたところへの追随です。

前方ループについては 同じ go fix の rangeint(for i := 0; i < n; i++for i := range n)が既にありましたが、逆順ループを検出・書き換えする既存linterは調べた範囲で見当たらず、おそらく初の自動化です。

なお s[i] への代入があるループは書き換えないことを手元で確認しました。読み取り専用ループのみが対象で、インデックスを他の用途にも使っている場合は for i, v := range slices.Backward(s) と両変数を残してくれます。

unsafefuncs: uintptrのポインタ演算を unsafe.Add に

Issueは #76648 です。

before/after
 func secondElem(p unsafe.Pointer, size uintptr) unsafe.Pointer {
- return unsafe.Pointer(uintptr(p) + size)
+ return unsafe.Add(p, size)
}

ポインタ演算のためだけに uintptr へ変換して足し算して戻す旧イディオムは、冗長なだけでなく危険でした。

uintptr はただの整数なのでGCがポインタとして追跡しません。変換と逆変換を1つの式で完結させれば安全ですが、うっかり uintptr 値を変数に置いた瞬間、GC移動後のdanglingポインタになり得ます。Go 1.17で追加された unsafe.Add() を使うことでこのリスクを軽減できます。

名前が複数形(unsafefuncs)なのに、現状書き換えるのは unsafe.Add だけという点は補足しておきます。ソースには unsafe.String / unsafe.Slice 系対応のTODOコメントがあり、将来の拡張を見込んでいると思われます。

errorsastype: errors.AsType[T] へ書き換えてくれる

rc2の go tool fix help を眺めていて気づいたのですが、リリースノートに記載のない errorsastype も登録されています。API自体の提案は #51945、モダナイザーのIssueは #75692です。

before/after
 func handle(err error) {
- var nf *NotFoundError
- if errors.As(err, &nf) {
+ if nf, ok := errors.AsType[*NotFoundError](err); ok {
fmt.Println("key:", nf.Key)
}
}

Go 1.26で追加されたジェネリクス版 errors.AsType[T] への書き換えです。errors.As は第2引数に「エラー型へのポインタ」以外を渡すと実行時パニックという罠があり、ジェネリクス版はこれをコンパイル時に排除できます。変数のスコープもif文に閉じて、1行減ります。

ちなみに、errors.As に渡した変数をif文の外でも使っているコードは書き換えないことを確認したので、変な書き換えはしなさそうな安心感があります。

リリースノートは正式版までに更新されるかもしれません。

fmtappendf が排除された

Issueは#77581です。

fmtappendf は []byte(fmt.Sprintf(...))fmt.Appendf(nil, ...) に書き換えるモダナイザーでした。go1.26.5 の go fix では次のように書き換えられます。

Go1.26時の書き換え例
 func formatBytes(n int) []byte {
- return []byte(fmt.Sprintf("count=%d", n))
+ return fmt.Appendf(nil, "count=%d", n)
}

go1.27rc2 では削除済みのため go fix では何も起きません。

これを外すに至った理由は2つです。

  1. より良い書き換えを邪魔する: 「このモダナイザーが fmt.Appendf を挿入する箇所の半分以上は、もう少し広い文脈を見ればずっと良い変更がある」。代表例が w.Write([]byte(fmt.Sprintf(...))) で、機械的に w.Write(fmt.Appendf(nil, ...)) にされますが、本来あるべき姿は fmt.Fprintf(w, ...) です
  2. 残るケースでも改善が微妙: 「コードがわずかに分かりにくくなる(なぜ何かを”append”しているのか?)」「nil を書かされるのはばかばかしい」

これを受けたメンテナーのAlan Donovanさんのが、「このモダナイザーを最初に追加したとき、prattmicも非常によく似た懸念を示していた。文章の世界には『2人目のレビュアーが同じ問題を指摘したら、対処が必要』という経験則がある。だから対処しよう。」といった旨をコメントしており、良い考えだと感じました。

性能面でも fmt.Appendf(nil, ...) の方がむしろ遅いというベンチマーク報告(#73666)が別途あり、「アロケーション削減」という当初の建前も揺らいでいた、という背景もあります。

fmtappendf はGo 1.26の go fix 入りからわずか1リリースでの退場となりました。一度入れた機能でも理由があれば引っ込める、という判断が健全だと感じます。偉い!

waitgroup 改め waitgroupgo は、何と被っていたのか

Issueは#77560です。

内容ですが一言でいうと、go vet 側に同名の waitgroup analyzer が先住していました。どちらもGo 1.25で追加されたものです。

  • vet版waitgroup: goroutineの中で wg.Add(1) を呼んでしまい、Wait とレースして早期リターンするバグを検知するチェッカー
  • fix版waitgroup: wg.Add(1) / go func() / defer wg.Done() の3点セットを wg.Go()に畳むモダナイザー

同じ sync.WaitGroup 対象、同じ名前で、片方はバグ検知・片方はモダン化という別物が同居していました。go1.26.5で go tool vet help waitgroupgo tool fix help waitgroup を叩くと、実際に全く違う説明が返ってきて確かに紛らわしいです。早めに修正できて良かったと思います。

さいごに

Go 1.27の go fix アップデートを紹介しました。1.27でも安全な書き換えしか行ないポリシーで開発されているため、安心して使えそうです。素敵!

こうしたモダン化は、今ではAIエージェントがコードを書くついでにやってくれる場面も多いですが、ツール側で 決定的に 行ってくれることの価値も依然として重要です。世の中のGoコードがモダンな書き方へ収束していけば、それを学習するモデル側の精度も上がり、AIがより良いコードを書いてくれる、という好循環が回ります。比較的、地味なコマンドですが、長期的なGo言語の発展にも寄与してくれると思います。

最後まで読んでいただき、ありがとうございました!


  1. 1.前回記事ではGo 1.26時点のモダン化機能を24個と紹介しましたが、go tool fix help で実測すると go1.26.0 / go1.26.5 ともに登録analyzerは22個でした(マイナーアップデートで減った訳ではなさそうです)。24という数字は、go fix には採用されていないgopls専用のモダナイザー(ベンチマーク結果を歪め得る bloop や、nil保存でない appendclipped / slicesdelete など。後述)を含めて数えていたためと思われます。本記事の個数はすべて go fix 側の実測値です。