Golang高性能代码应遵循哪些原则

Go高性能代码的核心是让编译器和运行时“省力”:少分配、少逃逸、少同步、少抽象;高频路径预分配切片、复用sync.Pool、避免隐式分配、小结构体传值、合理选用map/slice。

Go 高性能代码不是靠堆砌并发或炫技实现的,而是由一连串克制、可验证的选择构成:少分配、少逃逸、少同步、少抽象。核心原则就一条:让编译器和运行时尽可能“省力”

避免不必要的内存分配

每次 makenew、字符串拼接(+)、fmt.Sprintf 都可能触发堆分配,而 GC 压力会直接拖慢吞吐。高频路径上尤其敏感。

  • 循环内不要反复 make([]int, 10),改用预分配后复用:s := make([]int, 0, 10),再用 s = s[:0] 清空
  • JSON 解析等场景,优先用 sync.Pool 复用 bytes.Buffer 或结构体指针,而非每次都 &MyStruct{}
  • 避免隐式分配:返回局部切片(如 return data[10:20])可能造成底层数组逃逸;若原切片生命周期短,考虑复制:copy(dst, src[10:20])

慎用指针传递大值类型

Go 中 slice、map、chan、func 本身已是引用类型,传值开销极小;但传 *[]T*struct{...} 反而可能引发额外解引用、阻碍内联,甚至导致逃逸分析失败。

  • Bad:func process(s *[]byte) { ... } —— 强制分配堆内存存储 slice 头
  • Good:func process(s []byte) { ... } —— slice 是三个字长的值,栈上传递高效
  • 结构体小于 32 字节(如 type Point struct{ X, Y int }),通常建议传值;大于则按需传指针,但要确认是否真需要修改原值

用对数据结构,别为“看起来整洁”牺牲性能

map 查找是 O(1),slice 查找是 O(n),但 map 的内存占用、哈希计算、扩容成本远高于 slice。选型必须结合访问模式和数据规模。

  • 键集固定且数量少([]struct{key string; value int} + 线性查找更省内存、缓存友好
  • 需要频繁增删 key?map[string]int 合理;但只读场景下,考虑预排序 + sort.SearchStrings 二分查找,避免 map 的哈希扰动
  • 别用 map[interface{}]interface{} 存简单键值——类型断言开销大,且无法静态推导,易逃逸

goroutine 不是银弹,控制并发粒度比“开一堆”更重要

每 goroutine 至少占用 2KB 栈空间,调度器需维护其状态。无节制启动会导致 GOMAXPROCS 竞争、上下文切换飙升、GC 扫描压力倍增。

  • 避免在循环里直接 go doWork(i) —— i 可能被所有 goroutine 共享(经典闭包捕获 bug),且无并发上限
  • 用 worker pool 模式:固定数量 goroutine 消费带缓冲的 chan,例如 jobs := make(chan Task, 128)
  • I/O 密集任务(如 HTTP 请求)可适度放宽并发数;CPU 密集任务应严格限制,通常 ≤ GOMAXPROCS

最容易被忽略的一点是:**性能优化必须以 profile 为前提**。不跑 go test -benchmem、不看 pprof 的火焰图,所有“我觉得这里慢”的判断都是猜测。一个 sync.Pool 用错位置,可能比不用还慢。