modern-go
Go のソースコードに、バージョンに応じたイディオム、API、言語機能を適用して最新化します。go fix と同様に動作し、Go チームの modernize 解析パスやコミュニティのベストプラクティスに基づいて選定された追加の変換も適用します。
使用方法
ユーザーが Go コードの最新化を依頼した場合に、このスキルを呼び出します。デフォルトではプロジェクト全体を最新化しますが、ユーザーが代わりにファイルやディレクトリを指定することもできます。
呼び出された場合:
go.mod の go ディレクティブから、プロジェクトの Go バージョンを検出します。
- 対象範囲内のすべての
.go ファイルを検索します(vendor/、.git/、testdata/ は除外)。
- 各ファイルに、プロジェクトの Go バージョン以下のすべての変換を、古いものから新しいものの順に適用します。
- すべての変換の適用後、変更内容とスキップした内容の概要を表示します。
ユーザーがファイルまたはディレクトリを指定した場合は、対象範囲をそのパスに限定します。
変換カタログ
各変換にはGo バージョンの条件があります。プロジェクトの go.mod のバージョンがそのバージョン以上の場合にのみ適用します。プロジェクトで宣言されているバージョンより新しいバージョンを必要とする変換は、決して適用しないでください。
Go 1.0+ — time.Since
| 変更前 | 変更後 |
|---|
time.Now().Sub(start) | time.Since(start) |
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
Go 1.8+ — time.Until
| 変更前 | 変更後 |
|---|
deadline.Sub(time.Now()) | time.Until(deadline) |
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)
Go 1.10+ — strings.Builder(ループ内の文字列連結)
| 変更前 | 変更後 |
|---|
ループ内の s += item | var b strings.Builder; b.WriteString(item) |
// before
s := ""
for _, item := range items {
s += item
}
// after
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
s := b.String()
+= による連結がループ内で行われる場合に限ります。
Go 1.13+ — errors.Is
| 変更前 | 変更後 |
|---|
err == io.EOF | errors.Is(err, io.EOF) |
// before
if err == io.EOF {
return
}
// after
if errors.Is(err, io.EOF) {
return
}
Go 1.17+ — //go:build 制約(plusbuild)
| 変更前 | 変更後 |
|---|
// +build linux + //go:build linux(両方が存在) | //go:build linux のみを残す |
// before
//go:build linux && amd64
// +build linux,amd64
package foo
// after
//go:build linux && amd64
package foo
plusbuild モダナイザーは、同等の //go:build 行が存在する場合、古くなった // +build 制約行を削除します(//go:build 構文は Go 1.17 で導入されました)。既存の //go:build が一致する場合に限って古い行を削除し、唯一の制約を削除してはいけません。
Go 1.17+ — unsafe.Add / unsafe.Slice(unsafefuncs)
| 変更前 | 変更後 |
|---|
unsafe.Pointer(uintptr(ptr) + uintptr(n)) | unsafe.Add(ptr, n) |
(*[n]T)(unsafe.Pointer(p))[:] によるスライス構築 | unsafe.Slice(p, n) |
// before — pointer arithmetic via uintptr
p2 := unsafe.Pointer(uintptr(ptr) + uintptr(offset))
// after
p2 := unsafe.Add(ptr, offset)
// before — building a slice from a base pointer
s := (*[1 << 30]byte)(unsafe.Pointer(p))[:n:n]
// after
s := unsafe.Slice(p, n)
unsafefuncs モダナイザー(gopls v0.22.0)は、エラーが起きやすい uintptr によるポインター演算を、コンパイラーと go vet が GC セーフと認識する unsafe.Add / unsafe.Slice に書き換えます。
Go 1.18+ — any
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }
Go 1.18+ — strings.Cut
| 変更前 | 変更後 |
|---|
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):] | key, val, found := strings.Cut(s, sep) |
// before
if i := strings.Index(s, "="); i >= 0 {
key, val := s[:i], s[i+1:]
}
// after
if key, val, found := strings.Cut(s, "="); found {
...
}
Go 1.18+ — bytes.Cut
| 変更前 | 変更後 |
|---|
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):] | before, after, found := bytes.Cut(b, sep) |
// before
if i := bytes.Index(b, sep); i >= 0 {
before, after := b[:i], b[i+len(sep):]
}
// after
before, after, found := bytes.Cut(b, sep)
Go 1.19+ — fmt.Appendf
| 変更前 | 変更後 |
|---|
buf = append(buf, fmt.Sprintf(...)...) | buf = fmt.Appendf(buf, ...) |
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)
Go 1.19+ — 型安全なアトミック操作(atomictypes)
| 変更前 | 変更後 |
|---|
atomic.StoreInt32(&v, 1) / atomic.LoadInt32(&v) | var v atomic.Int32; v.Store(1); v.Load() |
atomic.AddInt64(&v, 1) | var v atomic.Int64; v.Add(1) |
atomic.Value + 型アサーション | atomic.Pointer[T] |
// before
var ready int32
atomic.StoreInt32(&ready, 1)
if atomic.LoadInt32(&ready) == 1 { ... }
// after
var ready atomic.Int32
ready.Store(1)
if ready.Load() == 1 { ... }
// before
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)
// after
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()
atomictypes モダナイザー(gopls v0.22.0、AtomicTypesAnalyzer)は、変数宣言とすべての呼び出し箇所を書き換えます。型付きラッパー(atomic.Int32/Int64/Uint32/Uint64/Bool/Pointer[T])のパフォーマンスは同等ですが、誤った非アトミックアクセスを防ぎ、32 ビットアーキテクチャで 64 ビット値のアラインメントが原因で発生するクラッシュを解消します。
Go 1.20+ — strings.Clone
| 変更前 | 変更後 |
|---|
string([]byte(s)) | strings.Clone(s) |
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)
Go 1.20+ — bytes.Clone
| Before | After |
|---|
make([]byte, len(src)); copy(dst, src) | bytes.Clone(src) |
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)
Go 1.20+ — strings.CutPrefix / strings.CutSuffix
| Before | After |
|---|
if strings.HasPrefix(s, p) { s = s[len(p):] } | if rest, ok := strings.CutPrefix(s, p); ok { s = rest } |
if strings.HasSuffix(s, sf) { s = s[:len(s)-len(sf)] } | if rest, ok := strings.CutSuffix(s, sf); ok { s = rest } |
// before
if strings.HasPrefix(s, "pre_") {
s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
s = rest
}
// before
if strings.HasSuffix(s, ".txt") {
s = s[:len(s)-len(".txt")]
}
// after
if rest, ok := strings.CutSuffix(s, ".txt"); ok {
s = rest
}
Go 1.20+ — errors.Join
| Before | After |
|---|
fmt.Errorf("...: %w: %w", err1, err2) | errors.Join(err1, err2) |
// before
return fmt.Errorf("load config: %w: %w", err1, err2)
// after
return errors.Join(fmt.Errorf("load config"), err1, err2)
Go 1.20+ — context.WithCancelCause
| Before | After |
|---|
ctx, cancel := context.WithCancel(parent) + bare cancel() | ctx, cancel := context.WithCancelCause(parent) + cancel(err) |
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()
// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdown
Go 1.21+ — min / max
| Before | After |
|---|
if a < b { v = a } else { v = b } | v = min(a, b) |
if a > b { v = a } else { v = b } | v = max(a, b) |
if x < lo { x = lo }; if x > hi { x = hi } | x = min(max(x, lo), hi) |
// before
lo := a
if b < lo {
lo = b
}
// after
lo := min(a, b)
// before
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
// after
x = min(max(x, 0), 100)
Go 1.21+ — clear
| Before | After |
|---|
for k := range m { delete(m, k) } | clear(m) |
for i := range s { s[i] = zero } | clear(s) |
// before
for k := range m {
delete(m, k)
}
// after
clear(m)
// before
for i := range s {
s[i] = 0
}
// after
clear(s)
Go 1.21+ — slices package
| Before | After |
|---|
| 要素を検索する手動ループ | slices.Contains(items, target) |
| インデックスまたは -1 を返すループ | slices.Index(items, target) |
sort.Slice(items, func(i,j int) bool { return items[i] < items[j] }) | slices.SortFunc(items, cmp.Compare) |
| 最大値/最小値を探すループ | slices.Max(items) / slices.Min(items) |
| 逆順に入れ替えるループ | slices.Reverse(s) |
| 連続する重複要素を削除するループ | slices.Compact(s) |
s[:len(s):len(s)] | slices.Clip(s) |
make([]T, len(src)); copy(dst, src) | slices.Clone(src) |
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
if x == target {
found = true
break
}
}
// before → after: slices.Index(items, target)
for i, x := range items {
if x == target {
return i
}
}
return -1
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
if v > max {
max = v
}
}
// before → after: slices.Reverse(s)
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
// before → after: slices.Compact(s)
i := 0
for j := 1; j < len(s); j++ {
if s[j] != s[i] {
i++
s[i] = s[j]
}
}
s = s[:i+1]
// before → after: slices.Clip(s)
s = s[:len(s):len(s)]
// before → after: slices.Clone(src)
dst := make([]T, len(src))
copy(dst, src)
SortFunc を使用する場合は、"slices" と "cmp" のインポートが必要です。
Go 1.21+ — slices.Delete / slices.Insert
| Before | After |
|---|
append(s[:i], s[i+1:]...) | slices.Delete(s, i, i+1) |
append(s[:i:i], append([]T{x}, s[i:]...)...) | slices.Insert(s, i, x) |
// before — element removal (classic aliasing/leak footgun)
s = append(s[:i], s[i+1:]...)
// after
s = slices.Delete(s, i, i+1)
// before — insert at index i
s = append(s[:i], append([]T{v}, s[i:]...)...)
// after
s = slices.Insert(s, i, v)
slices.Delete はポインターを保持しないよう、末尾の要素をゼロ値で上書きします(手動の append 形式ではポインターが残ります)。"slices" のインポートが必要です。
Go 1.21+ — slices.Equal / maps.Equal
| Before | After |
|---|
比較可能なスライスに対する reflect.DeepEqual(a, b) | slices.Equal(a, b) |
比較可能なマップに対する reflect.DeepEqual(m1, m2) | maps.Equal(m1, m2) |
// before
if reflect.DeepEqual(got, want) { ... } // got, want are []string
// after
if slices.Equal(got, want) { ... }
リフレクションを使わず、高速で型安全です。直接比較可能な要素型にのみ使用できます(それ以外の場合は slices.EqualFunc / maps.EqualFunc を使用してください)。"slices" または "maps" のインポートが必要です。
Go 1.21+ — maps package
| Before | After |
|---|
| マップをコピーする手動ループ | maps.Clone(m) |
for k, v := range src { dst[k] = v } | maps.Copy(dst, src) |
| ループと条件付き削除 | maps.DeleteFunc(m, predicate) |
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
dst[k] = v
}
// before → after: maps.Copy(dst, src)
for k, v := range src {
dst[k] = v
}
// before → after: maps.DeleteFunc(m, func(k K, v V) bool { return v == 0 })
for k, v := range m {
if v == 0 {
delete(m, k)
}
}
"maps" のインポートが必要です。
Go 1.22+ — slices.Concat (appendclipped)
| Before | After |
|---|
append(append([]T(nil), s1...), s2...) | slices.Concat(s1, s2) |
新しい結果を作る場合の append(slices.Clip(s1), s2...) | slices.Concat(s1, s2) |
// before
all := append(append([]int(nil), a...), b...)
// after
all := slices.Concat(a, b)
// before — three-way concat
merged := append(append(append([]string(nil), x...), y...), z...)
// after
merged := slices.Concat(x, y, z)
appendclipped モダナイザーは、複数のスライスを連結するネストされた append を slices.Concat に置き換えます。slices.Concat は、正しいサイズの新しい結果を割り当てます。slices.Concat は Go 1.22 で追加されました。"slices" のインポートが必要です。このパターンが新しいスライスを作る場合([]T(nil) またはクリップされたベースから始まる場合)にのみ適用してください。既存のスライスへのインプレース追加には適用しないでください。
Go 1.21+ — sync.OnceFunc / sync.OnceValue
| 変更前 | 変更後 |
|---|
var once sync.Once; once.Do(func() { ... }) | f := sync.OnceFunc(func() { ... }); f() |
sync.Once + 結果を格納する変数 | sync.OnceValue(func() T { return val }) |
// before
var once sync.Once
func init() { once.Do(func() { setup() }) }
// after
var initOnce = sync.OnceFunc(func() { setup() })
// before
var once sync.Once
var cfg *Config
func getConfig() *Config {
once.Do(func() { cfg = loadConfig() })
return cfg
}
// after
var getConfig = sync.OnceValue(func() *Config { return loadConfig() })
Go 1.21+ — context.AfterFunc
| 変更前 | 変更後 |
|---|
go func() { <-ctx.Done(); cleanup() }() | stop := context.AfterFunc(ctx, cleanup) |
// before
go func() {
<-ctx.Done()
conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })
Go 1.21+ — context.WithTimeoutCause / WithDeadlineCause
| 変更前 | 変更後 |
|---|
context.WithTimeout(parent, d) | context.WithTimeoutCause(parent, d, err) |
// before
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// after
ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, ErrTimeout)
意味のある原因エラーが利用できる場合にのみ適用してください。
Go 1.22+ — 整数に対する range
| 変更前 | 変更後 |
|---|
for i := 0; i < n; i++ { ... } | for i := range n { ... } |
for i := 0; i < n; i++ { ... }(i を使用しない場合) | for range n { ... } |
// before
for i := 0; i < len(items); i++ {
process(i, items[i])
}
// after
for i := range len(items) {
process(i, items[i])
}
// before
for i := 0; i < n; i++ {
doWork()
}
// after
for range n {
doWork()
}
Go 1.22+ — ループ変数のシャドーイングの削除
| 変更前 | 変更後 |
|---|
for _, x := range items { x := x; ... } | for _, x := range items { ... } |
// before
for _, x := range items {
x := x // capture for goroutine
go func() { use(x) }()
}
// after
for _, x := range items {
go func() { use(x) }()
}
x := x で値をキャプチャするイディオムは、Go 1.22 以降は不要です。
Go 1.22+ — cmp.Or
| 変更前 | 変更後 |
|---|
if v == "" { v = fallback } の連鎖 | v := cmp.Or(val, fallback1, fallback2, ...) |
// before
name := os.Getenv("NAME")
if name == "" {
name = os.Getenv("USER")
}
if name == "" {
name = "anonymous"
}
// after
name := cmp.Or(os.Getenv("NAME"), os.Getenv("USER"), "anonymous")
"cmp" のインポートが必要です。
Go 1.22+ — reflect.TypeFor
| 変更前 | 変更後 |
|---|
reflect.TypeOf((*T)(nil)).Elem() | reflect.TypeFor[T]() |
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()
Go 1.22+ — 強化された http.ServeMux
| 変更前 | 変更後 |
|---|
mux.HandleFunc("/api/", h) + 手動でのパス解析 | mux.HandleFunc("GET /api/{id}", h) + r.PathValue("id") |
// before
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/")
...
})
// after
mux.HandleFunc("GET /api/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
...
})
Go 1.22+ — math/rand/v2
| 変更前 | 変更後 |
|---|
rand.Intn(n) | rand.IntN(n) |
rand.Int63n(n) / rand.Int31n(n) | rand.Int64N(n) / rand.Int32N(n) |
rand.Seed(...) + グローバル関数 | Seed を削除し、自動シードされるトップレベル関数または rand.N[T] を使用 |
// before
import "math/rand"
n := rand.Intn(100)
// after
import "math/rand/v2"
n := rand.IntN(100)
// before — type-specific bound
d := time.Duration(rand.Int63n(int64(max)))
// after — generic N works for any integer type
d := rand.N(max)
math/rand/v2(Go 1.22)では、非推奨のグローバルな Seed が削除され(トップレベル関数は自動でシードされます)、ジェネリックな rand.N[T] が追加されました。**セマンティクス上の移行:**乱数列は math/rand と異なるため、固定シードによる再現性が重要な場合は適用しないでください。自動適用ではなく、提案として提示してください。
Go 1.23+ — 関数に対する range(イテレーター)
| 変更前 | 変更後 |
|---|
func Walk(yield func(T) bool) によるカスタムコールバック走査 | イテレーターを返す func Walk() iter.Seq[T] |
// before — callback-based iteration
func (t *Tree) Each(fn func(v int)) {
for _, v := range t.values {
fn(v)
}
}
// caller: t.Each(func(v int) { use(v) })
// after — standard iterator, usable with range
func (t *Tree) All() iter.Seq[int] {
return func(yield func(int) bool) {
for _, v := range t.values {
if !yield(v) { return }
}
}
}
// caller: for v := range t.All() { use(v) }
iter.Seq[T] / iter.Seq2[K,V] プロトコル(Go 1.23)を採用すると、カスタムコンテナを range、slices.Collect、maps.Keys などと組み合わせられます。"iter" のインポートが必要です。API の範囲が変わるため、提案として提示してください。
Go 1.23+ — イテレーターヘルパー
| 変更前 | 変更後 |
|---|
var keys []K; for k := range m { keys = append(keys, k) } | slices.Collect(maps.Keys(m)) |
var vals []V; for _, v := range m { vals = append(vals, v) } | slices.Collect(maps.Values(m)) |
// before
var keys []string
for k := range m {
keys = append(keys, k)
}
// after
keys := slices.Collect(maps.Keys(m))
// before
var vals []int
for _, v := range m {
vals = append(vals, v)
}
// after
vals := slices.Collect(maps.Values(m))
"slices" と "maps" のインポートが必要です。
Go 1.23+ — strings.SplitSeq / strings.FieldsSeq
| 変更前 | 変更後 |
|---|
for _, part := range strings.Split(s, sep) | for part := range strings.SplitSeq(s, sep) |
for _, field := range strings.Fields(s) | for field := range strings.FieldsSeq(s) |
// before
for _, part := range strings.Split(line, ",") {
process(part)
}
// after
for part := range strings.SplitSeq(line, ",") {
process(part)
}
ループ本体でインデックスやスライス全体を必要としない場合にのみ適用してください。
Go 1.23+ — bytes.SplitSeq / bytes.FieldsSeq
| 変更前 | 変更後 |
|---|
for _, part := range bytes.Split(b, sep) | for part := range bytes.SplitSeq(b, sep) |
// before
for _, part := range bytes.Split(data, sep) {
process(part)
}
// after
for part := range bytes.SplitSeq(data, sep) {
process(part)
}
Go 1.23+ — slices.Backward (slicesbackward)
| Before | After |
|---|
for i := len(s) - 1; i >= 0; i-- { use(s[i]) } | for _, v := range slices.Backward(s) { use(v) } |
| インデックスも必要な逆順ループ | for i, v := range slices.Backward(s) { ... } |
// before
for i := len(items) - 1; i >= 0; i-- {
process(items[i])
}
// after
for _, v := range slices.Backward(items) {
process(v)
}
// before — index still needed
for i := len(items) - 1; i >= 0; i-- {
fmt.Println(i, items[i])
}
// after
for i, v := range slices.Backward(items) {
fmt.Println(i, v)
}
slicesbackward モダナイザー(gopls v0.22.0)は、手動の降順インデックスループを slices.Backward イテレーターに置き換えます。"slices" のインポートが必要です。**注意点:**通常のケースでは、この書き換えによって正確なセマンティクスが保たれますが、ループ本体でスライスの長さを変更する場合や、インデックスを範囲外の算術演算に使用する場合は適用しないでください。こうしたエッジケースでは安全でなくなる可能性があります。
Go 1.24+ — テストでの t.Context()
| Before | After |
|---|
ctx, cancel := context.WithCancel(context.Background()); defer cancel() | ctx := t.Context() |
// before
func TestFetch(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result := fetch(ctx)
}
// after
func TestFetch(t *testing.T) {
result := fetch(t.Context())
}
Go 1.24+ — strings.Lines / bytes.Lines
| Before | After |
|---|
文字列/バッファーに対する bufio.Scanner の行ループ | for line := range strings.Lines(s) |
// before
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
process(sc.Text())
}
// after
for line := range strings.Lines(s) {
process(strings.TrimSuffix(line, "\n"))
}
strings.Lines / bytes.Lines(Go 1.24)は行イテレーターを返すため、Scanner のセットアップが不要で、デフォルトの 64KB トークンサイズ制限もありません。返される行には Scanner.Text() と異なり、末尾の \n が残ることに注意してください。以前のコードが改行を除去した行を前提としていた場合は、末尾の改行を削除してください。メモリー上の文字列/バッファーにのみ適用し、ストリーミングする io.Reader には適用しないでください。
Go 1.24+ — os.Root(ディレクトリーにスコープされたファイルシステムアクセス)
| Before | After |
|---|
パストラバーサルを防ぐための手動の filepath.Clean +プレフィックスチェック | root, _ := os.OpenRoot(dir); root.Open(name) |
// before — hand-rolled path-traversal guard
p := filepath.Join(base, name)
if !strings.HasPrefix(filepath.Clean(p), filepath.Clean(base)+string(os.PathSeparator)) {
return errUnsafePath
}
f, err := os.Open(p)
// after — the OS enforces the boundary
root, err := os.OpenRoot(base)
if err != nil { return err }
defer root.Close()
f, err := root.Open(name) // symlinks/".." escaping base are rejected
os.Root(Go 1.24)はすべての操作をディレクトリーツリー内に限定し、システムコール層で .. やシンボリックリンクによる脱出を拒否します。文字列のプレフィックスチェックよりもはるかに堅牢です。**セキュリティ強化:**ユーザーが制御するパスをベースディレクトリーに結合する箇所では、強く推奨する改善として指摘してください。
Go 1.24+ — omitzero 構造体タグ
| Before | After |
|---|
json:"field,omitempty"(time.Time、time.Duration、構造体、スライス、マップの場合) | json:"field,omitzero" |
// before
type Config struct {
Timeout time.Duration `json:"timeout,omitempty"`
Labels []string `json:"labels,omitempty"`
}
// after
type Config struct {
Timeout time.Duration `json:"timeout,omitzero"`
Labels []string `json:"labels,omitzero"`
}
omitempty が機能しない型、つまり time.Time、time.Duration、構造体、スライス、マップにのみ適用します。自動適用せず、提案として指摘してください。
Go 1.24+ — ベンチマークでの b.Loop()
| Before | After |
|---|
for i := 0; i < b.N; i++ { ... } | for b.Loop() { ... } |
// before
func BenchmarkHash(b *testing.B) {
for i := 0; i < b.N; i++ {
hash(input)
}
}
// after
func BenchmarkHash(b *testing.B) {
for b.Loop() {
hash(input)
}
}
Go 1.25+ — sync.WaitGroup.Go
| Before | After |
|---|
wg.Add(1); go func() { defer wg.Done(); fn() }() | wg.Go(fn) |
// before
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
// after
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() { process(item) })
}
wg.Wait()
Go 1.26+ — 式を使った new
| Before | After |
|---|
v := val; &v | new(val) |
ヘルパー func ptr[T any](v T) *T { return &v } | 直接 new(val) を使用 |
// before
timeout := 30
debug := true
cfg := Config{
Timeout: &timeout,
Debug: &debug,
}
// after
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
// before
func ptr[T any](v T) *T { return &v }
cfg := Config{Count: ptr(10)}
// after
cfg := Config{Count: new(10)}
Go 1.26+ — errors.AsType
| Before | After |
|---|
var t *T; errors.As(err, &t) | t, ok := errors.AsType[*T](err) |
// before
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println(pathErr.Path)
}
// after
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
log.Println(pathErr.Path)
}
Go 1.27+ — 埋め込みフィールドのリテラル(embedlit)
| Before | After |
|---|
T{U: U{x: 1}}(冗長な埋め込み型指定子) | T{x: 1} |
type Base struct {
ID int
Name string
}
type User struct {
Base
Age int
}
// before
u := User{
Base: Base{ID: 1, Name: "alice"},
Age: 30,
}
// after
u := User{
ID: 1,
Name: "alice",
Age: 30,
}
embedlit モダナイザー(gopls v0.22.0、EmbedLitAnalyzer)は、複合リテラルから冗長な埋め込み構造体フィールド型指定子を取り除きます。Go 1.27 では、ネストされたリテラルを使わずに昇格されたフィールドを直接初期化できます。昇格されたフィールド名が外側の構造体のフィールド名と衝突しない場合にのみ適用してください。
操作フェーズ
フェーズ 1:検出
go.mod を読み込み、Go のバージョン(go 1.xx の行)を抽出します。go.mod が見つからない場合は、デフォルトで go 1.21 を使用します。
フェーズ 2:ファイルの収集
対象範囲(プロジェクトのルート、またはユーザーが指定したファイル/ディレクトリー)内のすべての .go ファイルを検索します。vendor/、.git/、testdata/ ディレクトリーは除外します。
フェーズ 3:変換を適用する
各 .go ファイルに対し、検出された Go バージョン以下を対象とするすべての変換を適用します。ファイルは順番に処理します。各ファイルについて、次の手順を実行します。
- ファイルの内容を読み取ります。
- 「Before」パターンを検索して、適用可能な変換を特定します。
- Edit ツールを使用して各変換を適用します。
- すべての編集後に、ファイルに対して
goimports -w(または gofmt -w)を実行します。
プロジェクトの Go バージョンより高いバージョンを必要とする変換は、決して適用しないでください。
フェーズ 4:報告
次の項目を示す概要テーブルを出力します。
- ファイル:プロジェクトルートからの相対パス
- 適用した変換:ファイルごとに適用した変換名の一覧
- 変更したファイルの合計数と適用した変換の合計数
- スキップした変換(利用可能だが、バージョンの制約により適用できなかったもの)と、それに必要な Go バージョン
概要出力の例
## Modernization Summary
| File | Transformations |
|---|---|
| main.go | any, strings.Cut, min/max (2 occurrences) |
| pkg/handler.go | range over int (3), slices.Contains, t.Context() |
| pkg/util.go | new(expr) (1), errors.AsType → errors.Is |
**3 files modified, 10 transformations applied**
Skipped (requires higher Go version):
- new(expr): requires go 1.26 (project is go 1.24)
- WaitGroup.Go: requires go 1.25 (project is go 1.24)
安全規則
- エッジケースで動作を変える変換は、ユーザーに知らせずに決して適用しないでください。
omitzero をむやみに適用しないでください。JSON のシリアライズ動作が変わるため、提案として提示してください。
- セマンティクスを変える移行は、自動適用ではなく提案として扱ってください。
math/rand/v2(乱数ストリームが変わります)、iter.Seq イテレーター(API の形が変わります)、os.Root(動作やエラー経路が変わります)が該当します。ユーザーが選択できるよう、これらを提示してください。
bufio.Scanner ループを strings.Lines/bytes.Lines に置き換える場合、生成される行には末尾の \n が残る点に注意してください。従来のコードが Scanner.Text() のセマンティクスに依存していた場合は、TrimSuffix を追加してください。
- ループ本体がインデックスまたはスライス全体を別の場所で参照している場合は、
strings.SplitSeq または bytes.SplitSeq を適用しないでください。
- ループの外で連結している場合は、
strings.Builder を適用しないでください(単一の += は問題ありません)。
- 新しいインポートが必要な変換では、インポートがファイルに追加されていることを確認してください。
- すべての編集後に、変更した各ファイルに対して
goimports -w を実行し、インポートを整理してください。
goimports が利用できない場合は、代わりに gofmt -w を実行してください。
- プロジェクトに
go.mod がない場合は、作業を進める前に対象の Go バージョンをユーザーに確認してください。
自動化ツール(Go 1.27 以降)
これらの変換の多くは、現在 gopls/go fix の公式モダナイザーとして提供されています。Go 1.27 以降では、次のコマンドでモジュール全体に一連の変換を適用できます。
go fix ./...
gopls v0.22.0 では、上記で取り上げた次の 4 つのパスが追加されました。
| モダナイザー | 最小 Go バージョン | 変換 |
|---|
unsafefuncs | 1.17 | uintptr によるポインター演算 → unsafe.Add/unsafe.Slice |
atomictypes | 1.19 | プリミティブな atomic.* 関数 → 型付き atomic.Int32/Pointer[T] ラッパー |
slicesbackward | 1.23 | インデックスを降順にたどるループ → slices.Backward イテレーター |
embedlit | 1.27 | 冗長な埋め込みフィールドのリテラル → 昇格フィールドの初期化 |
このカタログで取り上げている同じ一連のその他のモダナイザーには、minmax、efaceany、fmtappendf、stringscut、stringsseq、sortslice/slicescontains、mapsloop、stditerators、forvar、rangeint、testingcontext、bloop、waitgroup、newexpr、errorsastype、appendclipped(→ slices.Concat)、plusbuild(→ 古い // +build の削除)があります。
過剰に適用されるパス(スライスを変更するループを書き換える slicesbackward など)を無効にするには、-fixes を指定して実行範囲を限定するか、エディターの gopls 設定でそのアナライザーを除外してください。