go fix のドキュメント見出しが “Update packages to use new APIs” から “Apply fixes suggested by static checkers” に変わりました。旧APIへの追従ツールから、古い書き方を現代化する静的解析ツールへ役割が移った形。Go 1.27rc2 で go tool fix help を叩いたところ、登録済み analyzer は 26個ありました。
go fix はAPI追従ツールから静的解析ツールになった
公式の go コマンドドキュメントは、go fix をこう説明しています。
Fix runs the Go fix tool (cmd/fix) on the named packages and applies suggested fixes.
2種類の analyzer を明確に分けている
go tool fix help の冒頭が、このツールの立ち位置を書いています。
fix examines Go source code and reports diagnostics for suspicious constructs or opportunities for improvement.
診断対象を suspicious construct(疑わしい構造)と opportunity for improvement(改善の余地)に分けているのが要点です。前者は Printf の引数と書式文字列が噛み合っていないケースで、こちらは go vet -vettool=PROGRAM の担当。後者は strings.Split(doc, "\n") のループを strings.SplitSeq のイテレータに置き換えて配列確保を省くようなケースで、go fix -fixtool=PROGRAM の担当。
ヘルプ本文の言い回しが両者の温度差を表しています。改善提案側は「report non-problems」つまり問題でないものを報告しうるが、「should carry fixes that may be safely applied」、安全に適用できる修正が付いている、と。バグ検出ではなく機械適用が前提の設計です。
go vet と同じ解析フレームワークに乗った
内部実装は golang.org/x/tools/go/analysis ベース。go vet や gopls が使っているものと同じフレームワークです。型情報とスコープを踏まえた書き換えになるので、正規表現置換とは精度が違います。gopls の modernize analyzer の実装がそのまま流用されている格好。エディタに出ていた提案を、コマンドラインから一括適用できるようになった形です。
Go 1.26 と 1.27 で登録 analyzer を実測比較
登録済みの一覧は go tool fix help の “Registered analyzers” セクションに出ます。
$ go version
go version go1.26.4 darwin/arm64
$ go tool fix help | sed -n '/Registered analyzers:/,/^By default/p' | grep -c '^ [a-z]'
22
$ ~/go/bin/go1.27rc2 version
go version go1.27rc2 darwin/arm64
$ ~/go/bin/go1.27rc2 tool fix help | sed -n '/Registered analyzers:/,/^By default/p' | grep -c '^ [a-z]'
26
22個から26個へ。内訳は次のとおりです。
| 変更 | analyzer | 書き換え内容 |
|---|---|---|
| 追加 | atomictypes | sync/atomic の基本型呼び出しを atomic 型のメソッドに |
| 追加 | embedlit | 複合リテラル内の埋め込みフィールド参照を簡潔に |
| 追加 | errorsastype | errors.As を errors.AsType[T] に |
| 追加 | slicesbackward | 逆順ループを slices.Backward に |
| 追加 | unsafefuncs | unsafe のポインタ演算を関数呼び出しに |
| 削除 | fmtappendf | []byte(fmt.Sprintf) → fmt.Appendf の提案が外れた |
| 改名 | waitgroup → waitgroupgo | 名前の曖昧さを避けるため |
リリースノートの数と実測の数が合わない
Go 1.27 のリリースノートおよびそれを紹介する記事は、新しい modernizer を atomictypes / embedlit / slicesbackward / unsafefuncs の4つとして挙げています。ただ実際に go tool fix help を並べて diff を取ると errorsastype も増えていて、追加は5つ。22 から1つ削除して5つ足すと26になり、実測値と一致します。
errorsastype が書き換える errors.AsType[T] は Go 1.26 で入った関数です。関数自体が1つ前のバージョンの機能なので、1.27 の新機能一覧からは漏れたと見られます。詳しい挙動はerrors.AsTypeでGoのエラーを型安全に取り出す—Go 1.26の新関数に書きました。
増えた modernizer が実際に書き換えるコード
次の main.go に go fix -diff をかけると、こう返ってきます。
var counter int64
func incr() int64 {
atomic.AddInt64(&counter, 1)
return atomic.LoadInt64(&counter)
}
func newUser() User {
return User{
Base: Base{ID: 1, Name: "moha"},
Email: "moha@example.com",
}
}
func printReverse(items []string) {
for i := len(items) - 1; i >= 0; i-- {
fmt.Println(items[i])
}
}
func fetchAll(urls []string) {
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(u)
}()
}
wg.Wait()
}
func classify(err error) string {
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
return pathErr.Path
}
return ""
}
実行結果:
$ go1.27rc2 fix -diff ./...
--- /tmp/gofixdemo/main.go (old)
+++ /tmp/gofixdemo/main.go (new)
@@ -4,6 +4,7 @@
"errors"
"fmt"
"io/fs"
+ "slices"
"sync"
"sync/atomic"
)
@@ -18,41 +19,38 @@
-var counter int64
+var counter atomic.Int64
func incr() int64 {
- atomic.AddInt64(&counter, 1)
- return atomic.LoadInt64(&counter)
+ counter.Add(1)
+ return counter.Load()
}
func newUser() User {
return User{
- Base: Base{ID: 1, Name: "moha"},
+ ID: 1, Name: "moha",
Email: "moha@example.com",
}
}
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)
}
}
func fetchAll(urls []string) {
var wg sync.WaitGroup
for _, u := range urls {
- wg.Add(1)
- go func() {
- defer wg.Done()
+ wg.Go(func() {
fmt.Println(u)
- }()
+ })
}
wg.Wait()
}
func classify(err error) string {
- var pathErr *fs.PathError
- if errors.As(err, &pathErr) {
+ if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
return pathErr.Path
}
return ""
1ファイルに4つの modernizer が同時に反応し、slices の import 追加まで面倒を見ています。
atomictypes は型でミスを防ぐ書き換え
atomic.AddInt64(&counter, 1) が counter.Add(1) になる変換。単なる記法の新しさではなく、変数の型が int64 から atomic.Int64 に変わる点が実質です。int64 のままだと counter++ のような非アトミックなアクセスをコンパイラが止められません。atomic.Int64 にすればその書き方自体が通らなくなります。加えて 32bit 環境での 64bit アライメント要求も型側が吸収します。
unsafefuncs は uintptr 経由の演算を消す
// before
func advance(p unsafe.Pointer, size uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + size)
}
実行結果:
$ go1.27rc2 fix -diff ./...
- return unsafe.Pointer(uintptr(p) + size)
+ return unsafe.Add(p, size)
unsafe.Pointer を uintptr に落として演算し戻す書き方は、途中で GC が走るとポインタを見失う余地があります。unsafe.Add なら演算がひとつの式に収まるため、その隙間が消えます。
waitgroup は waitgroupgo に改名された
個別に analyzer を指定するとき、-waitgroup は Go 1.27 で通りません。CI スクリプトやドキュメントに旧名をベタ書きしている場合は差し替えが要ります。書き換える内容は同じで、wg.Add(1) / go func() / defer wg.Done() の3点セットを wg.Go(...) にまとめるものです。
go.mod の go directive が適用範囲を決める
同じソースに go fix をかけても、書き換わる量がリポジトリによって変わります。手元のプロジェクトで走らせたら差分が2行しか出ず、上の検証用コードとの違いが分からず10分ほど原因を探しました。犯人は go.mod の go directive です。
directive を変えて同じファイルを測る
先ほどの main.go を固定したまま、go.mod の go 行だけ 1.24 から 1.27 まで動かして go fix -diff を実行しました。
$ for v in 1.24 1.25 1.26 1.27; do
sed -i '' "s/^go 1\..*/go $v/" go.mod
echo "go.mod: go $v"
go1.27rc2 fix -diff ./... | grep -cE '^\+'
done
実行結果:
go.mod: go 1.24
7
go.mod: go 1.25
9
go.mod: go 1.26
10
go.mod: go 1.27
11
ツール本体は同じ 1.27rc2 なのに、追加行数が 7 から 11 まで動きます。どの analyzer が黙るかを個別に測ると、境界がはっきりします。
| modernizer | 発火する最小の go directive | 置き換え先が入ったバージョン |
|---|---|---|
atomictypes | 1.24 以下でも発火 | atomic.Int64(Go 1.19) |
slicesbackward | 1.24 以下でも発火 | slices.Backward(Go 1.23) |
waitgroupgo | 1.25 | WaitGroup.Go(Go 1.25) |
errorsastype | 1.26 | errors.AsType(Go 1.26) |
embedlit | 1.27 | – |
閾値は「置き換え先の追加バージョン」と一致する
表の右2列がきれいに揃っています。waitgroupgo が go 1.25 未満で黙るのは、sync.WaitGroup.Go が Go 1.25 で追加されたメソッドだから。go 1.24 と宣言しているモジュールに wg.Go(...) を書き込めば、当然ビルドが通りません。errorsastype と errors.AsType の関係も同じです。
ツールが安全側に倒れています。書き換えた結果コンパイルできなくなる提案は最初から出さない設計。逆に言えば、go directive を上げないまま go fix を回しても、新しい modernizer の恩恵はゼロです。
先に directive を上げてから走らせる
$ go mod edit -go=1.27
$ go build ./... && go test ./...
$ go fix -diff ./...
順番が逆だと差分が出ないだけで、エラーにも警告にもなりません。「go fix を回したが何も出なかった」というときに疑うのは、モジュールの宣言バージョンが古いまま放置されているケースです。go mod edit -go= でバージョンを上げ、ビルドとテストが通ることを確認してから go fix に進むのが安全な順序になります。
削除された fmtappendf は 1.26 でだけ提案される
[]byte(fmt.Sprintf(...)) を含むコードを両バージョンで測ると、提案の有無が逆転します。
# Go 1.26.4
- return []byte(fmt.Sprintf("%s=%d", name, n))
+ return fmt.Appendf(nil, "%s=%d", name, n)
# Go 1.27rc2
(差分なし)
提案が外れた理由は stylistic concerns、つまり書き方の好みの問題という整理です。すでに fmt.Appendf に書き換え済みのコードが元に戻されるわけではないので、対応は不要。1.26 で一括適用した差分をレビュー中の場合だけ、根拠が消えている点に触れておくと話が早く済みます。
CI に組み込んで差分ゼロを保つ
-diff フラグの説明が、そのまま CI 用途を想定した書き方になっています。
instead of applying each fix, print the patch as a unified diff; exit with a non-zero status if the diff is not empty
終了コードを実測する
$ go1.27rc2 fix -diff ./... # 差分あり
--- /tmp/gofixdemo/main.go (old)
+++ /tmp/gofixdemo/main.go (new)
...
$ echo $?
1
$ go1.27rc2 fix ./... # 適用
$ go1.27rc2 build ./... && go1.27rc2 vet ./...
$ go1.27rc2 fix -diff ./... # 再実行
$ echo $?
0
適用後は build も vet も通り、再実行しても差分は出ません。冪等なので、CI で「差分が出たら落とす」というゲートを置けます。
GitHub Actions に置く
- uses: actions/setup-go@v6
with:
go-version: '1.27'
- name: go fix check
run: |
go fix -diff ./... | tee /tmp/fix.diff
test ! -s /tmp/fix.diff
パイプを挟むと終了コードがパイプ最後尾のものになるため、test ! -s でファイルサイズを見て判定しています。go fix -diff ./... を単体で走らせるなら終了コードだけで足ります。ツールのバージョン管理はGoのtoolディレクティブで開発ツールを管理する—tools.goからの移行手順の方式と揃えておくと、ローカルと CI の結果がずれません。
analyzer を個別に切り替える
全部を一度に当てる必要はありません。
$ go fix -diff -slicesbackward ./... # これだけ実行
$ go fix -diff -embedlit=false ./... # これ以外を実行
ヘルプの記述どおり、既定では全 analyzer が走ります。-NAME を1つでも付けると指定したものだけ、-NAME=false なら明示的に外したもの以外が走る、という切り替えです。-fixtool=prog で解析ツール自体を差し替えることもできます。
適用時に踏みやすい点
全 analyzer を一括適用して1コミットにまとめる
数万行のリポジトリで go fix ./... を無指定で回すと、数百ファイルに差分が散ります。レビューできない規模の変更になり、意味のある書き換えと機械的な置換が混ざって履歴も読めなくなる。
analyzer 単位で回してコミットを分けるほうが確実です。
$ for a in atomictypes slicesbackward waitgroupgo errorsastype; do
go fix -$a ./... && go test ./... && git commit -am "go fix: $a"
done
実行結果:
ok example/internal/queue 0.412s
[main 8f2a1c3] go fix: atomictypes
3 files changed, 11 insertions(+), 14 deletions(-)
...
テストが落ちた時点でループが止まり、どの analyzer が原因かが即座に分かります。
周辺ツールのバージョンを合わせる
書き換え後のコードを古い解析ツールが理解できず、CI やエディタだけが赤くなるケースがあります。Go 1.26 の newexpr が生成する new(値) パターンを staticcheck が SA4006(未使用の値)と誤検知する不具合が報告されており、staticcheck v0.7.0 で修正済み。gopls も古いままだと「xxx is not a type」といった存在しないエラーがエディタに並びます。go fix を導入する前に gopls、staticcheck、golangci-lint を上げておくのが順序として正しいです。
提案されないパターンが残る
ジェネリクスで書いた ToPtr[T any](v T) *T のようなヘルパーは、別パッケージの定数や独自型を引数にすると new(expr) への置換対象から外れることがあります。go fix は型情報を見るぶん保守的で、判断が付かないものは黙って残す。一括適用しても手作業の置換は残ります。
まとめ
go fixのドキュメント見出しは “Apply fixes suggested by static checkers”。旧API追従ツールではなく、gopls の modernize と同じ実装を使う静的解析ツール- 登録 analyzer は Go 1.26.4 で 22個、Go 1.27rc2 で 26個。
atomictypes/embedlit/errorsastype/slicesbackward/unsafefuncsが加わり、fmtappendfが外れ、waitgroupはwaitgroupgoに改名 - 発火するかどうかは
go.modのgodirective 次第。waitgroupgoは 1.25、errorsastypeは 1.26、embedlitは 1.27 以上が要る go fix -diffは差分ありで終了コード1、なしで0。適用後の再実行で差分が出ない冪等な動作なので CI ゲートに使える- 一括適用は履歴が読めなくなる。analyzer 単位で回してコミットを分け、staticcheck と gopls は先に上げておく
Go 1.27 は2026年8月時点で rc2。正式版で analyzer 構成が動く可能性はありますが、go tool fix help の “Registered analyzers” を見れば手元のツールチェーンが何を持っているかは常に確認できます。

