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点書かれています。整理すると次の通りです。
- モダナイザーの新規追加: リリースノート上は
atomictypes、embedlit、slicesbackward、unsafefuncsの4つが追加されました。ただ、go tool fix helpを実行するとerrorsastypeも含まれ、実際は5つ追加されているようです。従来分と合わせると登録analyzer数は1.26の22個 → 26個1になりました - fmtappendfの削除: 既存のモダナイザーでしたがスタイル上の懸念が理由
- 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です。
-var counter int64 |
atomic.AddInt64(&x, 1) のような関数スタイルを、Go 1.19で追加された型付きatomic(atomic.Int64 など。提案は #50860)へ書き換えます。
従来の関数スタイルには2つの困りごとがありました。
- 非atomicアクセスを止められない: 変数自体はただの
int64なので、うっかりx++やx == 0と書けてしまい、データ競合の温床でした。atomicに触るべき変数かどうかは、宣言を見ても分かりません - 32bit環境でのアライメント問題: 64bit系のatomic関数は対象変数が64bitアラインされていることを要求し、structフィールドの並び順次第で実行時パニックする罠が有名だったようです。
atomic.Int64型は自動で正しくアラインされ、この問題ごと消えます
atomictypesは他のモダナイザーと毛色が違い、式の書き換えではなく変数宣言そのものを atomic.Int64 に変えて、全利用箇所を追随させるという大掛きな変換をします。
その分だけ発動条件は厳格で、手元の検証では、対象変数に非atomicなアクセスが1箇所でもあると(例:&counter を unsafe.Pointer に渡していた)書き換え全体が抑止されました。「全使用箇所がatomic操作である場合のみ書き換える。さもないとデータ競合を隠してしまう」とソースコメントにも明記されています。
embedlit: 埋め込みフィールドのネストしたリテラルを直接初期化に
Issueは#77965です。
// type User struct { Base; Email string } |
この文法の拡張についてはGo 1.27の構造体リテラルのキー拡張で、埋め込みフィールドの初期化が楽になった を確認ください。
embedlitはこの新記法への書き換えで、実は2パターンあります。
// パターン1: ネスト除去 |
新しい言語機能と同時にモダナイザーを同梱してリリースする、素敵な対応です。
slicesbackward: 手書きの逆順ループを slices.Backward に
Issueは#78484です。
func printReverse(items []string) { |
slices.Backward() は初見でした。便利そうです。
この手書きの逆順ループ、len(s) - 1、>= 0、i-- の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 です。
func secondElem(p unsafe.Pointer, size uintptr) unsafe.Pointer { |
ポインタ演算のためだけに 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です。
func handle(err error) { |
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 では次のように書き換えられます。
func formatBytes(n int) []byte { |
go1.27rc2 では削除済みのため go fix では何も起きません。
これを外すに至った理由は2つです。
- より良い書き換えを邪魔する: 「このモダナイザーが
fmt.Appendfを挿入する箇所の半分以上は、もう少し広い文脈を見ればずっと良い変更がある」。代表例がw.Write([]byte(fmt.Sprintf(...)))で、機械的にw.Write(fmt.Appendf(nil, ...))にされますが、本来あるべき姿はfmt.Fprintf(w, ...)です - 残るケースでも改善が微妙: 「コードがわずかに分かりにくくなる(なぜ何かを”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 waitgroup と go tool fix help waitgroup を叩くと、実際に全く違う説明が返ってきて確かに紛らわしいです。早めに修正できて良かったと思います。
さいごに
Go 1.27の go fix アップデートを紹介しました。1.27でも安全な書き換えしか行ないポリシーで開発されているため、安心して使えそうです。素敵!
こうしたモダン化は、今ではAIエージェントがコードを書くついでにやってくれる場面も多いですが、ツール側で 決定的に 行ってくれることの価値も依然として重要です。世の中のGoコードがモダンな書き方へ収束していけば、それを学習するモデル側の精度も上がり、AIがより良いコードを書いてくれる、という好循環が回ります。比較的、地味なコマンドですが、長期的なGo言語の発展にも寄与してくれると思います。
最後まで読んでいただき、ありがとうございました!
- 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側の実測値です。 ↩