modern-go
通过应用适用于相应 Go 版本的惯用写法、API 和语言特性,使 Go 源代码现代化。其功能类似于 go fix,并额外包含从 Go 团队的现代化分析器和社区最佳实践中精选的转换。
用法
当用户要求现代化 Go 代码时,调用此技能。默认情况下,现代化整个项目;用户也可以指定文件或目录。
调用时:
- 从
go.mod(go 指令)检测项目的 Go 版本。
- 查找目标范围内的所有
.go 文件(排除 vendor/、.git/、testdata/)。
- 对每个文件,应用所有版本 ≤ 项目 Go 版本的转换,从最旧版本开始,依次处理到最新版本。
- 完成所有转换后,打印已更改和已跳过内容的摘要。
如果用户指定了文件或目录,则将范围限制为该路径。
转换目录
每项转换都设有 Go 版本门槛——仅当项目的 go.mod 版本 ≥ 该版本时才应用。绝不应用要求的版本高于项目声明版本的转换。
Go 1.0+ — time.Since
| 转换前 | 转换后 |
|---|
time.Now().Sub(start) | time.Since(start) |
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
Go 1.8+ — time.Until
| 转换前 | 转换后 |
|---|
deadline.Sub(time.Now()) | time.Until(deadline) |
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)
Go 1.10+ — strings.Builder(循环拼接)
| 转换前 | 转换后 |
|---|
循环中的 s += item | var b strings.Builder; b.WriteString(item) |
// before
s := ""
for _, item := range items {
s += item
}
// after
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
s := b.String()
仅当 += 拼接发生在循环内部时才应用。
Go 1.13+ — errors.Is
| 转换前 | 转换后 |
|---|
err == io.EOF | errors.Is(err, io.EOF) |
// before
if err == io.EOF {
return
}
// after
if errors.Is(err, io.EOF) {
return
}
Go 1.17+ — //go:build 约束(plusbuild)
| 转换前 | 转换后 |
|---|
// +build linux + //go:build linux(两者都存在) | 仅保留 //go:build linux |
// before
//go:build linux && amd64
// +build linux,amd64
package foo
// after
//go:build linux && amd64
package foo
plusbuild 现代化器会在存在等效的 //go:build 行时,移除已过时的 // +build 约束行(//go:build 语法于 Go 1.17 引入)。仅当存在匹配的 //go:build 行时才移除旧行——绝不删除唯一的约束。
Go 1.17+ — unsafe.Add / unsafe.Slice(unsafefuncs)
| 转换前 | 转换后 |
|---|
unsafe.Pointer(uintptr(ptr) + uintptr(n)) | unsafe.Add(ptr, n) |
(*[n]T)(unsafe.Pointer(p))[:] 切片构造 | unsafe.Slice(p, n) |
// before — pointer arithmetic via uintptr
p2 := unsafe.Pointer(uintptr(ptr) + uintptr(offset))
// after
p2 := unsafe.Add(ptr, offset)
// before — building a slice from a base pointer
s := (*[1 << 30]byte)(unsafe.Pointer(p))[:n:n]
// after
s := unsafe.Slice(p, n)
unsafefuncs 现代化器(gopls v0.22.0)会将容易出错的 uintptr 指针运算改写为 unsafe.Add / unsafe.Slice,编译器和 go vet 能将其识别为 GC 安全的写法。
Go 1.18+ — any
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }
Go 1.18+ — strings.Cut
| 转换前 | 转换后 |
|---|
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):] | key, val, found := strings.Cut(s, sep) |
// before
if i := strings.Index(s, "="); i >= 0 {
key, val := s[:i], s[i+1:]
}
// after
if key, val, found := strings.Cut(s, "="); found {
...
}
Go 1.18+ — bytes.Cut
| 转换前 | 转换后 |
|---|
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):] | before, after, found := bytes.Cut(b, sep) |
// before
if i := bytes.Index(b, sep); i >= 0 {
before, after := b[:i], b[i+len(sep):]
}
// after
before, after, found := bytes.Cut(b, sep)
Go 1.19+ — fmt.Appendf
| 转换前 | 转换后 |
|---|
buf = append(buf, fmt.Sprintf(...)...) | buf = fmt.Appendf(buf, ...) |
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)
Go 1.19+ — 类型安全的原子操作(atomictypes)
| 转换前 | 转换后 |
|---|
atomic.StoreInt32(&v, 1) / atomic.LoadInt32(&v) | var v atomic.Int32; v.Store(1); v.Load() |
atomic.AddInt64(&v, 1) | var v atomic.Int64; v.Add(1) |
atomic.Value + 类型断言 | atomic.Pointer[T] |
// before
var ready int32
atomic.StoreInt32(&ready, 1)
if atomic.LoadInt32(&ready) == 1 { ... }
// after
var ready atomic.Int32
ready.Store(1)
if ready.Load() == 1 { ... }
// before
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)
// after
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()
atomictypes 现代化器(gopls v0.22.0,AtomicTypesAnalyzer)会改写变量声明和每个调用点。类型化包装器(atomic.Int32/Int64/Uint32/Uint64/Bool/Pointer[T])的性能完全相同,但能防止意外进行非原子访问,并修复 32 位架构上的 64 位对齐崩溃问题。
Go 1.20+ — strings.Clone
| 转换前 | 转换后 |
|---|
string([]byte(s)) | strings.Clone(s) |
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)
Go 1.20+ — bytes.Clone
| Before | After |
|---|
make([]byte, len(src)); copy(dst, src) | bytes.Clone(src) |
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)
Go 1.20+ — strings.CutPrefix / strings.CutSuffix
| Before | After |
|---|
if strings.HasPrefix(s, p) { s = s[len(p):] } | if rest, ok := strings.CutPrefix(s, p); ok { s = rest } |
if strings.HasSuffix(s, sf) { s = s[:len(s)-len(sf)] } | if rest, ok := strings.CutSuffix(s, sf); ok { s = rest } |
// before
if strings.HasPrefix(s, "pre_") {
s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
s = rest
}
// before
if strings.HasSuffix(s, ".txt") {
s = s[:len(s)-len(".txt")]
}
// after
if rest, ok := strings.CutSuffix(s, ".txt"); ok {
s = rest
}
Go 1.20+ — errors.Join
| Before | After |
|---|
fmt.Errorf("...: %w: %w", err1, err2) | errors.Join(err1, err2) |
// before
return fmt.Errorf("load config: %w: %w", err1, err2)
// after
return errors.Join(fmt.Errorf("load config"), err1, err2)
Go 1.20+ — context.WithCancelCause
| Before | After |
|---|
ctx, cancel := context.WithCancel(parent) + bare cancel() | ctx, cancel := context.WithCancelCause(parent) + cancel(err) |
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()
// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdown
Go 1.21+ — min / max
| Before | After |
|---|
if a < b { v = a } else { v = b } | v = min(a, b) |
if a > b { v = a } else { v = b } | v = max(a, b) |
if x < lo { x = lo }; if x > hi { x = hi } | x = min(max(x, lo), hi) |
// before
lo := a
if b < lo {
lo = b
}
// after
lo := min(a, b)
// before
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
// after
x = min(max(x, 0), 100)
Go 1.21+ — clear
| Before | After |
|---|
for k := range m { delete(m, k) } | clear(m) |
for i := range s { s[i] = zero } | clear(s) |
// before
for k := range m {
delete(m, k)
}
// after
clear(m)
// before
for i := range s {
s[i] = 0
}
// after
clear(s)
Go 1.21+ — slices 包
| Before | After |
|---|
| 手动循环查找元素 | slices.Contains(items, target) |
| 返回索引或 -1 的循环 | slices.Index(items, target) |
sort.Slice(items, func(i,j int) bool { return items[i] < items[j] }) | slices.SortFunc(items, cmp.Compare) |
| 查找最大值/最小值的循环 | slices.Max(items) / slices.Min(items) |
| 反转交换循环 | slices.Reverse(s) |
| 删除连续重复项的循环 | slices.Compact(s) |
s[:len(s):len(s)] | slices.Clip(s) |
make([]T, len(src)); copy(dst, src) | slices.Clone(src) |
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
if x == target {
found = true
break
}
}
// before → after: slices.Index(items, target)
for i, x := range items {
if x == target {
return i
}
}
return -1
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
if v > max {
max = v
}
}
// before → after: slices.Reverse(s)
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
// before → after: slices.Compact(s)
i := 0
for j := 1; j < len(s); j++ {
if s[j] != s[i] {
i++
s[i] = s[j]
}
}
s = s[:i+1]
// before → after: slices.Clip(s)
s = s[:len(s):len(s)]
// before → after: slices.Clone(src)
dst := make([]T, len(src))
copy(dst, src)
需要导入 "slices" 和 "cmp"(用于 SortFunc)。
Go 1.21+ — slices.Delete / slices.Insert
| Before | After |
|---|
append(s[:i], s[i+1:]...) | slices.Delete(s, i, i+1) |
append(s[:i:i], append([]T{x}, s[i:]...)...) | slices.Insert(s, i, x) |
// before — element removal (classic aliasing/leak footgun)
s = append(s[:i], s[i+1:]...)
// after
s = slices.Delete(s, i, i+1)
// before — insert at index i
s = append(s[:i], append([]T{v}, s[i:]...)...)
// after
s = slices.Insert(s, i, v)
slices.Delete 会将尾部元素置零,以避免保留指针(手动使用 append 的写法会导致泄漏)。需要导入 "slices"。
Go 1.21+ — slices.Equal / maps.Equal
| Before | After |
|---|
对可比较切片使用 reflect.DeepEqual(a, b) | slices.Equal(a, b) |
对可比较映射使用 reflect.DeepEqual(m1, m2) | maps.Equal(m1, m2) |
// before
if reflect.DeepEqual(got, want) { ... } // got, want are []string
// after
if slices.Equal(got, want) { ... }
速度更快且类型安全,无需反射。仅适用于元素类型可直接比较的情况(否则使用 slices.EqualFunc / maps.EqualFunc)。需要导入 "slices" 或 "maps"。
Go 1.21+ — maps 包
| Before | After |
|---|
| 手动循环复制映射 | maps.Clone(m) |
for k, v := range src { dst[k] = v } | maps.Copy(dst, src) |
| 循环 + 条件删除 | maps.DeleteFunc(m, predicate) |
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
dst[k] = v
}
// before → after: maps.Copy(dst, src)
for k, v := range src {
dst[k] = v
}
// before → after: maps.DeleteFunc(m, func(k K, v V) bool { return v == 0 })
for k, v := range m {
if v == 0 {
delete(m, k)
}
}
需要导入 "maps"。
Go 1.22+ — slices.Concat(appendclipped)
| Before | After |
|---|
append(append([]T(nil), s1...), s2...) | slices.Concat(s1, s2) |
对于生成新结果的 append(slices.Clip(s1), s2...) | slices.Concat(s1, s2) |
// before
all := append(append([]int(nil), a...), b...)
// after
all := slices.Concat(a, b)
// before — three-way concat
merged := append(append(append([]string(nil), x...), y...), z...)
// after
merged := slices.Concat(x, y, z)
appendclipped 现代化工具会将多个切片的嵌套 append 拼接替换为 slices.Concat,后者会分配一个大小恰当的新结果。slices.Concat 在 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; &v | new(val) |
辅助函数 func ptr[T any](v T) *T { return &v } | 直接使用 new(val) |
// before
timeout := 30
debug := true
cfg := Config{
Timeout: &timeout,
Debug: &debug,
}
// after
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
// before
func ptr[T any](v T) *T { return &v }
cfg := Config{Count: ptr(10)}
// after
cfg := Config{Count: new(10)}
Go 1.26+ — errors.AsType
| 之前 | 之后 |
|---|
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 版本及更早版本的转换。按顺序处理文件。对于每个文件:
- 读取文件内容。
- 扫描“Before”模式,找出适用的转换。
- 使用 Edit 工具应用每项转换。
- 完成所有编辑后,对文件运行
goimports -w(或 gofmt -w)。
切勿应用所需版本高于项目 Go 版本的转换。
阶段 4:报告
打印一张摘要表,显示:
- 文件:相对于项目根目录的路径
- 已应用的转换:每个文件应用的转换名称列表
- 已修改文件总数和已应用转换总数
- 跳过的转换(可用但因版本限制而不适用)及其所需的 Go 版本
摘要输出示例
## Modernization Summary
| File | Transformations |
|---|---|
| main.go | any, strings.Cut, min/max (2 occurrences) |
| pkg/handler.go | range over int (3), slices.Contains, t.Context() |
| pkg/util.go | new(expr) (1), errors.AsType → errors.Is |
**3 files modified, 10 transformations applied**
Skipped (requires higher Go version):
- new(expr): requires go 1.26 (project is go 1.24)
- WaitGroup.Go: requires go 1.25 (project is go 1.24)
安全规则
- 切勿应用会在边缘情况下改变语义、但未告知用户的转换。
- 不要盲目应用
omitzero——它会改变 JSON 序列化行为;应将其标记为建议。
- 将语义迁移视为建议,而非自动应用:
math/rand/v2(会改变随机数流)、iter.Seq 迭代器(会重塑 API)、os.Root(会改变行为和错误路径)。指出这些迁移,让用户选择是否采用。
- 将
bufio.Scanner 循环替换为 strings.Lines/bytes.Lines 时,请记住生成的行会保留末尾的 \n;如果旧代码依赖 Scanner.Text() 的语义,请添加 TrimSuffix。
- 如果循环体引用了索引或其他位置使用了完整切片,则不要应用
strings.SplitSeq 或 bytes.SplitSeq。
- 如果拼接发生在循环外(单次
+= 没问题),则不要应用 strings.Builder。
- 如果转换需要新的导入,请确保将其添加到文件中。
- 完成所有编辑后,对每个已修改文件运行
goimports -w 以清理导入。
- 如果
goimports 不可用,则改用 gofmt -w。
- 如果项目没有
go.mod,请在继续之前询问用户目标 Go 版本。
自动化工具(Go 1.27+)
这些转换中有许多现已作为官方现代化工具包含在 gopls/go fix 中。在 Go 1.27+ 中,可以通过以下命令将整套转换应用于整个模块:
go fix ./...
gopls v0.22.0 新增了上文提到的四项重要处理:
| 现代化工具 | 最低 Go 版本 | 转换 |
|---|
unsafefuncs | 1.17 | uintptr 指针运算 → unsafe.Add / unsafe.Slice |
atomictypes | 1.19 | 基本类型的 atomic.* 函数 → 类型化的 atomic.Int32/Pointer[T] 封装类型 |
slicesbackward | 1.23 | 降序索引循环 → slices.Backward 迭代器 |
embedlit | 1.27 | 冗余的嵌入字段字面量 → 提升字段初始化 |
该套件中的其他现代化工具也涵盖在本目录中:minmax、efaceany、fmtappendf、stringscut、stringsseq、sortslice/slicescontains、mapsloop、stditerators、forvar、rangeint、testingcontext、bloop、waitgroup、newexpr、errorsastype、appendclipped(→ slices.Concat)以及 plusbuild(→ 删除过时的 // +build)。
要禁用过于激进的处理(例如 slicesbackward 重写会修改切片的循环),请通过 -fixes 限定运行范围,或在编辑器的 gopls 设置中排除相应分析器。