mcpField
M
modern-go

modern-go

github.com/smallnest開発296523 回インストール6 週間前に更新
リポジトリ

modern-go について

modern-goは、プロジェクトが宣言するGoバージョンに合わせて、GoソースのイディオムやAPIを更新するスキルです。バージョン条件付きの変換を適用し、変更点と対象外を報告します。

主な機能

  1. 01go.modからGoバージョンを確認
  2. 02対象ファイルを指定し所定のディレクトリを除外
  3. 03適用可能な変換をバージョン順に実行
  4. 04標準ライブラリAPIと言語イディオムを更新
  5. 05変更点と対象外の変換を要約

活用シーン

  1. 01古いGoの書き方を宣言バージョン向けに更新
  2. 02指定したファイルやディレクトリを近代化
  3. 03標準ライブラリのパターンを手作業で置き換える負担を軽減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/smallnest/goal-workflow --skill modern-go

インストール後、Claude Code で/modern-goを入力して読み込まれたか確認します。

詳細

modern-go

Go のソースコードに、バージョンに応じたイディオム、API、言語機能を適用して最新化します。go fix と同様に動作し、Go チームの modernize 解析パスやコミュニティのベストプラクティスに基づいて選定された追加の変換も適用します。

使用方法

ユーザーが Go コードの最新化を依頼した場合に、このスキルを呼び出します。デフォルトではプロジェクト全体を最新化しますが、ユーザーが代わりにファイルやディレクトリを指定することもできます。

呼び出された場合:

  1. go.mod の go ディレクティブから、プロジェクトの Go バージョンを検出します。
  2. 対象範囲内のすべての .go ファイルを検索します(vendor/、.git/、testdata/ は除外)。
  3. 各ファイルに、プロジェクトの Go バージョン以下のすべての変換を、古いものから新しいものの順に適用します。
  4. すべての変換の適用後、変更内容とスキップした内容の概要を表示します。

ユーザーがファイルまたはディレクトリを指定した場合は、対象範囲をそのパスに限定します。

変換カタログ

各変換には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 += itemvar 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.EOFerrors.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
変更前変更後
interface{}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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
要素を検索する手動ループ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
BeforeAfter
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
BeforeAfter
比較可能なスライスに対する 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
BeforeAfter
マップをコピーする手動ループ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)
BeforeAfter
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)
BeforeAfter
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()
BeforeAfter
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
BeforeAfter
文字列/バッファーに対する 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(ディレクトリーにスコープされたファイルシステムアクセス)
BeforeAfter
パストラバーサルを防ぐための手動の 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 構造体タグ
BeforeAfter
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()
BeforeAfter
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
BeforeAfter
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
BeforeAfter
v := val; &vnew(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
BeforeAfter
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)
BeforeAfter
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 バージョン以下を対象とするすべての変換を適用します。ファイルは順番に処理します。各ファイルについて、次の手順を実行します。

  1. ファイルの内容を読み取ります。
  2. 「Before」パターンを検索して、適用可能な変換を特定します。
  3. Edit ツールを使用して各変換を適用します。
  4. すべての編集後に、ファイルに対して 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 バージョン変換
unsafefuncs1.17uintptr によるポインター演算 → unsafe.Add/unsafe.Slice
atomictypes1.19プリミティブな atomic.* 関数 → 型付き atomic.Int32/Pointer[T] ラッパー
slicesbackward1.23インデックスを降順にたどるループ → slices.Backward イテレーター
embedlit1.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 設定でそのアナライザーを除外してください。

コメント