不能直接用 sync.pool 存 string,因为 string 是只读结构体,无法重置,复用无意义;应池化 strings.builder 或 []byte,使用前调 reset(),用后放回,确保每次使用均为干净状态。

为什么不能直接用 sync.Pool 存 string?
因为 string 在 Go 里是只读的底层结构体(包含指针和长度),sync.Pool 的设计目标是复用可重置的、带状态的对象,而 string 本身无法“清空”或“重置”。你往池里放一个 string,取出来还是那个不可变值,毫无复用意义。
真正要复用的是拼接过程中的可变缓冲区,比如 []byte 或 strings.Builder。
-
strings.Builder内部持有一个可增长的[]byte,调用Reset()可清空内容并保留底层数组,适合池化 - 直接池化
[]byte也行,但需手动管理容量、避免越界,风险更高 - 别用
bytes.Buffer—— 它的Reset()不释放底层数组内存(直到 GC),且方法多、开销略大
sync.Pool 配置 strings.Builder 的正确写法
关键在 New 函数:必须返回一个已初始化、可立即使用的 strings.Builder 实例;取出来后必须调用 Reset(),否则旧数据会残留。
var builderPool = &sync.Pool{
New: func() interface{} {
return new(strings.Builder)
},
}
func getBuilder() *strings.Builder {
return builderPool.Get().(*strings.Builder)
}
func putBuilder(b *strings.Builder) {
b.Reset()
builderPool.Put(b)
}
- 不要在
New里预分配容量(如strings.Builder{cap: 1024})——strings.Builder没有公开的cap字段,只能靠Grow()控制 - 如果知道常见拼接长度,可在首次写入前调用
b.Grow(512),减少扩容次数 - 务必在每次使用后
putBuilder(),否则池子会漏对象,GC 压力上升
拼接场景下性能差异明显吗?
取决于拼接频率和单次长度。低频小字符串(比如日志中拼几个字段)用 + 或 fmt.Sprintf 更简单、无心智负担;高频或长字符串拼接(如模板渲染、序列化中间结果)才值得引入 sync.Pool。
- 实测:10 万次拼接 100 字节字符串,
strings.Builder+ 池比+快约 3–4 倍,GC 分配次数下降 90%+ - 但池本身有锁开销,单 goroutine 场景几乎没收益,甚至略慢
- 注意:如果拼接结果立刻转成
string并丢弃,Builder.String()会触发一次底层数组拷贝,这部分无法避免
容易被忽略的生命周期陷阱
sync.Pool 不保证对象存活时间,GC 可能在任意时刻清理池中对象。这意味着:
- 绝不能把从池里取出的
strings.Builder保存在全局变量、闭包或结构体字段里——下次取可能已是脏数据或 panic - 不能跨 goroutine 传递未
Reset()的 builder:A goroutine 放回去前没 reset,B goroutine 取出来直接用,会拼出 A 留下的旧内容 - 池中对象可能被复用多次,但每次使用都必须视为“全新”,依赖初始为空的状态
最安全的模式就是:取 → 用 → reset → 放回,一气呵成,不跨函数边界持有。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











