ブログに戻る

Go 1.26 の `go fix` に含まれる全モダナイザー

Go 1.26 で go fix がアナライザーフレームワーク上に作り直されました。同梱されているすべてのモダナイザーを、言語と標準ライブラリにまたがる 26 個の変更前後の差分で紹介します。

Go 1.26 の `go fix` に含まれる全モダナイザー

Go 1.26 では、go fix が go vet と同じフレームワーク上に作り直されました。新しいコマンドには、Go チームと x/tools の作者たちが作ってきたモダナイザーがすべて同梱されています。それぞれが、古いパターンを検出して現在の Go の書き方に書き換える小さなアナライザーです。

1 つのコマンドですべてを実行できます。

example.bashbash
go fix ./...

クリーンなブランチで実行し、差分を確認して、テストを走らせてからコミットします。ファイルを書き換えずに結果だけ確認したい場合は go fix -diff ./... を使います。

以下では、1.26 に同梱されているすべてのモダナイザーを分野ごとにまとめています。各項目には、書き換え先の API が導入された Go のバージョンと、変更前と変更後のコード例を載せています。最初から読むだけでなく、ざっと眺めたり Ctrl+F で探したりする使い方も想定しています。

目次


言語と組み込み関数

ここには 7 つのモダナイザーがあります。大半は言語機能(any、新しい range n 形式、1.22 でのループ変数の変更、new(expr) の拡張)に加えて、複数のバージョンにまたがるいくつかの整理を扱います。

any (Go 1.18): interface{} を any に置き換える

any は単なるエイリアスですが、関数シグネチャや型パラメータの中ではこちらのほうが読みやすくなります。

変更前:

example.gogo
func log(args ...interface{}) {
    fmt.Println(args...)
}

var cache map[string]interface{}

変更後:

example.gogo
func log(args ...any) {
    fmt.Println(args...)
}

var cache map[string]any

minmax (Go 1.21): if/else によるクランプ処理を min と max に置き換える

組み込み関数の min と max は可変長引数を取り、順序付け可能な任意の型に使えます。

変更前:

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

変更後:

example.gogo
x := max(0, min(100, f()))

rangeint (Go 1.22): 整数の for ループを for i := range n にする

スライスや配列を伴わない単純なカウント用のループでは、整数を直接 range で回せます。

変更前:

example.gogo
for i := 0; i < n; i++ {
    fmt.Println(i)
}

変更後:

example.gogo
for i := range n {
    fmt.Println(i)
}

forvar (Go 1.22): x := x によるキャプチャの小技を削除する

1.22 より前は、ループ変数がすべての反復で共有されていました。そのため、ループ変数をキャプチャするクロージャや goroutine では、ループ本体の中で x := x と書く必要がありました。1.22 からは反復ごとに別の変数が作られるので、このシャドウイングは不要になりました。

変更前:

example.gogo
for _, x := range items {
    x := x // pre-1.22 capture trick
    go func() { use(x) }()
}

変更後:

example.gogo
for _, x := range items {
    go func() { use(x) }()
}

古いモジュールでは変数が共有される挙動のままなので、このアナライザーは go.mod で go 1.22 以降が宣言されている場合にのみ動作します。

newexpr (Go 1.26): new(expr) で値へのポインタを作る

1.26 では組み込み関数 new が拡張され、型だけでなく式も受け取れるようになりました。new(s) は s のコピーを指す *string を返します。これで、よくある strPtr/intPtr/boolPtr のようなヘルパー関数は不要になります。

変更前:

example.gogo
func strPtr(s string) *string {
    p := s
    return &p
}

cfg.Name = strPtr("LevelUpGo")

変更後:

example.gogo
cfg.Name = new("LevelUpGo")

他の呼び出し元があったり公開 API だったりして、まだ削除できないヘルパーがある場合は、後述の inline を参照してください。段階的に移行するためのディレクティブを紹介しています。

inline (ディレクティブ): //go:fix inline が付いた関数と定数をインライン化する

ラッパー関数にこのディレクティブを付けると、go fix がすべての呼び出し箇所を内側の式に書き換えます。私は名前の変更や薄いラッパーの廃止に便利だと感じています。社内のコードベースに独自のモダナイザーを展開するのにも使えます。

変更前:

example.gogo
//go:fix inline
func Square(x int) int { return Pow(x, 2) }

a := Square(5)
b := Square(10)

変更後:

example.gogo
//go:fix inline
func Square(x int) int { return Pow(x, 2) }

a := Pow(5, 2)
b := Pow(10, 2)

同じディレクティブは定数にも使えるので、名前が変わった列挙値への移行にも役立ちます。

plusbuild (Go 1.17): // +build を //go:build に置き換える

古いビルドタグの構文は 1.17 から非推奨になっています。新しいディレクティブでは、通常の Go の論理式を使います。

変更前:

example.gogo
// +build linux,amd64

package sys

変更後:

example.gogo
//go:build linux && amd64

package sys

スライス、マップ、イテレーション

このグループの 7 つのモダナイザーのうち 5 つは、手書きのループを slices パッケージや maps パッケージの呼び出しに置き換えます。残りの 2 つは reflect.TypeFor と JSON タグの omitzero を扱います。

slicessort (Go 1.21): sort.Slice を slices.Sort にする

順序付け可能な要素のスライスを昇順に並べるというよくあるケースでは、ジェネリックな slices.Sort を使えば比較用のクロージャが一切不要になります。

変更前:

example.gogo
sort.Slice(s, func(i, j int) bool {
    return s[i] < s[j]
})

変更後:

example.gogo
slices.Sort(s)

slicescontains (Go 1.21): 手書きの検索ループを slices.Contains にする

変更前:

example.gogo
func has(s []int, x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

変更後:

example.gogo
func has(s []int, x int) bool {
    return slices.Contains(s, x)
}

slicesbackward (Go 1.23): slices.Backward で逆順に反復する

slices.Backward は、スライスの末尾から先頭に向かってインデックスと値のペアを返すイテレータを返します。

変更前:

example.gogo
for i := len(s) - 1; i >= 0; i-- {
    use(i, s[i])
}

変更後:

example.gogo
for i, v := range slices.Backward(s) {
    use(i, v)
}

mapsloop (Go 1.21): コピーやクローン、挿入のループを maps の呼び出しにする

maps パッケージには map を扱うよくあるループごとに対応する関数があるので、関数名を見ればそのループが何をしていたのかが読み手に伝わります。

変更前:

example.gogo
func copyMap(src map[string]int) map[string]int {
    dst := make(map[string]int, len(src))
    for k, v := range src {
        dst[k] = v
    }
    return dst
}

変更後:

example.gogo
func copyMap(src map[string]int) map[string]int {
    dst := make(map[string]int, len(src))
    maps.Copy(dst, src)
    return dst
}

同じモダナイザーが、maps.Clone、maps.Equal、キーと値のペアから挿入するパターンも扱います。

stditerators (Go 1.23): range ループで標準ライブラリのイテレータ関数を使う

maps.Keys、maps.Values、slices.All、slices.Values などは、そのまま range で回せるイテレータを返すので、先に中間のスライスを組み立てる必要はありません。

変更前:

example.gogo
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    use(k)
}

変更後:

example.gogo
for k := range maps.Keys(m) {
    use(k)
}

reflecttypefor (Go 1.22): nil ポインタを使った reflect の小技をやめる

reflect.TypeFor[T]() は、静的な型の reflect.Type を型付きで取得するジェネリックな方法です。昔ながらの TypeOf((*T)(nil)).Elem() という回避策は、ジェネリクス登場以前のものです。

変更前:

example.gogo
t := reflect.TypeOf((*MyType)(nil)).Elem()

変更後:

example.gogo
t := reflect.TypeFor[MyType]()

omitzero (Go 1.24): 壊れやすい omitempty を omitzero に置き換える

omitempty は、値が Go の意味でゼロかどうかを見ます。そのため、フィールドを持つ struct である time.Time{} は「空」と見なされず、出力から省略されることがありません。omitzero は値に IsZero メソッドがあればそれを呼び出すので、time.Time のゼロ値も期待どおり出力から消えます。

アナライザーは、omitzero にしても安全なフィールドだけを書き換えます。文字列、スライス、ポインタは挙動が変わってしまうため、omitempty のまま残ります。

変更前:

example.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitempty"`
}

変更後:

example.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitzero"`
}

文字列とフォーマット

この 5 つは、strings や fmt の古いパターンを新しい API に置き換えます。アロケーションを 1 回減らせるものもあれば、単に読みやすくなるだけのものもあります。

fmtappendf (Go 1.19): []byte(fmt.Sprintf(...)) を fmt.Appendf にする

fmt.Appendf は、渡した []byte にフォーマット結果を直接書き込みます(新しく確保させたい場合は nil を渡します)。中間の文字列は作られません。

変更前:

example.gogo
b := []byte(fmt.Sprintf("%s=%d", key, val))

変更後:

example.gogo
b := fmt.Appendf(nil, "%s=%d", key, val)

stringscut (Go 1.18): strings.Index とスライス操作を strings.Cut にする

strings.Cut は、区切り文字より前の部分、後ろの部分、見つかったかどうかを示す bool を 1 回の呼び出しで返します。見つからなかった場合も明示的に扱えます。

変更前:

example.gogo
i := strings.Index(s, "=")
if i >= 0 {
    key, val := s[:i], s[i+1:]
    use(key, val)
}

変更後:

example.gogo
if key, val, ok := strings.Cut(s, "="); ok {
    use(key, val)
}

stringscutprefix (Go 1.20): HasPrefix と TrimPrefix の組み合わせを CutPrefix にする

CutPrefix はプレフィックスを取り除いた文字列と、見つかったかどうかを示す bool を返すので、プレフィックスのチェックは 1 回で済みます。

変更前:

example.gogo
if strings.HasPrefix(s, "go-") {
    rest := strings.TrimPrefix(s, "go-")
    handle(rest)
}

変更後:

example.gogo
if rest, ok := strings.CutPrefix(s, "go-"); ok {
    handle(rest)
}

同じモダナイザーが CutSuffix も扱います。

stringsseq (Go 1.24): range ループ内の Split と Fields を SplitSeq と FieldsSeq にする

SplitSeq と FieldsSeq は、スライスを確保する代わりにイテレータを返します。分割した要素を 1 回たどるだけなら、イテレータ版ではアロケーションがまったく発生しません。

変更前:

example.gogo
for _, line := range strings.Split(s, "\n") {
    handle(line)
}

変更後:

example.gogo
for line := range strings.SplitSeq(s, "\n") {
    handle(line)
}

アナライザーが書き換えるのは、スライスを 1 回だけ消費して捨てるケースに限られます。スライスを保存したり、インデックスでアクセスしたりしている場合は、元の Split のまま残ります。

stringsbuilder (Go 1.10): ループ内で二乗のコストがかかる文字列連結を strings.Builder にする

ループ内の s += p は反復のたびに再確保とコピーを行うため、処理量が O(n²) になります。strings.Builder はスライスと同じように容量を伸ばします。

変更前:

example.gogo
s := ""
for _, p := range parts {
    s += p
}
return s

変更後:

example.gogo
var sb strings.Builder
for _, p := range parts {
    sb.WriteString(p)
}
return sb.String()

このアナライザーは慎重に動きます。ループ内のすべての += ではなく、明らかに値を蓄積していくパターンでのみ動作します。ループで連結するのが 2、3 個の小さな定数だけなら、単純な書き方のままで問題ありません。


ネットワーク、エラー、リフレクション

このグループは 3 つです。net の昔からある落とし穴、新しいジェネリックな errors.AsType、より安全な unsafe のヘルパーを扱います。

hostport (Go 1.0): アドレス組み立ての Sprintf を net.JoinHostPort にする

IPv6 アドレスはそれ自体にコロンを含むため、host が IPv6 リテラルだと fmt.Sprintf("%s:%d", host, port) は壊れたアドレスを作ってしまいます。net.JoinHostPort は IPv6 のホストを角括弧で囲みます。host:port 形式の文字列を組み立てるときの標準的な方法です。

変更前:

example.gogo
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)

変更後:

example.gogo
addr := net.JoinHostPort(host, strconv.Itoa(port))
net.Dial("tcp", addr)

errorsastype (Go 1.26): errors.As をジェネリックな errors.AsType にする

1.26 で追加された errors.AsType[T] は、型付きのエラーと bool を返します。受け取り用の変数を宣言して、そのアドレスを渡す必要はありません。

変更前:

example.gogo
var myerr *MyErr
if errors.As(err, &myerr) {
    handle(myerr)
}

変更後:

example.gogo
if myerr, ok := errors.AsType[*MyErr](err); ok {
    handle(myerr)
}

unsafefuncs (Go 1.17): unsafe を使った演算を unsafe.Add と unsafe.Slice にする

1.17 で unsafe.Add と unsafe.Slice が追加されました。uintptr を使った面倒な変換を手で書かなくても、ポインタ演算や、任意のメモリ上へのスライスの構築ができます。

変更前(base は unsafe.Pointer):

example.gogo
p := unsafe.Pointer(uintptr(base) + uintptr(off))

変更後:

example.gogo
p := unsafe.Add(base, off)

同じモダナイザーは、unsafe.Pointer と reflect.SliceHeader を手作業で組み合わせるパターンも unsafe.Slice に書き換えます。


並行処理とテスト

最後の 4 つは、sync、atomic、testing パッケージを使ったコードを整理します。

waitgroupgo (Go 1.25): Add(1) と defer Done() を wg.Go にする

1.25 で追加された WaitGroup.Go は、Add、goroutine の起動、defer Done を 1 回の呼び出しにまとめます。

変更前:

example.gogo
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go!")
}()
wg.Wait()

変更後:

example.gogo
var wg sync.WaitGroup
wg.Go(func() {
    fmt.Println("go!")
})
wg.Wait()

atomictypes (Go 1.19): 関数形式の atomic 操作をメソッド形式にする

atomic.Int64、atomic.Uint64、atomic.Pointer[T] などの型は、値を自分自身で保持します。そのため、ポインタを受け渡す必要がなくなり、操作のサイズと変数の型が食い違うこともなくなります。

変更前:

example.gogo
var count int64
atomic.AddInt64(&count, 1)
n := atomic.LoadInt64(&count)

変更後:

example.gogo
var count atomic.Int64
count.Add(1)
n := count.Load()

testingcontext (Go 1.24): テスト内の手動の WithCancel を t.Context() にする

t.Context() は、テストの終了時に自動でキャンセルされる context を返します。defer cancel() を覚えておく必要はなく、context はサブテストにも引き継がれます。

変更前:

example.gogo
func TestThing(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    work(ctx)
}

変更後:

example.gogo
func TestThing(t *testing.T) {
    ctx := t.Context()
    work(ctx)
}

bloop (Go 1.24): for i := 0; i < b.N; i++ を for b.Loop() にする

b.Loop() は新しいベンチマーク用のループです。タイマーのリセットと停止も自動で行うので、ほとんどのベンチマークでは、セットアップの前後に手で書いていた b.ResetTimer() や b.StopTimer() の呼び出しが不要になります。

変更前:

example.gogo
func BenchmarkThing(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        work()
    }
}

変更後:

example.gogo
func BenchmarkThing(b *testing.B) {
    for b.Loop() {
        work()
    }
}

go fix を安全に実行する

go fix は実際のソースファイルを書き換えます。他の自動リファクタリングと同じように扱ってください。

安全に進める手順は次のとおりです。

  1. 他に進行中の変更がない、クリーンなブランチから始めます。
  2. まず go fix -diff ./... を実行します。このフラグを付けると、ファイルを書き換える代わりに unified diff を標準出力に表示します。ざっと目を通してください。
  3. go fix ./... を実行して変更を適用します。
  4. git diff を実行して、実際の変更内容を確認します。意外な書き換えがないか、特にあえて古いスタイルで書いたコードに注意してください。
  5. テスト、リンター、go build ./... を実行します。
  6. コミットします。

1 回の実行では足りないことが多い

ある修正を適用すると別の修正の余地が生まれることがありますが、アナライザーは 1 回のパスの中でその連鎖を追いかけません。

minmax の節で取り上げたクランプ処理で確認してみます。出発点は次のコードです。

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

1 回目の実行で、下限の処理が max に書き換えられます。

example.gogo
x := max(0, f())
if x > 100 {
    x = 100
}

2 回目の実行で、残った if と代入のパターンが検出され、書き換えが完了します。

example.gogo
x := min(100, max(0, f()))

2 回のパスが必要なのは、各モダナイザーが独立したアナライザーで、go fix を 1 回実行するごとにコードを 1 回しか見ないからです。構文木を走査し、知っているパターンを見つけて、その修正を出力します。

クランプの例では、1 回目のパスで < 0 のブロックが書き換えられます。その結果、> 100 のブロックの直前にあるのは、ただの x := f() ではなく max(...) の呼び出しになり、新たに minmax を適用できる形になります。ただし minmax はすでに 1 回のパスを終えているので、もう一度 go fix を実行するまでこの新しい形には気づきません。

同じことはアナライザー同士の間でも起こります。mapsloop による書き換えで、stditerators を適用できる箇所が生まれることがあります。イテレータのアナライザーが探している range maps.Keys(m) という形は、ループが書き換えられる前には存在しなかったからです。

対処法は、差分が空になるまで go fix ./... を再実行することです。ほとんどのコードベースは 2 回で済み、3 回目が必要になることはまれです。

その他の注意点

  • バージョンに依存するモダナイザー(forvar、rangeint、newexpr、errorsastype)は、go.mod で必要な最小バージョンが宣言されている場合にのみ動作します。期待した書き換えが行われない場合は、go ディレクティブを確認してください。
  • 自分のコードベースに固有の移行には、//go:fix inline ディレクティブを使います。ラッパーや定数にタグを付けて go fix を実行すれば、すべての呼び出し箇所がその場で更新されます。
  • アナライザーが見落とした形で書き換えによって挙動が変わった場合、それはアナライザーのバグなので、報告する価値があります。Go チームはこうしたバグにすばやく対応してくれます。

さらに学ぶには

Go 1.26 リリース全体の解説(新しい暗号 API、Green Tea GC、goroutine リーク検出など)は、Go 1.26 の新機能ガイドをご覧ください。

一次情報としては、x/tools/go/analysis/passes/modernize のドキュメントでは各アナライザーが詳しく解説されており、go fix に関する Go ブログの記事では新しいフレームワークが説明されています。

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成