modern-go

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

modern-go

modern-go

Modernize Go source code by applying version-appropriate idioms, APIs, and language features. Works like
go fix
plus additional transformations curated from the Go team's modernize analysis passes and community best practices.
通过应用适配对应版本的惯用写法、API和语言特性来现代化Go源代码。功能类似
go fix
,并额外包含了Go团队现代化分析流程以及社区最佳实践中的转换规则。

Usage

使用方法

Invoke this skill when the user asks to modernize Go code. By default, modernize the entire project; the user may specify a file or directory instead.
When invoked:
  1. Detect the project's Go version from
    go.mod
    (the
    go
    directive).
  2. Find all
    .go
    files in the target scope (excluding
    vendor/
    ,
    .git/
    ,
    testdata/
    ).
  3. For each file, apply all transformations for versions ≤ the project's Go version, starting from the oldest to the newest.
  4. After all transformations, print a summary of what was changed and what was skipped.
If the user specifies a file or directory, limit the scope to that path.
当用户要求现代化Go代码时调用此技能。默认会现代化整个项目;用户也可以指定单个文件或目录。
调用时执行以下步骤:
  1. go.mod
    文件的
    go
    指令中检测项目的Go版本。
  2. 在目标范围内查找所有
    .go
    文件(排除
    vendor/
    .git/
    testdata/
    目录)。
  3. 对每个文件,应用所有版本≤项目Go版本的转换规则,从最旧版本到最新版本依次执行。
  4. 完成所有转换后,打印变更内容和跳过项的汇总信息。
如果用户指定了文件或目录,则将范围限制在该路径内。

Transformation Catalog

转换规则目录

Each transformation includes a Go version gate—only apply when the project's
go.mod
version ≥ that version. Never apply a transformation that requires a version higher than the project declares.
每个转换规则都包含一个Go版本限制——仅当项目
go.mod
中声明的版本≥该版本时才会应用。绝不应用需要高于项目声明版本的转换规则。

Go 1.0+ —
time.Since

Go 1.0+ —
time.Since

BeforeAfter
time.Now().Sub(start)
time.Since(start)
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
转换前转换后
time.Now().Sub(start)
time.Since(start)
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)

Go 1.8+ —
time.Until

Go 1.8+ —
time.Until

BeforeAfter
deadline.Sub(time.Now())
time.Until(deadline)
go
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)
转换前转换后
deadline.Sub(time.Now())
time.Until(deadline)
go
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)

Go 1.10+ —
strings.Builder
(loop concatenation)

Go 1.10+ —
strings.Builder
(循环拼接)

BeforeAfter
s += item
in a loop
var b strings.Builder; b.WriteString(item)
go
// before
s := ""
for _, item := range items {
    s += item
}
// after
var b strings.Builder
for _, item := range items {
    b.WriteString(item)
}
s := b.String()
Only when
+=
concatenation happens inside a loop.
转换前转换后
循环中的
s += item
var b strings.Builder; b.WriteString(item)
go
// 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

Go 1.13+ —
errors.Is

BeforeAfter
err == io.EOF
errors.Is(err, io.EOF)
go
// before
if err == io.EOF {
    return
}
// after
if errors.Is(err, io.EOF) {
    return
}
转换前转换后
err == io.EOF
errors.Is(err, io.EOF)
go
// before
if err == io.EOF {
    return
}
// after
if errors.Is(err, io.EOF) {
    return
}

Go 1.18+ —
any

Go 1.18+ —
any

BeforeAfter
interface{}
any
go
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }
转换前转换后
interface{}
any
go
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }

Go 1.18+ —
strings.Cut

Go 1.18+ —
strings.Cut

BeforeAfter
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):]
key, val, found := strings.Cut(s, sep)
go
// before
if i := strings.Index(s, "="); i >= 0 {
    key, val := s[:i], s[i+1:]
}
// after
if key, val, found := strings.Cut(s, "="); found {
    ...
}
转换前转换后
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):]
key, val, found := strings.Cut(s, sep)
go
// 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

Go 1.18+ —
bytes.Cut

BeforeAfter
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):]
before, after, found := bytes.Cut(b, sep)
go
// 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)
转换前转换后
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):]
before, after, found := bytes.Cut(b, sep)
go
// 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

Go 1.19+ —
fmt.Appendf

BeforeAfter
buf = append(buf, fmt.Sprintf(...)...)
buf = fmt.Appendf(buf, ...)
go
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)
转换前转换后
buf = append(buf, fmt.Sprintf(...)...)
buf = fmt.Appendf(buf, ...)
go
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)

Go 1.19+ — Type-safe atomics

Go 1.19+ — 类型安全的原子操作

BeforeAfter
atomic.StoreInt32(&v, 1)
/
atomic.LoadInt32(&v)
var v atomic.Int32; v.Store(1); v.Load()
atomic.Value
+ type assertion
atomic.Pointer[T]
go
// 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 { ... }
go
// before
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)

// after
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()
转换前转换后
atomic.StoreInt32(&v, 1)
/
atomic.LoadInt32(&v)
var v atomic.Int32; v.Store(1); v.Load()
atomic.Value
+ 类型断言
atomic.Pointer[T]
go
// 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 { ... }
go
// before
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)

// after
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()

Go 1.20+ —
strings.Clone

Go 1.20+ —
strings.Clone

BeforeAfter
string([]byte(s))
strings.Clone(s)
go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)
转换前转换后
string([]byte(s))
strings.Clone(s)
go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)

Go 1.20+ —
bytes.Clone

Go 1.20+ —
bytes.Clone

BeforeAfter
make([]byte, len(src)); copy(dst, src)
bytes.Clone(src)
go
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)
转换前转换后
make([]byte, len(src)); copy(dst, src)
bytes.Clone(src)
go
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)

Go 1.20+ —
strings.CutPrefix
/
strings.CutSuffix

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 }
go
// before
if strings.HasPrefix(s, "pre_") {
    s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
    s = rest
}
go
// before
if strings.HasSuffix(s, ".txt") {
    s = s[:len(s)-len(".txt")]
}
// after
if rest, ok := strings.CutSuffix(s, ".txt"); ok {
    s = rest
}
转换前转换后
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 }
go
// before
if strings.HasPrefix(s, "pre_") {
    s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
    s = rest
}
go
// 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

Go 1.20+ —
errors.Join

BeforeAfter
fmt.Errorf("...: %w: %w", err1, err2)
errors.Join(err1, err2)
go
// before
return fmt.Errorf("load config: %w: %w", err1, err2)
// after
return errors.Join(fmt.Errorf("load config"), err1, err2)
转换前转换后
fmt.Errorf("...: %w: %w", err1, err2)
errors.Join(err1, err2)
go
// 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

Go 1.20+ —
context.WithCancelCause

BeforeAfter
ctx, cancel := context.WithCancel(parent)
+ bare
cancel()
ctx, cancel := context.WithCancelCause(parent)
+
cancel(err)
go
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()

// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdown
转换前转换后
ctx, cancel := context.WithCancel(parent)
+ 无参数
cancel()
ctx, cancel := context.WithCancelCause(parent)
+
cancel(err)
go
// 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

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)
go
// before
lo := a
if b < lo {
    lo = b
}
// after
lo := min(a, b)
go
// before
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}
// after
x = min(max(x, 0), 100)
转换前转换后
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)
go
// before
lo := a
if b < lo {
    lo = b
}
// after
lo := min(a, b)
go
// before
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}
// after
x = min(max(x, 0), 100)

Go 1.21+ —
clear

Go 1.21+ —
clear

BeforeAfter
for k := range m { delete(m, k) }
clear(m)
for i := range s { s[i] = zero }
clear(s)
go
// before
for k := range m {
    delete(m, k)
}
// after
clear(m)
go
// before
for i := range s {
    s[i] = 0
}
// after
clear(s)
转换前转换后
for k := range m { delete(m, k) }
clear(m)
for i := range s { s[i] = zero }
clear(s)
go
// before
for k := range m {
    delete(m, k)
}
// after
clear(m)
go
// before
for i := range s {
    s[i] = 0
}
// after
clear(s)

Go 1.21+ —
slices
package

Go 1.21+ —
slices

BeforeAfter
Manual loop to find element
slices.Contains(items, target)
Loop returning index or -1
slices.Index(items, target)
sort.Slice(items, func(i,j int) bool { return items[i] < items[j] })
slices.SortFunc(items, cmp.Compare)
Max/min finding loop
slices.Max(items)
/
slices.Min(items)
Reverse swap loop
slices.Reverse(s)
Remove consecutive duplicates loop
slices.Compact(s)
s[:len(s):len(s)]
slices.Clip(s)
make([]T, len(src)); copy(dst, src)
slices.Clone(src)
go
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
    if x == target {
        found = true
        break
    }
}
go
// before → after: slices.Index(items, target)
for i, x := range items {
    if x == target {
        return i
    }
}
return -1
go
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
go
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
    if v > max {
        max = v
    }
}
go
// 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]
}
go
// 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]
go
// before → after: slices.Clip(s)
s = s[:len(s):len(s)]
go
// before → after: slices.Clone(src)
dst := make([]T, len(src))
copy(dst, src)
Requires importing
"slices"
and
"cmp"
(for
SortFunc
).
转换前转换后
手动循环查找元素
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)
go
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
    if x == target {
        found = true
        break
    }
}
go
// before → after: slices.Index(items, target)
for i, x := range items {
    if x == target {
        return i
    }
}
return -1
go
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
go
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
    if v > max {
        max = v
    }
}
go
// 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]
}
go
// 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]
go
// before → after: slices.Clip(s)
s = s[:len(s):len(s)]
go
// before → after: slices.Clone(src)
dst := make([]T, len(src))
copy(dst, src)
需要导入
"slices"
"cmp"
(用于
SortFunc
)。

Go 1.21+ —
maps
package

Go 1.21+ —
maps

BeforeAfter
Manual loop to copy a map
maps.Clone(m)
for k, v := range src { dst[k] = v }
maps.Copy(dst, src)
Loop + conditional delete
maps.DeleteFunc(m, predicate)
go
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
    dst[k] = v
}
go
// before → after: maps.Copy(dst, src)
for k, v := range src {
    dst[k] = v
}
go
// 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)
    }
}
Requires importing
"maps"
.
转换前转换后
手动循环复制map
maps.Clone(m)
for k, v := range src { dst[k] = v }
maps.Copy(dst, src)
循环+条件删除
maps.DeleteFunc(m, predicate)
go
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
    dst[k] = v
}
go
// before → after: maps.Copy(dst, src)
for k, v := range src {
    dst[k] = v
}
go
// 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.21+ —
sync.OnceFunc
/
sync.OnceValue

Go 1.21+ —
sync.OnceFunc
/
sync.OnceValue

BeforeAfter
var once sync.Once; once.Do(func() { ... })
f := sync.OnceFunc(func() { ... }); f()
sync.Once
+ stored result variable
sync.OnceValue(func() T { return val })
go
// before
var once sync.Once
func init() { once.Do(func() { setup() }) }

// after
var initOnce = sync.OnceFunc(func() { setup() })
go
// 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() })
转换前转换后
var once sync.Once; once.Do(func() { ... })
f := sync.OnceFunc(func() { ... }); f()
sync.Once
+ 存储结果的变量
sync.OnceValue(func() T { return val })
go
// before
var once sync.Once
func init() { once.Do(func() { setup() }) }

// after
var initOnce = sync.OnceFunc(func() { setup() })
go
// 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 1.21+ —
context.AfterFunc

BeforeAfter
go func() { <-ctx.Done(); cleanup() }()
stop := context.AfterFunc(ctx, cleanup)
go
// before
go func() {
    <-ctx.Done()
    conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })
转换前转换后
go func() { <-ctx.Done(); cleanup() }()
stop := context.AfterFunc(ctx, cleanup)
go
// before
go func() {
    <-ctx.Done()
    conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })

Go 1.21+ —
context.WithTimeoutCause
/
WithDeadlineCause

Go 1.21+ —
context.WithTimeoutCause
/
WithDeadlineCause

BeforeAfter
context.WithTimeout(parent, d)
context.WithTimeoutCause(parent, d, err)
go
// before
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// after
ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, ErrTimeout)
Only apply when a meaningful cause error is available.
转换前转换后
context.WithTimeout(parent, d)
context.WithTimeoutCause(parent, d, err)
go
// before
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// after
ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, ErrTimeout)
仅当存在有意义的错误原因时才应用此规则。

Go 1.22+ — Range over integer

Go 1.22+ — 遍历整数

BeforeAfter
for i := 0; i < n; i++ { ... }
for i := range n { ... }
for i := 0; i < n; i++ { ... }
(i unused)
for range n { ... }
go
// before
for i := 0; i < len(items); i++ {
    process(i, items[i])
}
// after
for i := range len(items) {
    process(i, items[i])
}
go
// before
for i := 0; i < n; i++ {
    doWork()
}
// after
for range n {
    doWork()
}
转换前转换后
for i := 0; i < n; i++ { ... }
for i := range n { ... }
for i := 0; i < n; i++ { ... }
(i未使用)
for range n { ... }
go
// before
for i := 0; i < len(items); i++ {
    process(i, items[i])
}
// after
for i := range len(items) {
    process(i, items[i])
}
go
// before
for i := 0; i < n; i++ {
    doWork()
}
// after
for range n {
    doWork()
}

Go 1.22+ — Loop variable shadowing removal

Go 1.22+ — 移除循环变量遮蔽

BeforeAfter
for _, x := range items { x := x; ... }
for _, x := range items { ... }
go
// before
for _, x := range items {
    x := x          // capture for goroutine
    go func() { use(x) }()
}
// after
for _, x := range items {
    go func() { use(x) }()
}
The
x := x
capture idiom is redundant since Go 1.22.
转换前转换后
for _, x := range items { x := x; ... }
for _, x := range items { ... }
go
// before
for _, x := range items {
    x := x          // capture for goroutine
    go func() { use(x) }()
}
// after
for _, x := range items {
    go func() { use(x) }()
}
自Go 1.22起,
x := x
这种捕获写法已不再必要。

Go 1.22+ —
cmp.Or

Go 1.22+ —
cmp.Or

BeforeAfter
Chain of
if v == "" { v = fallback }
v := cmp.Or(val, fallback1, fallback2, ...)
go
// 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")
Requires importing
"cmp"
.
转换前转换后
链式
if v == "" { v = fallback }
v := cmp.Or(val, fallback1, fallback2, ...)
go
// 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

Go 1.22+ —
reflect.TypeFor

BeforeAfter
reflect.TypeOf((*T)(nil)).Elem()
reflect.TypeFor[T]()
go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()
转换前转换后
reflect.TypeOf((*T)(nil)).Elem()
reflect.TypeFor[T]()
go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()

Go 1.22+ — Enhanced
http.ServeMux

Go 1.22+ — 增强版
http.ServeMux

BeforeAfter
mux.HandleFunc("/api/", h)
+ manual path parsing
mux.HandleFunc("GET /api/{id}", h)
+
r.PathValue("id")
go
// 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")
    ...
})
转换前转换后
mux.HandleFunc("/api/", h)
+ 手动路径解析
mux.HandleFunc("GET /api/{id}", h)
+
r.PathValue("id")
go
// 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.23+ — Iterator helpers

Go 1.23+ — 迭代器辅助函数

BeforeAfter
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))
go
// before
var keys []string
for k := range m {
    keys = append(keys, k)
}
// after
keys := slices.Collect(maps.Keys(m))
go
// before
var vals []int
for _, v := range m {
    vals = append(vals, v)
}
// after
vals := slices.Collect(maps.Values(m))
Requires importing
"slices"
and
"maps"
.
转换前转换后
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))
go
// before
var keys []string
for k := range m {
    keys = append(keys, k)
}
// after
keys := slices.Collect(maps.Keys(m))
go
// 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

Go 1.23+ —
strings.SplitSeq
/
strings.FieldsSeq

BeforeAfter
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)
go
// before
for _, part := range strings.Split(line, ",") {
    process(part)
}
// after
for part := range strings.SplitSeq(line, ",") {
    process(part)
}
Only when the loop body does not need the index or the full slice.
转换前转换后
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)
go
// 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

Go 1.23+ —
bytes.SplitSeq
/
bytes.FieldsSeq

BeforeAfter
for _, part := range bytes.Split(b, sep)
for part := range bytes.SplitSeq(b, sep)
go
// before
for _, part := range bytes.Split(data, sep) {
    process(part)
}
// after
for part := range bytes.SplitSeq(data, sep) {
    process(part)
}
转换前转换后
for _, part := range bytes.Split(b, sep)
for part := range bytes.SplitSeq(b, sep)
go
// before
for _, part := range bytes.Split(data, sep) {
    process(part)
}
// after
for part := range bytes.SplitSeq(data, sep) {
    process(part)
}

Go 1.24+ —
t.Context()
in tests

Go 1.24+ — 测试中的
t.Context()

BeforeAfter
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
ctx := t.Context()
go
// 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())
}
转换前转换后
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
ctx := t.Context()
go
// 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+ —
omitzero
struct tag

Go 1.24+ —
omitzero
结构体标签

BeforeAfter
json:"field,omitempty"
(for
time.Time
,
time.Duration
, structs, slices, maps)
json:"field,omitzero"
go
// 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"`
}
Only for types where
omitempty
fails:
time.Time
,
time.Duration
, structs, slices, maps. Flag as suggestion, not auto-apply.
转换前转换后
json:"field,omitempty"
(适用于
time.Time
time.Duration
、结构体、切片、map)
json:"field,omitzero"
go
// 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
、结构体、切片、map。此规则仅作为建议,不自动应用。

Go 1.24+ —
b.Loop()
in benchmarks

Go 1.24+ — 基准测试中的
b.Loop()

BeforeAfter
for i := 0; i < b.N; i++ { ... }
for b.Loop() { ... }
go
// 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)
    }
}
转换前转换后
for i := 0; i < b.N; i++ { ... }
for b.Loop() { ... }
go
// 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

Go 1.25+ —
sync.WaitGroup.Go

BeforeAfter
wg.Add(1); go func() { defer wg.Done(); fn() }()
wg.Go(fn)
go
// 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()
转换前转换后
wg.Add(1); go func() { defer wg.Done(); fn() }()
wg.Go(fn)
go
// 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
with expressions

Go 1.26+ — 带表达式的
new

BeforeAfter
v := val; &v
new(val)
Helper
func ptr[T any](v T) *T { return &v }
new(val)
directly
go
// before
timeout := 30
debug := true
cfg := Config{
    Timeout: &timeout,
    Debug:   &debug,
}
// after
cfg := Config{
    Timeout: new(30),
    Debug:   new(true),
}
go
// before
func ptr[T any](v T) *T { return &v }
cfg := Config{Count: ptr(10)}

// after
cfg := Config{Count: new(10)}
转换前转换后
v := val; &v
new(val)
辅助函数
func ptr[T any](v T) *T { return &v }
直接使用
new(val)
go
// before
timeout := 30
debug := true
cfg := Config{
    Timeout: &timeout,
    Debug:   &debug,
}
// after
cfg := Config{
    Timeout: new(30),
    Debug:   new(true),
}
go
// 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

Go 1.26+ —
errors.AsType

BeforeAfter
var t *T; errors.As(err, &t)
t, ok := errors.AsType[*T](err)
go
// 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)
}
转换前转换后
var t *T; errors.As(err, &t)
t, ok := errors.AsType[*T](err)
go
// 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)
}

Operation Phases

操作阶段

Phase 1: Detect

阶段1:检测版本

Read
go.mod
to extract the Go version (
go 1.xx
line). If no
go.mod
is found, default to
go 1.21
.
读取
go.mod
文件提取Go版本(
go 1.xx
行)。如果未找到
go.mod
文件,默认使用
go 1.21

Phase 2: Gather files

阶段2:收集文件

Find all
.go
files in the target scope (project root, or user-specified file/directory). Exclude
vendor/
,
.git/
, and
testdata/
directories.
在目标范围内查找所有
.go
文件(项目根目录,或用户指定的文件/目录)。排除
vendor/
.git/
testdata/
目录。

Phase 3: Apply transformations

阶段3:应用转换规则

For each
.go
file, apply all transformations for versions ≤ the detected Go version. Process files sequentially. For each file:
  1. Read the file content.
  2. Identify applicable transformations by scanning for the "Before" patterns.
  3. Apply each transformation using the Edit tool.
  4. Run
    goimports -w
    (or
    gofmt -w
    ) on the file after all edits.
Never apply a transformation that requires a version higher than the project's Go version.
对每个
.go
文件,应用所有版本≤检测到的Go版本的转换规则。按顺序处理文件。对每个文件执行以下步骤:
  1. 读取文件内容。
  2. 扫描「转换前」模式,识别适用的转换规则。
  3. 使用编辑工具应用每个转换规则。
  4. 所有编辑完成后,对文件运行
    goimports -w
    (或
    gofmt -w
    )。
绝不应用需要高于项目Go版本的转换规则。

Phase 4: Report

阶段4:生成报告

Print a summary table showing:
  • File: path relative to project root
  • Transformations applied: list of transformation names per file
  • Total files modified and total transformations applied
  • Skipped transformations (available but not applicable due to version constraints) and their required Go version
打印汇总表格,包含:
  • 文件:相对于项目根目录的路径
  • 已应用的转换规则:每个文件对应的转换规则列表
  • 修改的文件总数已应用的转换规则总数
  • 跳过的转换规则(可用但因版本限制无法应用)及其所需的Go版本

Example Summary Output

汇总输出示例

undefined
undefined

Modernization Summary

现代化汇总

FileTransformations
main.goany, strings.Cut, min/max (2 occurrences)
pkg/handler.gorange over int (3), slices.Contains, t.Context()
pkg/util.gonew(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)
undefined
文件转换规则
main.goany, strings.Cut, min/max(2处)
pkg/handler.go遍历整数(3处), slices.Contains, t.Context()
pkg/util.gonew(expr)(1处), errors.AsType → errors.Is
修改了3个文件,应用了10项转换规则
跳过的规则(需要更高Go版本):
  • new(expr): 需要go 1.26(当前项目为go 1.24)
  • WaitGroup.Go: 需要go 1.25(当前项目为go 1.24)
undefined

Safety Rules

安全规则

  • Never apply transformations that change semantics in edge cases without the user's awareness.
  • Do not apply
    omitzero
    blindly—it changes JSON serialization behavior; flag it as a suggestion instead.
  • Do not apply
    strings.SplitSeq
    or
    bytes.SplitSeq
    when the loop body references the index or the full slice elsewhere.
  • Do not apply
    strings.Builder
    if the concatenation happens outside a loop (single
    +=
    is fine).
  • When a transformation requires a new import, ensure the import is added to the file.
  • After all edits, run
    goimports -w
    on each modified file to clean up imports.
  • If
    goimports
    is not available, fall back to
    gofmt -w
    .
  • If the project has no
    go.mod
    , ask the user for the target Go version before proceeding.
  • 绝不在用户不知情的情况下应用会改变边缘情况语义的转换规则。
  • 不要盲目应用
    omitzero
    ——它会改变JSON序列化行为;应将其作为建议提出。
  • 当循环体在其他地方引用索引或完整切片时,不要应用
    strings.SplitSeq
    bytes.SplitSeq
  • 如果拼接操作发生在循环外部(单次
    +=
    ),不要应用
    strings.Builder
    规则。
  • 当转换规则需要新增导入时,确保将导入语句添加到文件中。
  • 所有编辑完成后,对每个修改后的文件运行
    goimports -w
    来整理导入语句。
  • 如果
    goimports
    不可用,回退到
    gofmt -w
  • 如果项目没有
    go.mod
    文件,在继续之前询问用户目标Go版本。