别再用 2018 年的方式写 Go 了——那些定义了今天地道 Go 代码的 before/after 写法
现代 Go 惯用法:各版本演进中你应该知道的写法 JetBrains 最近发布了 go-modern-guidelines ,一套专门为 AI 代码助手设计的规范,让它们能根据项目的实际 Go 版本生成地道代码。这个动机很真实:同一段逻辑,在 Go 1.13、Go 1.21 和 Go 1.24 中,地道的写法确实不同。如果你的 AI(或者你的同事)不知道目标版本,写出来的代码虽然能编译,但已经悄悄过时了。
这篇文章以before/after 对比 为核心,按主题整理了各版本最重要的写法演进。如果你写 Go 已经有几年了,有些你可能见过,另一些说不定会让你惊讶。
🔑 any 别名:一个小改动,一个大信号(1.18) Go 1.18 带来了泛型——Go 历史上最重大的语言新增——但同时也悄悄引入了一个对日常代码立竿见影的变化:any 作为 interface{} 的内置别名。
1 2 3 4 5 6 7 func Print (v interface {}) { fmt.Println(v) }var cache map [string ]interface {}func Print (v any) { fmt.Println(v) }var cache map [string ]any
注意:any 是真正的类型别名,interface{} 和 any 在类型层面完全可互换,无需迁移,只是改变习惯。现代 Go 代码库和标准库本身都已全面采用 any。
⚠️ 错误处理在进化 错误处理是 Go 地道写法变化最明显的领域之一。
Go 1.13 — 停止用 == 比较错误
errors.Is 出现之前,检查特定错误用的是直接比较,一旦错误被包装就会失效:
1 2 3 4 5 6 7 8 9 if err == os.ErrNotExist { } if errors.Is(err, os.ErrNotExist) { }
errors.Is 会自动解包错误链,任何用 fmt.Errorf("context: %w", err) 包装的错误都能正确检查。
Go 1.20 — 合并多个错误
errors.Join 之前,聚合多个错误要么只保留第一个,要么写自定义类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func validate (cfg Config) error { var errs []error if cfg.Host == "" { errs = append (errs, errors.New("missing host" )) } if cfg.Port == 0 { errs = append (errs, errors.New("missing port" )) } if len (errs) > 0 { return errs[0 ] } return nil } func validate (cfg Config) error { var errs []error if cfg.Host == "" { errs = append (errs, errors.New("missing host" )) } if cfg.Port == 0 { errs = append (errs, errors.New("missing port" )) } return errors.Join(errs...) }
Go 1.26 — 类型断言错误,不再需要临时变量
1 2 3 4 5 6 7 8 9 10 var pathErr *os.PathErrorif errors.As(err, &pathErr) { fmt.Println(pathErr.Path) } if pathErr, ok := errors.AsType[*os.PathError](err); ok { fmt.Println(pathErr.Path) }
✂️ 字符串与字节操作 Go 1.18 — strings.Cut 取代 Index+切片
“在分隔符处切分字符串,取前半或后半”是 Go 代码里最常见的手写操作之一,strings.Cut 把它变成一次调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 s := "user:password" idx := strings.Index(s, ":" ) if idx >= 0 { user, pass := s[:idx], s[idx+1 :] _, _ = user, pass } user, pass, found := strings.Cut(s, ":" ) if found { _, _ = user, pass }
bytes.Cut 对字节切片提供完全相同的 API。
Go 1.20 — 按已知前后缀裁剪
1 2 3 4 5 6 7 8 9 10 if strings.HasPrefix(s, "Bearer " ) { token := strings.TrimPrefix(s, "Bearer " ) _ = token } if token, ok := strings.CutPrefix(s, "Bearer " ); ok { _ = token }
Go 1.24 — 迭代分割结果,不分配中间切片
1 2 3 4 5 6 7 8 9 for _, part := range strings.Split(csv, "," ) { process(part) } for part := range strings.SplitSeq(csv, "," ) { process(part) }
strings.FieldsSeq、bytes.SplitSeq、bytes.FieldsSeq 遵循同样的模式。
🔢 内置人体工学:min、max、clear 与 range Go 1.21 — min、max、clear 成为内置函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func maxInt (a, b int ) int { if a > b { return a } return b } smallest := math.MinInt for _, v := range values { if v < smallest { smallest = v } }smallest := min(values...) largest := max(a, b, c) for k := range m { delete (m, k) } clear(m)
Go 1.22 — range 整数
1 2 3 4 5 6 7 8 9 for i := 0 ; i < 10 ; i++ { fmt.Println(i) } for i := range 10 { fmt.Println(i) }
Go 1.22 — 循环变量捕获 bug 被修复
这是 Go 历史上最臭名昭著的陷阱,几乎每个 Go 开发者都踩过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for _, item := range items { go func () { process(item) }() } for _, item := range items { item := item go func () { process(item) }() } for _, item := range items { go func () { process(item) }() }
注意:此变化仅适用于 go.mod 中 go 1.22 或更高版本的代码,旧代码行为保持不变。
📦 slices 与 maps 标准库包 Go 1.21 将 slices 和 maps 升入标准库,取代了大量手写的工具循环。
常用切片操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func contains (items []string , x string ) bool { for _, v := range items { if v == x { return true } } return false } sort.Slice(items, func (i, j int ) bool { return items[i].Name < items[j].Name }) slices.Contains(items, x) slices.Index(items, x) slices.SortFunc(items, func (a, b Item) int { return cmp.Compare(a.Name, b.Name) }) slices.Reverse(items) slices.Compact(items) clone := slices.Clone(items)
Map 工具
1 2 3 4 5 6 7 8 clone := make (map [string ]int , len (m)) for k, v := range m { clone[k] = v }clone := maps.Clone(m) maps.Copy(dst, src) maps.DeleteFunc(m, func (k string , v int ) bool { return v == 0 })
Go 1.22 — cmp.Or 用于”取第一个非零值”的回退链
1 2 3 4 5 6 7 name := os.Getenv("APP_NAME" ) if name == "" { name = cfg.Name }if name == "" { name = "default" }name := cmp.Or(os.Getenv("APP_NAME" ), cfg.Name, "default" )
Go 1.23 — maps.Keys 和 maps.Values 返回迭代器
1 2 3 4 5 6 7 8 9 10 11 12 13 keys := make ([]string , 0 , len (m)) for k := range m { keys = append (keys, k) }sort.Strings(keys) keys := slices.Collect(maps.Keys(m)) sortedKeys := slices.Sorted(maps.Keys(m)) for k := range maps.Keys(m) { process(k) }
⚙️ 并发原语 Go 1.19 — 类型安全的原子操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var flag int32 atomic.StoreInt32(&flag, 1 ) if atomic.LoadInt32(&flag) == 1 { ... }var flag atomic.Boolflag.Store(true ) if flag.Load() { ... }var counter atomic.Int64counter.Add(1 ) var cfg atomic.Pointer[Config]cfg.Store(newConfig) current := cfg.Load()
Go 1.20 — 带原因的取消
1 2 3 4 5 6 7 8 9 10 11 ctx, cancel := context.WithCancel(parent) defer cancel()ctx, cancel := context.WithCancelCause(parent) cancel(ErrShutdownRequested) if cause := context.Cause(ctx); cause != nil { log.Printf("cancelled because: %v" , cause) }
Go 1.21 — sync.OnceFunc 和 sync.OnceValue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var ( once sync.Once instance *Service ) func getInstance () *Service { once.Do(func () { instance = newService() }) return instance } getInstance := sync.OnceValue(func () *Service { return newService() })
Go 1.25 — wg.Go 取代 Add/Done 三件套
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var wg sync.WaitGroupfor _, item := range items { wg.Add(1 ) go func () { defer wg.Done() process(item) }() } wg.Wait() var wg sync.WaitGroupfor _, item := range items { wg.Go(func () { process(item) }) } wg.Wait()
🧪 测试与基准测试 Go 1.24 — 测试中用 t.Context()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func TestFetchUser (t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() user, err := fetchUser(ctx, 42 ) _ = user; _ = err } func TestFetchUser (t *testing.T) { ctx := t.Context() user, err := fetchUser(ctx, 42 ) _ = user; _ = err }
Go 1.24 — 基准测试用 b.Loop()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func BenchmarkEncode (b *testing.B) { data := generateData() for i := 0 ; i < b.N; i++ { encode(data) } } func BenchmarkEncode (b *testing.B) { data := generateData() for b.Loop() { encode(data) } }
Go 1.24 — JSON struct tag 用 omitzero
1 2 3 4 5 6 7 8 9 10 11 type Event struct { Timeout time.Duration `json:"timeout,omitempty"` At time.Time `json:"at,omitempty"` } type Event struct { Timeout time.Duration `json:"timeout,omitzero"` At time.Time `json:"at,omitzero"` }
🔑 Go 1.26:消灭”取指针必须临时变量”的样板 Go API 设计里最常见的抱怨之一:想传一个字面值的指针,new 只接受类型,不接受表达式,只能先声明一个临时变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 timeout := 30 * time.Second debug := true cfg := Config{ Timeout: &timeout, Debug: &debug, } cfg := Config{ Timeout: new (30 * time.Second), Debug: new (true ), }
✅ 总结 翻完 JetBrains 的 go-modern-guidelines,你会发现一个规律:Go 不是在增加复杂度,而是在消除样板代码 。每个大版本都把开发者之前手写的东西提升为更简洁的内置形式。
strings.Cut 消灭了 Index 下标算术;errors.Join 消灭了手动聚合错误;slices.Contains 消灭了搜索循环;wg.Go 消灭了 Add/Done 仪式感。每一处,旧代码依然可以编译——但新写法更短、更不容易出错、意图更清晰。
实际建议:对照你的 go.mod 版本,检查项目里是否还在用这份清单上的旧写法。每改一处,都是可读性的提升和 bug 面的收窄。
你最没想到的是哪个变化?还有哪些你一直在用旧写法写、这里却没提到的场景?欢迎在评论区分享!
评论