はじめに
Go 1.26からは、 go fix コマンドによって古い書き方のコードを自動的にモダンな書き方に修正できるようになりました。この処理は golang.org/x/tools/go/analysis/passes/modernizeパッケージに定義されている種々の静的解析器 (Analyzer) によって実現されています。
これらのAnalyzerがどのようなコードを修正してくれるのか知ることで、我々もモダンなGoの書き方を速習できるのではないでしょうか。……ということで、この記事ではmodernizeパッケージが提供するAnalyzerに対応するモダンなGoの書き方をまとめてみました。
- はじめに
- Analyzerに対応するモダンな書き方の一覧
- any (Go 1.18)
- appendclipped (Go 1.22)
- bloop (Go 1.24)
- errorsastype (Go 1.26)
- fmtappendf (Go 1.19)
- forvar (Go 1.22)
- mapsloop (Go 1.23)
- minmax (Go 1.21)
- newexpr (Go 1.26)
- omitzero (Go 1.24)
- plusbuild (Go 1.17)
- rangeint (Go 1.22)
- reflecttypefor (Go 1.22)
- slicescontains (Go 1.21)
- slicesdelete (Go 1.21)
- slicessort (Go 1.21)
- stditerators (Go 1.23)
- stringscut (Go 1.18)
- stringscutprefix (Go 1.20)
- stringsseq (Go 1.24)
- stringsbuilder (Go 1.10)
- testingcontext (Go 1.24)
- unsafefuncs (Go 1.17)
- waitgroup (Go 1.25)
- おわりに
- 参考
Analyzerに対応するモダンな書き方の一覧
any (Go 1.18)
Go 1.18が出るまでは、任意の型が満たすinterfaceとして interface{} 型が使われていました。
Go 1.18以降は any 型が使えるようになりました。内部的には interface{} 型と同じですが、こちらのほうが短く書けるし、任意の型に当てはまることが名前から明確になります。
appendclipped (Go 1.22)
Go 1.22以前は、一度に複数のスライスを結合するときは、組み込みの append 関数をネストさせつつ、結合するスライスの要素を引数として展開する……のような書き方をする必要がありました。
// before append(append(s, s1...), s2...)
Go 1.22で導入された slices.Concat 関数を使うことで、以下のようにスライスの結合をシンプルに記述できます。
// after
slices.Concat(s, s1, s2)
bloop (Go 1.24)
Go 1.24までは、ベンチマークコードでは b.N の値に対するforループを書く必要がありました。
// before for i := 0; i < b.N; i++ { // benchmark code } // あるいはrange over intで for range b.N { // benchmark code }
Go 1.24以降では B.Loop メソッドを使って以下のように書くのがよいでしょう。b.N の値に対するforループよりも堅牢で効率的になっているようです。
// after for b.Loop() { // benchmark code }
errorsastype (Go 1.26)
errorの具体的な型に応じて処理を分岐したい場合、Go 1.26以前は errors.As 関数を使っていました。第2引数に具体的なerror型の値へのポインタを渡す必要があるなど、微妙に使い方にコツが必要でした。
// before var myErr *MyError if errors.As(err, &myErr) { // myErr != nil }
Go 1.26からは errors.AsType 関数を使って分かりやすく書けるようになりました。ジェネリクスの型引数を使って欲しいerrorの型を明示するだけでよくなります。変数宣言をif文に押し込められるので、変数のスコープも明確になるでしょう。
// after if myErr, ok := errors.AsType[*MyError](err); ok { // myErr != nil }
fmtappendf (Go 1.19)
fmt.Sprintf で整形した文字列をバイト列に変換する場合、以下のように整形結果の文字列をバイト列に変換していたと思います。
// before []byte(fmt.Sprintf("%s", x))
Go 1.19で導入された fmt.Appendf 関数を使うことで以下のように書けます。こうすることでアロケーションを減らすことができ、メモリ効率の向上が期待できます。
// after fmt.Appendf(nil, "%s", x)
(2026/3/2 11:20 追記) ただし、[]byte(fmt.Sprintf("%s", x)) を機械的に fmt.Appendf に置き換えるよりも効率的な書き方があることにも注意しましょう。たとえば、以下のように整形した文字列をバイト列として io.Writer に書き込むコードを考えてみます。
// before w.Write([]byte(fmt.Sprintf("%s", x)))
この場合は以下のように fmt.Fprintf 関数を使うほうが適切でしょう。間にバイト列を挟むことなく、文字列を整形しつつバイト列として書き込めます。
// after // w.Write(fmt.Appendf(nil, "%s", x)) ではない fmt.Fprintf(w, "%s", x)
また、バイト列に対して fmt.Sprintf 関数で整形したバイト列を結合したい場合は、fmt.Appenf 関数の第1引数に nil ではなく結合元のバイト列を渡すほうが効率的です。
// before b = append(b, []byte(fmt.Sprintf("%s", x))...)
// after // b = append(b, fmt.Appendf(nil, "%s", x)...) ではない b = fmt.Appendf(b, "%s", x)
Analyzer fmtappendfについては以下のようなissueが立てられており、Go 1.27に向けて何らかの整理が行われるかもしれません。
(追記ここまで)
forvar (Go 1.22)
Go 1.22以前は、forループの中でgoroutineを起動した際にループ変数をコピーしないと意図した結果にならない、ということがしばしば起こっていました。
Go 1.22からは、そのような対応をしなくてもループ変数がコピーされるようになりました。
var wg sync.WaitGroup for i := range 10 { i := i // Go 1.22以降ではこの変数定義が不要 wg.Go(func() { fmt.Println(i) }) } wg.Wait()
ループ変数まわりの経緯や細かな挙動については、以下の記事やそこからリンクされている発表資料によくまとまっているので、そちらもあわせてご覧ください。
mapsloop (Go 1.23)
Go 1.23以前ではmapに対するforループで記述されていた処理が、Go 1.23で導入されたmapsパッケージの関数を使うことで簡潔に書けるようになります。
たとえば、以下のコードは maps.Copy(y, x) と等価です。
y := make(map[string]int) for k, v := range x { y[k] = v }
関連する話題として、実験的パッケージのgolang.org/x/exp/mapsを使っているのであれば、いい機会なので標準のmapsパッケージに移行するのがよいでしょう。一部の関数がイテレータを返すようになっています。
minmax (Go 1.21)
Goで最小値・最大値の計算といえば条件分岐を書くしかない、というイメージがあったと思いますが、Go 1.21で導入された min max 組み込み関数を使うことでシンプルに書けるようになりました。
// before var x int if a < b { x = a } else { x = b }
// after
x := min(a, b)
newexpr (Go 1.26)
Go 1.26以前では、structリテラルに対する参照を取ることは簡単にできましたが、整数・文字列リテラルなどに対して参照を取るためには変数を経由する必要がありました。以下のようなヘルパー関数を導入することで記述を簡略化したことがある人もいると思います。
func Ptr[T](t T) *T { return &t }
aws-sdk-go-v2の String 関数やgithub.com/samber/loの ToPtr 関数など、種々のライブラリが同様の機能を持つ関数を提供していました。
Go 1.26から new 組み込み関数に型だけではなく式を渡せるようになり、整数・文字列リテラルの参照を簡単に作れるようになりました。
new(1) new("string")
omitzero (Go 1.24)
structのフィールドがゼロ値なら json.Marshal 関数でmarshalするときにフィールドを出力したくない、という場合、従来はomitemptyを使っていました。
しかしながら、omitemptyを指定したときに省略されるフィールドの条件は、Goのゼロ値とは厳密には異なります。とくに、structや time.Time 型のゼロ値はomitemptyを指定していてもフィールドが出力されるため、直感的でない挙動になることがありました。
type Data struct { Time time.Time `json:",omitempty"` } data, _ := json.Marshal(&Data{}) fmt.Println(string(data)) // {"Time":"0001-01-01T00:00:00Z"}
Go 1.24から、挙動がより明確なomitzeroが導入されました。omitzeroがフィールドを省略する条件は以下のいずれかを満たす場合です。
- フィールドの型が
IsZero() boolというシグネチャのメソッドを持っており、それがtrueを返す - フィールドの値がゼロ値である
type Data struct { Time time.Time `json:",omitzero"` } data, _ := json.Marshal(&Data{}) fmt.Println(string(data)) // {}
plusbuild (Go 1.17)
Go 1.17までは、goファイルがビルド対象に含まれる条件 (OSのアーキテクチャやGoのバージョンなど) を指定する際に // +build 形式のコメントを使っていました。// +build 形式のコメントは挙動が難しいという課題を抱えていました。詳しくは以下の記事を参照してください。
Go 1.17以降では //go:build 形式のコメントが使えるようになりました。論理積や論理和などが直感的に書けるようになり、驚きの小さい挙動に修正されています。
rangeint (Go 1.22)
0からN-1までの整数に対してループを書く場合、Go 1.22まではC言語などでも馴染みの深い書き方でforループを書いていました。
for i := 0; i < N; i++ { }
Go 1.22でrange over intが導入され、このようなforループを短く書けるようになりました。
for i := range N { }
ループのインデックスが不要な場合は更に短く記述できます。
for range N { }
reflecttypefor (Go 1.22)
Goのリフレクションにおいて、interface型に対応する reflect.Type 型の値を取得するには、少し特殊なイディオムを使う必要がありました。interface型以外でも、ゼロ値などなんらかの具体的な値を指定して型情報を取得する必要がありました。
t := reflect.TypeOf((*error)(nil)).Elem() // t は error 型に対応する reflect.Type
Go 1.22ではreflectパッケージに TypeFor 関数が追加され、ジェネリクスの型引数として reflect.Type を得る型を渡せるようになりました。
t := reflect.TypeFor[error]() // t は error 型に対応する reflect.Type
slicescontains (Go 1.21)
スライスに指定した条件を満たす要素が存在するか確かめる際に、従来はスライスに対してforループを回して探索していたと思います。
// before func HasEven(xs []int) bool { for _, x := range xs { if x%2 == 0 { return true } } return false }
Go 1.21で導入された slices.Contains slices.ContainsFunc 関数によって、そのような処理が短く書けるようになりました。
// after func HasEven(xs []int) bool { return slices.ContainsFunc(xs, func(x int) bool { return x%2 == 0 }) }
実験的パッケージである golang.org/x/exp/slices を使っている場合は、いい機会なので標準のslicesパッケージを使うようにしましょう。
ほかにも、スライスに対する操作がいろいろslicesパッケージに定義されているので、forループを書く前に一度slicesパッケージに欲しい関数が定義されていないか確認してみるとよいと思います。
slicesdelete (Go 1.21)
スライスの特定範囲の要素を削除するために、以下のようなイディオムのコードを書いたことがある人もいるかもしれません。
s = append(s[:i], s[j:]...)
Go 1.21で導入された slices.Delete 関数を使うことで、以下のように書けます。slices.Delete 関数を使うことで削除された要素がゼロ初期化されるようになり、メモリリークを回避してくれます。
s = slices.Delete(s, i, j)
slicessort (Go 1.21)
Goでスライスをソートする際に sort.Slice 関数や、型ごとの sort.Ints sort.Strings のような関数を使っていたことがあるかもしれません。
Go 1.21ではslicesパッケージに Sort SortFunc SortStableFunc などの関数が追加されたので、こちらを使いましょう。
// 単純に比較してソートする場合 slices.Sort(xs) // ソートの条件を指定する場合 slices.SortFunc(xs, func(x, y int) int { // ... }) // 安定ソートにする場合 slices.SortFunc(xs, func(x, y int) int { // ... })
要素をソートした順に取り出すことが目的なのであれば、Go 1.23で導入された Sorted SortedFunc SortedStableFunc 関数などを使ってイテレータとして取り回すのもよいでしょう。
もはやsortパッケージのことは忘れてしまっても構わない、と自分は考えています。
stditerators (Go 1.23)
標準ライブラリが提供する型の中には、Len At 形式のメソッドを使ってforループで要素を走査するインタフェースを提供しているものがあります。
// before for i := 0; i < x.Len(); i++ { // 長さを取得する use(x.At(i)) // i番目の要素にアクセスする }
これらの型を使っている場合、Go 1.23で導入されたイテレータを使うことで以下のような形式で走査できるようになります。
// after for elem := range x.All() { use(elem) }
stditeratorsの実装で言及されているのはgo/typesとreflectパッケージの一部の型ですが、一般に要素を走査することだけが目的の場合はイテレータを経由するほうが効率がよいので、イテレータを返す版の関数・メソッドがあれば積極的に使っていきたいですね。
stringscut (Go 1.18)
Go 1.18以降では、文字列のうち、指定された文字列が最初に登場するよりも前の部分文字列が欲しいときには strings.Cut 関数が使えます。
if before, _, ok := strings.Cut(s, substr); ok { return before }
stringscutprefix (Go 1.20)
Go 1.20からは、文字列からprefix/suffixを取り除くときにはそれぞれ strings.CutPrefix strings.CutSuffix 関数が使えます。
if after, ok := strings.CutPrefix(s, prefix); ok { return after }
stringsseq (Go 1.24)
文字列を特定の文字列で分割したあとforループを回すとき、Go 1.24で導入された strings.SplitSeq 関数を使うとイテレータに対するループになって効率がよいです。
for part := range strings.SplitSeq(s, sep) { }
stringsbuilder (Go 1.10)
+= 演算子による文字列結合は都度アロケーションが発生して効率がよくないです。
// before s := "[" for x := range seq { s += x s += "." } s += "]"
Go 1.10で導入された strings.Builder などの型を使うと効率よく文字列を生成できます。
// after var s strings.Builder s.WriteString("[") for x := range seq { s.WriteString(x) s.WriteString(".") } s.WriteString("]")
testingcontext (Go 1.24)
従来は、テストの終了時にcancelされる context.Context は以下のように context.WithCancel 関数を使って生成するのが一般的でした。
// before func TestXxx(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) }
Go 1.24からは、テストの終了時にcancelされる context.Context が欲しい場合は t.Context() メソッドを呼ぶようにしましょう。
// after func TestXxx(t *testing.T) { ctx := t.Context() }
unsafefuncs (Go 1.17)
Go 1.17で導入された unsafe.Add 関数を使うことでポインタ演算を短く書けます。もしかすると一般的なアプリケーションを書いている範疇だとお世話になることはないかも?
// before unsafe.Pointer(uintptr(ptr) + uintptr(n))
// after
unsafe.Add(ptr, n)
waitgroup (Go 1.25)
従来、sync.WaitGroup を使って複数のgoroutineの起動を待機できるようにするには以下のようなコードを書いていました。wg.Add(1) を新しいgoroutineの起動直前に呼ぶ、wg.Done() が新しいgoroutineの終了時に必ず呼ばれるようにする、など気にしなければならない点がいくつかありました。
// before wg.Add(1) go func() { defer wg.Done() // ... }()
Go 1.25で導入された Go メソッドを使うことで以下のようにシンプルに書けます。golang.org/x/sync/errgroup や github.com/sourcegraph/conc などのライブラリが同様のインタフェースを実装しているので、そちらを触ったことのある方には馴染みの深い形だと思います。
// after wg.Go(func(){ // ... })
おわりに
この記事は kamakura.go #8 のLTの副産物でした。LTのためにmodernizeパッケージが提供するAnalyzerや、それが修正する古いコード・モダンなコードの差分をまとめたので、加筆修正しつつブログ記事として検索しやすい形にまとめました。
最新の・よりメモリ効率のよいコードの書き方が世の中に浸透することで、我々の生産性もきっと向上するでしょう。短く・簡潔に書けることは正義ですね。
参考
- The Go Programming Language Specification - The Go Programming Language
- Using go fix to modernize Go code - The Go Programming Language
- x/tools/go/analysis/passes/modernize: remove the fmtappendf modernizer · Issue #77581 · golang/go · GitHub
- pkg.go.dev
- Goのビルドタグの書き方が// +buildから//go:buildに変わった理由
- Go 1.26で go fix が面白くなった | フューチャー技術ブログ
- Go Conference 2024 に登壇しました! · からまるのブログ