「modernize」パッケージとは?
「modernize」パッケージは、Go のツール群の中でも特に注目すべき解析器(アナライザー)です。gopls(Go 言語サーバー)に統合されており、既存のコードを最新の言語機能や標準ライブラリの改善点に沿って自動的にリファクタリングするための提案を行ってくれます。たとえば、古い if/else 構文による条件分岐を、Go 1.21 で追加された組み込みの min/max 関数に置き換えるなど、コードをよりシンプルで読みやすい形に更新できます。
さらに、modernize パッケージには、提案された変更を一括で適用できるコマンドラインツールも用意されています。たとえば、以下のコマンドを実行することで、テスト対象のコードに対してすべての現代化修正を一括で適用できます。
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...
このコマンドは、指定したパッケージやディレクトリ(上記例ではカレントディレクトリ以下のテストコード)に対して、modernize パッケージが提案するリファクタリングを実施します。もしツールが修正の競合を警告する場合は、複数回実行することで最終的にすべての修正が適用されることがあります。ただし、このコマンドは公式なインターフェースではなく、将来的に変更される可能性があるため、利用時には注意が必要です。
この記事では、modernize パッケージがどのようにしてコードを現代化し、リファクタリングの流れをスムーズにするかを、具体的なコード例を交えて解説しています。各サンプルを通じて、従来の記述方法と最新の記法との違いを理解し、あなた自身のプロジェクトにおける改善のヒントとして活用していただければと思います。これをきっかけに、古くなったコードを最新の Go 機能に合わせてアップデートし、保守性と可読性の向上を実現しましょう。
replacing an if/else conditional assignment by a call to the built-in min or max functions added in go1.21;
従来のコード例(if/else を使った最小値の選択)
package main
import "fmt"
func main() {
a := 10
b := 20
var minVal int
if a < b {
minVal = a
} else {
minVal = b
}
fmt.Println("Minimum:", minVal)
}
現在のコード例(組み込みの min 関数を利用)
package main
import "fmt"
func main() {
a := 10
b := 20
// Go 1.21 以降では、min 関数が組み込みとして利用可能
minVal := min(a, b)
fmt.Println("Minimum:", minVal)
}
同様に、最大値を選ぶ場合もif/else
から組み込みのmax
関数に置き換えることができます。
従来のコード例(if/else を使った最大値の選択)
package main
import "fmt"
func main() {
a := 10
b := 20
var maxVal int
if a > b {
maxVal = a
} else {
maxVal = b
}
fmt.Println("Maximum:", maxVal)
}
現在のコード例(組み込みの max 関数を利用)
package main
import "fmt"
func main() {
a := 10
b := 20
// Go 1.21 以降では、max 関数が組み込みとして利用可能
maxVal := max(a, b)
fmt.Println("Maximum:", maxVal)
}
このように、if/else
で条件分岐して値を選択するコードを、min
やmax
の呼び出しに置き換えることで、コードがより簡潔になり可読性が向上します
replacing sort.Slice(x, func(i, j int) bool) { return s[i] < s[j] } by a call to slices.Sort(s), added in go1.21;
従来のコード例
従来はsort.Slice
を使って、無名関数で比較処理を記述していました。
package main
import (
"fmt"
"sort"
)
func main() {
s := []int{5, 3, 8, 1, 2}
// sort.Slice を使ってスライスをソート
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
fmt.Println("Sorted slice:", s)
}
現在のコード例
Go 1.21 以降では、標準ライブラリのslices
パッケージにより、より簡潔にソートが行えます。
package main
import (
"fmt"
"slices"
)
func main() {
s := []int{5, 3, 8, 1, 2}
// slices.Sort を使ってスライスをソート
slices.Sort(s)
fmt.Println("Sorted slice:", s)
}
このように、無名関数を定義する必要がなくなり、コードがシンプルかつ読みやすくなります
replacing interface{} by the 'any' type added in go1.18;
従来のコード例
package main
import "fmt"
// interface{} を使って任意の型の値を受け取る関数
func printValue(v interface{}) {
fmt.Println(v)
}
func main() {
printValue(42)
printValue("Hello, World!")
}
現在のコード例
package main
import "fmt"
// any は interface{} のエイリアスとして定義されており、より意味が明確
func printValue(v any) {
fmt.Println(v)
}
func main() {
printValue(42)
printValue("Hello, World!")
}
このように、interface{}
を直接使う代わりにany
を用いることで、コードがより読みやすく、意図が明確になります
replacing append([]T(nil), s...) by slices.Clone(s) or slices.Concat(s), added in go1.21;
従来のコード例
従来は、append([]T(nil), s...)
を使ってスライスのコピーを作成していました。
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
// nil スライスに対して append を行い、コピーを作成
sClone := append([]int(nil), s...)
fmt.Println("Cloned slice:", sClone)
}
現在のコード例
Go 1.21 以降は、slices.Clone
を使うことでより簡潔にコピーが作成できます。
package main
import (
"fmt"
"slices"
)
func main() {
s := []int{1, 2, 3, 4}
// slices.Clone を使ってスライスをコピー
sClone := slices.Clone(s)
fmt.Println("Cloned slice:", sClone)
}
従来のコード例
複数のスライスを連結する場合、従来は次のようにしていました。
package main
import (
"fmt"
)
func main() {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
// まず a のコピーを作成し、b を連結する
concat := append(append([]int(nil), a...), b...)
fmt.Println("Concatenated slice:", concat)
}
現在のコード例
Go 1.21 以降は、slices.Concat
を使うことで、複数のスライスをシンプルに連結できます。
package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
// slices.Concat を使ってスライスを連結
concat := slices.Concat(a, b)
fmt.Println("Concatenated slice:", concat)
}
これらの改善により、コードがより読みやすく、意図が明確になります
replacing a loop around an m[k]=v map update by a call to one of the Collect, Copy, Clone, or Insert functions from the maps package, added in go1.21;
従来のコード例
従来は、別のマップのキー・値を更新するためにループを用いていました。
package main
import (
"fmt"
)
func main() {
// 既存のマップ
m := map[string]int{"a": 1, "b": 2}
// 新しいキー・値のセット
newEntries := map[string]int{"c": 3, "d": 4}
// ループで各エントリを更新
for k, v := range newEntries {
m[k] = v
}
fmt.Println("Updated map:", m)
}
現在のコード例
Go 1.21 以降は、maps.Copy
を使うことでループを使わずにマップを更新できます。
package main
import (
"fmt"
"maps"
)
func main() {
// 既存のマップ
m := map[string]int{"a": 1, "b": 2}
// 新しいキー・値のセット
newEntries := map[string]int{"c": 3, "d": 4}
// maps.Copy を使って newEntries の内容を m にコピー(更新)する
maps.Copy(m, newEntries)
fmt.Println("Updated map:", m)
}
このように、ループを使わずにmaps.Copy
を利用することで、コードが簡潔になり意図が明確になります
replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...), added in go1.19;
従来のコード例
従来は、fmt.Sprintf
で文字列を生成し、それを[]byte
に変換していました。
package main
import (
"fmt"
)
func main() {
// fmt.Sprintf で文字列生成し、[]byte に変換
b := []byte(fmt.Sprintf("Hello, %s!", "World"))
fmt.Println(string(b))
}
現在のコード例
Go 1.19 以降は、fmt.Appendf
を使って直接バッファに書き込むことで、余計な変換処理を省けます。
package main
import (
"fmt"
)
func main() {
// fmt.Appendf を使って nil バッファに直接書き込み
b := fmt.Appendf(nil, "Hello, %s!", "World")
fmt.Println(string(b))
}
このように、fmt.Appendf を利用することで、不要な中間の文字列生成や []byte 変換を避け、より効率的なコードになります
replacing uses of context.WithCancel in tests with t.Context, added in go1.24;
従来のコード例
テスト内で独自にcontext.WithCancel
を呼び出している例です。
package mypkg_test
import (
"context"
"testing"
"time"
)
func TestSomething(t *testing.T) {
// テスト用の context を自前で生成している
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 何らかの処理(例としてタイムアウト待ち)
select {
case <-time.After(100 * time.Millisecond):
// 正常終了
case <-ctx.Done():
t.Fatal("context was cancelled")
}
}
現在のコード例
Go 1.24 からは、t.Context()
を使うことでテストのライフサイクルに合わせた context を利用できます。
package mypkg_test
import (
"testing"
"time"
)
func TestSomething(t *testing.T) {
// t.Context() を使ってテスト用の context を取得する
ctx := t.Context()
// 何らかの処理(例としてタイムアウト待ち)
select {
case <-time.After(100 * time.Millisecond):
// 正常終了
case <-ctx.Done():
t.Fatal("test context was cancelled")
}
}
このように、t.Context() を使うと、テスト終了時に自動でキャンセルされる context を利用できるため、明示的に cancel 関数を管理する必要がなくなり、コードがシンプルになります
replacing omitempty by omitzero on structs, added in go1.24;
従来のコード例
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Timeout int `json:"timeout"`
}
type Options struct {
Config Config `json:"config,omitempty"`
}
func main() {
opts := Options{} // Config はゼロ値
data, _ := json.Marshal(opts)
fmt.Println(string(data))
// 出力例: {"config":{}} ← ゼロ値にも関わらず空のオブジェクトとして出力される
}
現在のコード例
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Timeout int `json:"timeout"`
}
type Options struct {
Config Config `json:"config,omitzero"`
}
func main() {
opts := Options{} // Config はゼロ値
data, _ := json.Marshal(opts)
fmt.Println(string(data))
// 出力例: {} ← ゼロ値ならフィールドが省略される
}
このように、omitzero
を利用することで、構造体フィールドがゼロ値の場合にそのフィールド自体を省略でき、より意図したJSON出力が得られます
replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1), added in go1.21
従来のコード例
従来は、以下のようにスライスから要素を削除するために、append
を使って 2 つのスライスを連結していました。
package main
import (
"fmt"
)
func removeIndex(s []int, i int) []int {
// s[i] を削除するために、前半と後半のスライスを連結
return append(s[:i], s[i+1:]...)
}
func main() {
s := []int{10, 20, 30, 40, 50}
s = removeIndex(s, 2) // 30 を削除
fmt.Println(s) // 出力: [10 20 40 50]
}
現在のコード例
Go 1.21 以降は、slices.Delete
を使うことで、指定範囲の要素を削除する処理がシンプルに記述できます。
package main
import (
"fmt"
"slices"
)
func removeIndex(s []int, i int) []int {
// slices.Delete を使って s[i] のみ削除する(削除範囲は [i, i+1))
return slices.Delete(s, i, i+1)
}
func main() {
s := []int{10, 20, 30, 40, 50}
s = removeIndex(s, 2) // 30 を削除
fmt.Println(s) // 出力: [10 20 40 50]
}
このように、slices.Delete
を利用することで、コードがより直感的で読みやすくなります
replacing a 3-clause for i := 0; i < n; i++ {} loop by for i := range n {}, added in go1.22;
従来のコード
従来は、3 句の for ループでカウンタを初期化し、条件判定、インクリメントを明示していました。
package main
import "fmt"
func main() {
n := 5
for i := 0; i < n; i++ {
fmt.Println(i)
}
}
現在のコード例
Go 1.22 以降は、for i := range n {}
のように、整数nをrangeの対象として使うことで、カウンタの初期化や条件判定、インクリメントを省略できます(シンタックスシュガー)。
package main
import "fmt"
func main() {
n := 5
for i := range n {
fmt.Println(i)
}
}
この新しい構文により、単純なカウントループがより簡潔に書けるようになり、コードの可読性が向上します
replacing Split in "for range strings.Split(...)" by go1.24's more efficient SplitSeq;
従来のコード例
従来は、strings.Split
を用いて全ての要素を生成し、その結果をfor range
で処理していました。
package main
import (
"fmt"
"strings"
)
func main() {
s := "apple,banana,cherry"
// まず全要素をスライスに分割してから反復処理する
parts := strings.Split(s, ",")
for _, fruit := range parts {
fmt.Println(fruit)
}
}
現在のコード例
Go 1.24 以降は、strings.SplitSeq
を使うことで、メモリアロケーションを抑えた効率的な分割処理が可能です。
package main
import (
"fmt"
"strings"
)
func main() {
s := "apple,banana,cherry"
// SplitSeq を使って、効率的に文字列を分割しながら反復処理する
for _, fruit := range strings.SplitSeq(s, ",") {
fmt.Println(fruit)
}
}
このように、SplitSeq
を利用することで、従来のstrings.Split
を用いた方法に比べて、不要なメモリアロケーションを削減し、パフォーマンスの向上が期待できます
最後に
今回ご紹介した各リファクタリング手法は、Go の新機能を活用してコードベースをシンプルに保つための一助となるはずです。modernize パッケージが提案する改善案は、自動化ツールとしてだけでなく、自分自身のコードレビューやリファクタリングの方向性を定めるうえでも非常に有用です。
今後も、Go のアップデートに合わせた最適な記述方法を意識し、定期的なリファクタリングを行うことで、コードの健全性や拡張性を維持していきましょう。