modern-go
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesemodern-go
modern-go
Modernize Go source code by applying version-appropriate idioms, APIs, and language features. Works like plus additional transformations curated from the Go team's modernize analysis passes and community best practices.
go fix通过应用适配对应版本的惯用写法、API和语言特性来现代化Go源代码。功能类似,并额外包含了Go团队现代化分析流程以及社区最佳实践中的转换规则。
go fixUsage
使用方法
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:
- Detect the project's Go version from (the
go.moddirective).go - Find all files in the target scope (excluding
.go,vendor/,.git/).testdata/ - For each file, apply all transformations for versions ≤ the project's Go version, starting from the oldest to the newest.
- 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代码时调用此技能。默认会现代化整个项目;用户也可以指定单个文件或目录。
调用时执行以下步骤:
- 从文件的
go.mod指令中检测项目的Go版本。go - 在目标范围内查找所有文件(排除
.go、vendor/、.git/目录)。testdata/ - 对每个文件,应用所有版本≤项目Go版本的转换规则,从最旧版本到最新版本依次执行。
- 完成所有转换后,打印变更内容和跳过项的汇总信息。
如果用户指定了文件或目录,则将范围限制在该路径内。
Transformation Catalog
转换规则目录
Each transformation includes a Go version gate—only apply when the project's version ≥ that version. Never apply a transformation that requires a version higher than the project declares.
go.mod每个转换规则都包含一个Go版本限制——仅当项目中声明的版本≥该版本时才会应用。绝不应用需要高于项目声明版本的转换规则。
go.modGo 1.0+ — time.Since
time.SinceGo 1.0+ — time.Since
time.Since| Before | After |
|---|---|
| |
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)| 转换前 | 转换后 |
|---|---|
| |
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)Go 1.8+ — time.Until
time.UntilGo 1.8+ — time.Until
time.Until| Before | After |
|---|---|
| |
go
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)| 转换前 | 转换后 |
|---|---|
| |
go
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)Go 1.10+ — strings.Builder
(loop concatenation)
strings.BuilderGo 1.10+ — strings.Builder
(循环拼接)
strings.Builder| Before | After |
|---|---|
| |
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.
+=| 转换前 | 转换后 |
|---|---|
循环中的 | |
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
errors.IsGo 1.13+ — errors.Is
errors.Is| Before | After |
|---|---|
| |
go
// before
if err == io.EOF {
return
}
// after
if errors.Is(err, io.EOF) {
return
}| 转换前 | 转换后 |
|---|---|
| |
go
// before
if err == io.EOF {
return
}
// after
if errors.Is(err, io.EOF) {
return
}Go 1.18+ — any
anyGo 1.18+ — any
any| Before | After |
|---|---|
| |
go
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }| 转换前 | 转换后 |
|---|---|
| |
go
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }Go 1.18+ — strings.Cut
strings.CutGo 1.18+ — strings.Cut
strings.Cut| Before | After |
|---|---|
| |
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
// 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
bytes.CutGo 1.18+ — bytes.Cut
bytes.Cut| Before | After |
|---|---|
| |
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
// 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
fmt.AppendfGo 1.19+ — fmt.Appendf
fmt.Appendf| Before | After |
|---|---|
| |
go
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)| 转换前 | 转换后 |
|---|---|
| |
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+ — 类型安全的原子操作
| Before | After |
|---|---|
| |
| |
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
// 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
strings.CloneGo 1.20+ — strings.Clone
strings.Clone| Before | After |
|---|---|
| |
go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)| 转换前 | 转换后 |
|---|---|
| |
go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)Go 1.20+ — bytes.Clone
bytes.CloneGo 1.20+ — bytes.Clone
bytes.Clone| Before | After |
|---|---|
| |
go
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := 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
strings.CutPrefixstrings.CutSuffixGo 1.20+ — strings.CutPrefix
/ strings.CutSuffix
strings.CutPrefixstrings.CutSuffix| Before | After |
|---|---|
| |
| |
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
// 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
errors.JoinGo 1.20+ — errors.Join
errors.Join| Before | After |
|---|---|
| |
go
// before
return fmt.Errorf("load config: %w: %w", err1, err2)
// after
return errors.Join(fmt.Errorf("load config"), 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
context.WithCancelCauseGo 1.20+ — context.WithCancelCause
context.WithCancelCause| Before | After |
|---|---|
| |
go
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()
// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdown| 转换前 | 转换后 |
|---|---|
| |
go
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()
// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdownGo 1.21+ — min
/ max
minmaxGo 1.21+ — min
/ max
minmax| Before | After |
|---|---|
| |
| |
| |
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
// 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
clearGo 1.21+ — clear
clear| Before | After |
|---|---|
| |
| |
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
// 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
slicesGo 1.21+ — slices
包
slices| Before | After |
|---|---|
| Manual loop to find element | |
| Loop returning index or -1 | |
| |
| Max/min finding loop | |
| Reverse swap loop | |
| Remove consecutive duplicates loop | |
| |
| |
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 -1go
// 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 and (for ).
"slices""cmp"SortFunc| 转换前 | 转换后 |
|---|---|
| 手动循环查找元素 | |
| 返回索引或-1的循环 | |
| |
| 查找最大值/最小值的循环 | |
| 反转交换循环 | |
| 移除连续重复项的循环 | |
| |
| |
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 -1go
// 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"SortFuncGo 1.21+ — maps
package
mapsGo 1.21+ — maps
包
maps| Before | After |
|---|---|
| Manual loop to copy a map | |
| |
| Loop + conditional delete | |
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 | |
| |
| 循环+条件删除 | |
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
sync.OnceFuncsync.OnceValueGo 1.21+ — sync.OnceFunc
/ sync.OnceValue
sync.OnceFuncsync.OnceValue| Before | After |
|---|---|
| |
| |
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
// 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
context.AfterFuncGo 1.21+ — context.AfterFunc
context.AfterFunc| Before | After |
|---|---|
| |
go
// before
go func() {
<-ctx.Done()
conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })| 转换前 | 转换后 |
|---|---|
| |
go
// before
go func() {
<-ctx.Done()
conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })Go 1.21+ — context.WithTimeoutCause
/ WithDeadlineCause
context.WithTimeoutCauseWithDeadlineCauseGo 1.21+ — context.WithTimeoutCause
/ WithDeadlineCause
context.WithTimeoutCauseWithDeadlineCause| Before | After |
|---|---|
| |
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.
| 转换前 | 转换后 |
|---|---|
| |
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+ — 遍历整数
| Before | After |
|---|---|
| |
| |
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
// 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+ — 移除循环变量遮蔽
| Before | After |
|---|---|
| |
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 capture idiom is redundant since Go 1.22.
x := x| 转换前 | 转换后 |
|---|---|
| |
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 := xGo 1.22+ — cmp.Or
cmp.OrGo 1.22+ — cmp.Or
cmp.Or| Before | After |
|---|---|
Chain of | |
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"| 转换前 | 转换后 |
|---|---|
链式 | |
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
reflect.TypeForGo 1.22+ — reflect.TypeFor
reflect.TypeFor| Before | After |
|---|---|
| |
go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()| 转换前 | 转换后 |
|---|---|
| |
go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()Go 1.22+ — Enhanced http.ServeMux
http.ServeMuxGo 1.22+ — 增强版http.ServeMux
http.ServeMux| Before | After |
|---|---|
| |
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
// 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+ — 迭代器辅助函数
| Before | After |
|---|---|
| |
| |
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 and .
"slices""maps"| 转换前 | 转换后 |
|---|---|
| |
| |
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
strings.SplitSeqstrings.FieldsSeqGo 1.23+ — strings.SplitSeq
/ strings.FieldsSeq
strings.SplitSeqstrings.FieldsSeq| Before | After |
|---|---|
| |
| |
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.
| 转换前 | 转换后 |
|---|---|
| |
| |
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
bytes.SplitSeqbytes.FieldsSeqGo 1.23+ — bytes.SplitSeq
/ bytes.FieldsSeq
bytes.SplitSeqbytes.FieldsSeq| Before | After |
|---|---|
| |
go
// before
for _, part := range bytes.Split(data, sep) {
process(part)
}
// after
for part := range bytes.SplitSeq(data, sep) {
process(part)
}| 转换前 | 转换后 |
|---|---|
| |
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
t.Context()Go 1.24+ — 测试中的t.Context()
t.Context()| Before | After |
|---|---|
| |
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
// 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
omitzeroGo 1.24+ — omitzero
结构体标签
omitzero| Before | After |
|---|---|
| |
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 fails: , , structs, slices, maps. Flag as suggestion, not auto-apply.
omitemptytime.Timetime.Duration| 转换前 | 转换后 |
|---|---|
| |
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"`
}仅适用于失效的类型:、、结构体、切片、map。此规则仅作为建议,不自动应用。
omitemptytime.Timetime.DurationGo 1.24+ — b.Loop()
in benchmarks
b.Loop()Go 1.24+ — 基准测试中的b.Loop()
b.Loop()| Before | After |
|---|---|
| |
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
// 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
sync.WaitGroup.GoGo 1.25+ — sync.WaitGroup.Go
sync.WaitGroup.Go| Before | After |
|---|---|
| |
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
// 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
newGo 1.26+ — 带表达式的new
new| Before | After |
|---|---|
| |
Helper | |
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
// 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
errors.AsTypeGo 1.26+ — errors.AsType
errors.AsType| Before | After |
|---|---|
| |
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)
}| 转换前 | 转换后 |
|---|---|
| |
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 to extract the Go version ( line). If no is found, default to .
go.modgo 1.xxgo.modgo 1.21读取文件提取Go版本(行)。如果未找到文件,默认使用。
go.modgo 1.xxgo.modgo 1.21Phase 2: Gather files
阶段2:收集文件
Find all files in the target scope (project root, or user-specified file/directory). Exclude , , and directories.
.govendor/.git/testdata/在目标范围内查找所有文件(项目根目录,或用户指定的文件/目录)。排除、和目录。
.govendor/.git/testdata/Phase 3: Apply transformations
阶段3:应用转换规则
For each file, apply all transformations for versions ≤ the detected Go version. Process files sequentially. For each file:
.go- Read the file content.
- Identify applicable transformations by scanning for the "Before" patterns.
- Apply each transformation using the Edit tool.
- Run (or
goimports -w) on the file after all edits.gofmt -w
Never apply a transformation that requires a version higher than the project's Go version.
对每个文件,应用所有版本≤检测到的Go版本的转换规则。按顺序处理文件。对每个文件执行以下步骤:
.go- 读取文件内容。
- 扫描「转换前」模式,识别适用的转换规则。
- 使用编辑工具应用每个转换规则。
- 所有编辑完成后,对文件运行(或
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
汇总输出示例
undefinedundefinedModernization 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)
undefined| 文件 | 转换规则 |
|---|---|
| main.go | any, strings.Cut, min/max(2处) |
| pkg/handler.go | 遍历整数(3处), slices.Contains, t.Context() |
| pkg/util.go | new(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)
undefinedSafety Rules
安全规则
- Never apply transformations that change semantics in edge cases without the user's awareness.
- Do not apply blindly—it changes JSON serialization behavior; flag it as a suggestion instead.
omitzero - Do not apply or
strings.SplitSeqwhen the loop body references the index or the full slice elsewhere.bytes.SplitSeq - Do not apply if the concatenation happens outside a loop (single
strings.Builderis fine).+= - When a transformation requires a new import, ensure the import is added to the file.
- After all edits, run on each modified file to clean up imports.
goimports -w - If is not available, fall back to
goimports.gofmt -w - If the project has no , ask the user for the target Go version before proceeding.
go.mod
- 绝不在用户不知情的情况下应用会改变边缘情况语义的转换规则。
- 不要盲目应用——它会改变JSON序列化行为;应将其作为建议提出。
omitzero - 当循环体在其他地方引用索引或完整切片时,不要应用或
strings.SplitSeq。bytes.SplitSeq - 如果拼接操作发生在循环外部(单次),不要应用
+=规则。strings.Builder - 当转换规则需要新增导入时,确保将导入语句添加到文件中。
- 所有编辑完成后,对每个修改后的文件运行来整理导入语句。
goimports -w - 如果不可用,回退到
goimports。gofmt -w - 如果项目没有文件,在继续之前询问用户目标Go版本。
go.mod