登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go sync.Pool.New 怎么避免 Get 返回 nil

来源:17golang原创

时间:2026-10-06 23:16:39 466浏览 收藏

我第一次用 sync.Pool 缓存 bytes.Buffer 时,直接写了 pool.Get().(*bytes.Buffer)。只要池还是空的,这段代码就可能因为 Get 返回 nil 而触发类型断言 panic。解决办法很明确:在 Pool 对外使用前设置 New,并保证它每次都返回同一种非 nil 具体指针。

官方文档:https://pkg.go.dev/sync

要点速览
  • Get 在原本要返回 nil 且 New 非 nil 时,返回调用 New 的结果。
  • New 自己返回 nil 或 typed nil,调用方仍可能拿到不可用值。
  • Pool 中的对象可被运行时随时移除,不能靠预先 Put 一个对象来保证以后永不为空。

先把保证边界说清楚

sync.Pool 保存的是可临时复用的对象。官方文档明确说明:池内项目可能在没有通知的情况下自动移除,Get 也可能忽略已有对象,把池当成空池。因此,我不会把 Pool 当成“至少保留一个对象”的容器,也不会用它管理必须关闭、必须归还或创建可能失败的连接。

真正能避免空池返回 nil 的机制是 New:当 Get 否则将返回 nil,且 New 已设置时,Get 会调用它。这个保证还有一个前提——工厂返回值本身必须可用。

sync.Pool、New、Get、Put、调用方与临时对象之间的静态契约关系图
图1:Pool 配置、借用与归还边界的静态说明图,不是运行截图。

在并发使用前固定 New

下面是我现在常用的最小写法。工厂直接返回指针,既符合官方示例的建议,也让调用方能统一断言为 *bytes.Buffer:

package bufferpool

import (
	"bytes"
	"sync"
)

var buffers = sync.Pool{
	New: func() any {
		// 工厂必须返回非 nil,且返回类型要与 Get 处的断言一致。
		return new(bytes.Buffer)
	},
}

func acquire() *bytes.Buffer {
	// New 已在并发访问前固定,因此空池时也会得到可用指针。
	b := buffers.Get().(*bytes.Buffer)
	b.Reset()
	return b
}

New 不能在其他 goroutine 正调用 Get 时修改。工程里最好在包级变量初始化或构造阶段一次设置完成,之后只读。工厂还应保持无共享状态或自行保证并发安全,因为多个 goroutine 可能同时遇到池空。

把归还规则放进同一组函数

只设置 New 还不够。复用对象可能携带上一次调用留下的数据,甚至因为一次大请求扩容得很大。我倾向于把 Pool 保持为未导出变量,只暴露借用和归还函数:

const maxBufferCapacity = 64  maxBufferCapacity {
		return
	}

	// 清掉可见内容后再归还,下个调用方不会读到旧数据。
	b.Reset()
	buffers.Put(b)
}

这里的容量阈值只是策略示例,应按业务对象大小分布调整。关键不是固定数字,而是把“允许归还什么类型、归还前如何清理、什么对象直接丢弃”集中到一个入口。

typed nil 是最容易漏掉的陷阱

接口是否为 nil,取决于动态类型和值是否都为空。一个装进 any 的 nil 指针带有动态类型,因此接口本身不等于 nil,但断言后得到的指针仍然是 nil:

var bad = sync.Pool{
	New: func() any {
		// 错误示例:返回值接口带有 *bytes.Buffer 类型,但内部指针仍为 nil。
		var b *bytes.Buffer
		return b
	},
}

func badAcquire() *bytes.Buffer {
	// 类型断言会成功,但结果为 nil,后续调用 b.Reset() 仍会 panic。
	return bad.Get().(*bytes.Buffer)
}

因此不要只检查 New != nil,还要让工厂直接构造非 nil 值,例如 new(bytes.Buffer) 或 &bytes.Buffer{}。同样,不要把别的类型放进这个 Pool;否则下一次 Get 可能在类型断言处 panic。

sync.Pool 中非 nil 指针、typed nil、类型断言、Reset 与容量边界的静态关系图
图2:工厂返回值、类型边界与复用卫生的静态说明图,不是运行结果。

用测试冻结非 nil 契约

Pool 的目标不是保证复用命中,而是即使命中失败也能安全得到新对象。测试时不要断言两次 Get 必须拿到同一指针;应该断言借用结果始终非 nil、状态已清空,并确认并发调用不会暴露 nil:

package bufferpool

import (
	"sync"
	"testing"
)

func TestAcquireNeverReturnsNil(t *testing.T) {
	const workers = 32
	var wg sync.WaitGroup
	wg.Add(workers)

	for i := 0; i 

如果对象创建会返回错误,例如网络连接、文件句柄或外部资源,就不要把失败隐藏在 Pool.New 里。New 没有 error 返回位,这类资源需要显式构造、健康检查和关闭流程。

常见误区与推荐处理

做法问题推荐处理
只提前 Put 一个对象对象可能被自动移除,Get 也可能忽略池设置返回非 nil 值的 New
New 返回 typed nil接口非 nil,但断言后的指针仍为 nil直接 return new(T)
并发期间修改 New违反 Pool 的使用约束初始化时固定,之后不再修改
多个类型混用一个 Pool类型断言可能 panic每种类型独立封装 Pool
依赖复用命中率做正确性判断Pool 不保证保存或返回已 Put 对象把 Pool 仅作为性能优化

速查清单

  • New 在任何并发 Get 前设置完成。
  • New 返回固定类型的非 nil 指针。
  • Pool 未导出,外部不能混入其他类型。
  • acquire 统一类型断言与状态初始化。
  • release 负责 Reset、nil 过滤和容量上限。
  • 测试只验证公开契约,不假设对象一定被复用。

相关问题

设置了 New,Get 就一定复用旧对象吗?

不一定。Get 可以忽略池中对象,池内对象也可能被自动移除;New 只保证在原本要返回 nil 时提供一个新值。

New 可以返回结构体而不是指针吗?

语义上可以,但官方示例建议通常返回指针类型,因为指针装入接口值时可避免额外分配。更重要的是,全程保持返回与归还类型一致。

sync.Pool 适合数据库连接吗?

通常不适合。连接需要容量、健康检查、超时和关闭语义,而 sync.Pool 允许对象随时消失,更适合可丢弃、可重新创建的临时对象。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>