mcpField
M
modern-go

modern-go

github.com/smallnest开发296523 次安装6 周前更新
代码仓库

关于 modern-go

modern-go 是一项用于按项目 Go 版本更新 Go 源码惯用写法的技能,适合维护和现代化 Go 项目的开发者。它按版本门槛应用多种转换,并汇总修改与跳过的内容。

主要功能

  1. 01读取 go.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 源代码现代化。其功能类似于 go fix,并额外包含从 Go 团队的现代化分析器和社区最佳实践中精选的转换。

用法

当用户要求现代化 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 指针运算改写为 unsafe.Add / unsafe.Slice,编译器和 go vet 能将其识别为 GC 安全的写法。

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 包
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)

需要导入 "slices" 和 "cmp"(用于 SortFunc)。

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 包
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 在 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+ — 遍历整数范围
之前之后
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) }()
}

自 Go 1.22 起,x := x 捕获惯用写法已无必要。

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+ — 遍历函数(迭代器)
之前之后
自定义 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)
之前之后
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()
之前之后
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
之前之后
使用 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 令牌大小限制。请注意,返回的行会保留末尾的 \n,与 Scanner.Text() 不同;如果旧代码依赖去除换行符的行,请对其进行裁剪。仅适用于内存中的字符串或缓冲区,不适用于流式 io.Reader。

Go 1.24+ — os.Root(目录范围内的文件系统访问)
之前之后
手动使用 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 结构体标签
之前之后
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()
之前之后
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
之前之后
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
之前之后
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
之前之后
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)
之前之后
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 新增了上文提到的四项重要处理:

现代化工具最低 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 设置中排除相应分析器。

评论

支持 Markdown