sync.Pool适用于中大型、高频创建且可复位的对象(如bytes.Buffer、[]byte),不适用于小结构体、值类型或不可变对象;误用会导致性能下降或竞态问题。

sync.Pool 什么时候该用、什么时候不该用
不是所有对象都值得塞进 sync.Pool。小结构体(比如 struct{a, b int})、值类型(int、time.Time、string)或短生命周期对象,复用开销反而高于分配成本。Go 编译器对它们做了逃逸优化,栈分配比池化更快更轻量。
真正适合池化的,是那些:中大型(>1KB)、高频创建(如 HTTP 中间件每请求一次)、内部含可复位状态的对象。典型例子:*bytes.Buffer、预分配的 []byte、带大字段的结构体(如含 []byte 或 map[string]string 的日志 entry)。
- 常见误用:把
fmt.Sprintf返回的string放进池——string不可变,无法 Reset,池化无效 - 常见误用:为
strconv.Itoa结果建池——底层[]byte已由运行时管理,手动干预无意义 - 验证方式:用
go build -gcflags="-m -l"看 New 函数里是否出现escapes to heap;没逃逸就别池化
为什么不能直接池化 []byte 而必须用 *[]byte
[]byte 是值类型,包含三个字段(ptr、len、cap)。直接 Put 一个 []byte 值,等于每次存一份新拷贝的切片头;Get 后修改 len 或追加数据,不会影响池中原始副本,导致复用失效,甚至多个 goroutine 拿到同一底层数组的不同切片头,引发竞态写。
正确做法是池化 *[]byte:
立即学习“go语言免费学习笔记(深入)”;
-
New函数返回&make([]byte, 0, 1024) - Get 后解引用:
b := bytePool.Get().(*[]byte); buf := *b - 用完前必须重置长度:
*b = (*b)[:0](不是nil,也不是truncate) - 不这么做,下次 Get 到的
buf可能残留旧数据,或者len非零,append 会覆盖或叠加
bytes.Buffer 和 []byte 选哪个?Reset 和 [:0] 怎么用才安全
两者都可行,但适用场景不同:
- 用
*bytes.Buffer:适合追加为主、长度难预估的场景(如日志拼接、HTTP body 构建),调用buf.Reset()清空语义明确,且自动管理底层数组增长 - 用
*[]byte:适合长度可预估、后续要转string或直接传给 syscall 的场景,更轻量,避免Buffer的额外字段和方法调用开销 -
buf.Reset()必须在Get()后立即调用——buf.Truncate(0)不够,它不清 cap 外残留,也不释放引用 - 对
[]byte,b = b[:0]是唯一安全清空方式;b = nil或b = make([]byte, 0)都会丢失底层数组引用,等于放弃复用
Put 前忘了 Reset 就 panic?Get 后不检查 nil 的真实代价
sync.Pool.Get() 可能返回 nil(比如 GC 清空后首次 Get,或池为空且未设 New)。不检查直接类型断言会 panic。
更隐蔽的问题是状态污染:从池里取出的 *bytes.Buffer 可能已写入 5KB 数据,你没 Reset() 就直接 WriteString("hello"),结果是 "xxxhello";同理,[]byte 的 len 可能是 8192,append 会从第 8193 位开始写,越界或覆盖。
- 必须把初始化逻辑写死在 Get 后:先断言,再判空,再 Reset 或截断
- Put 前务必确保对象处于干净状态——
buf.Reset()或*b = (*b)[:0]要在Put()之前执行,否则脏数据被放回池,污染所有人 - 如果结构体字段含
[]byte,Put 前要显式重置:s.Data = s.Data[:0],不是s.Data = nil
真正容易被忽略的是:sync.Pool 对象随时可能被 GC 清空,你无法控制其存活时间;而复用的前提是“对象可被安全重置”,这要求你对每个字段的状态归零有完全掌控——哪怕一个指针字段没清,都可能造成内存泄漏或数据错乱。


















