为何最终我放弃了 Go 的 sync.Pool

一、使用场景

一句话总结:保存和复用临时对象,减少内存分配,降低GC压力

1.1、引入:

举个简单的例子:

go 复制代码
type User struct {
    ID       int64  `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Profile  [512]byte `json:"profile_data"` // 简介
}

var buf, _ = json.Marshal(
		User{
			ID: 1, 
			Username: "john_doe", 
			Email: "john@example.com",
		   },
		)

 user := &User{}
 json.Unmarshal(buf, user)

AI写代码
1234567891011121314151617

json的[反序列化]在文本解析网络通信中非常常见,当程序并发度非常高的情况下,

短时间内需要创建大量临时对象。而这些临时对象都是分配在堆上的,会给GC造成很大的压力,严重影响程序的性能。

所以可以通过sync.Pool来解决。

1.2、什么是sync.pool?

Go语言,从1.3版本开始提供对象重用机制,即 sync.Pool

sync.Pool 是 sync 包下的一个组件,可以作为保存临时取还对象的一个"池子"。

同时sync.Pool是可伸缩的与并发安全的,他的大小受限于内存的大小。sync.Pool用于存储那些被分配了但是没有被使用,而未来还会使用的值。

复制代码
这样就不用再次经过内存分配,而是直接复用对象,减轻GC压力,从而提升性能。

AI写代码
1

但个人觉得它的名字有一定的误导性,因为 Pool 里装的对象可以被无通知地被回收,可能 sync.Cache(临时缓存) 是一个更合适的名字。

有什么用?

二、如何使用

sync.Pool 的使用方式非常简单:

2.1、声明对象池

只需要实现New函数即可,当对象池(sync.Pool)中没有对象时,就会自动调用New函数进行。

go 复制代码
var userPool = sync.Pool{
    New: func() interface{} { 
        return new(User) 
    },
}

AI写代码
12345
2.2、GET & PUT
scss 复制代码
// 取出
user := userPool.Get().(*User) 
json.Unmarshal(buf,user)
// 放回
userPool.Put(user) 

AI写代码
12345
  • Get() 用于从对象池中获取对象,因为返回值是 interface{},因此需要类型转换。
  • Put() 则是在对象使用完毕后,返回对象池。

三、实例:

3.1、标准库中的应用
3.1.1: fmt.Printf

Go语言标准库大量使用了sync.Pool,例如: fmtencoding/json

以下是fmt.Printf的源代码(go/src/fmt/print.go) - 你可以到自己的项目中看一下

go 复制代码
// go 1.13.6

// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
// pp用于存储打印机的状态,并与sync.Pool一起重用。以避免分配。
type pp struct {
    buf buffer
    ...
}

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
// newPrinter分配了一个新的pp结构体或获取一个缓存的pp结构体。
func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

// free saves used pp structs in ppFree; avoids an allocation per invocation.
// 在ppFree中保存使用过的pp结构体;避免每次调用分配。
func (p *pp) free() {
	if cap(p.buf) > 64<<10 {
		return
	}

	p.buf = p.buf[:0]
	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErr = nil
	ppFree.Put(p)
}

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

// Printf formats according to a format specifier and writes to standard output.
// Printf根据格式说明符进行格式化,并写入标准输出。
// It returns the number of bytes written and any write error encountered.
// 返回写入的字节数和遇到的任何写入错误。
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

AI写代码
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253

fmt.Printf 的调用是非常频繁的,利用 sync.Pool 复用 pp 对象能够极大地提升性能,减少内存占用,同时降低 GC 压力。

3.2、Gin框架的应用(context)

在Gin框架中,Context 对象代表了处理一个HTTP请求的上下文。每个请求都需要一个Context,请求处理完毕,Context的生命周期也就结束了。

  • 高频的创建于销毁 :在高并发下,每秒会创建和销毁大量Context对象。
  • 固定生命周期:Context的生命周期始于请求到来,止于请求处理完毕,非常短暂。
3.2.1、定义对象池

gin.Engine结构体的定义中,你可以看到pool字段就是一个sync.Pool

go 复制代码
type Engine struct {
    // ... 其他字段
    pool sync.Pool // context 对象池
}
如下:

AI写代码
12345
3.2.2、初始化对象池

在创建Gin引擎实例的时,会初始化sync.Pool,并指定New函数。

当池子中无对象可用的时,会调用此函数创建新的Context。

go 复制代码
func New() *Engine {
    // ...
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}

func (engine *Engine) allocateContext(maxParams uint16) *Context {
    // 分配并初始化一个Context
    v := make(Params, 0, maxParams)
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}

AI写代码
12345678910111213
3.2.3、从池中获取Context

当HTTP请求到达时,Gin会从sync.Pool中获取一个Context对象

scss 复制代码
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从对象池中获取一个 context[citation:7]
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()
    // ... 处理 http 请求
    engine.handleHTTPRequest(c)
    // 把 context 放回对象池[citation:7]
    engine.pool.Put(c)
}

AI写代码
1234567891011
3.2.4、处理请求后放回池中

请求处理完毕后,Gin会将Contex重置并放回sync.Pool中,以供后面复用。

scss 复制代码
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    // ... 处理 http 请求
    engine.handleHTTPRequest(c)
    // 请求处理完成后,将 Context 放回池中[citation:7]
    engine.pool.Put(c)
}

AI写代码
1234567

切记,重点是要重置的,如调用c.reset()。确保放回的是干净的上下文。

四、我在项目中的实战

4.1、为何最初选择sync.Pool

因以后其他博客还会提及,所以这里就简洁的说一下:
我的目的:

复制代码
设计了一个支持多存储驱动的图片上传模块,重点解决了并发性能、资源管理和动态切换的问题

AI写代码
1

为了解决所谓的高并发,复用实例的问题,我很自然的想到了sync.Pool,但问题来了!

  1. 对象复用:避免频繁创建和销毁对象
  2. 并发安全:多个用户可同时使用不同驱动

为此,我还美滋滋的,描绘了一个草图:

scss 复制代码
// 多驱动对象池管理器
type MultiDriverPool struct {
    pools   map[string]ObjectPool
    mu      sync.RWMutex
    current string // 当前默认驱动
}
// 对象池接口
type ObjectPool interface {
    Get() (Driver, error)
    Put(Driver) error
    Close()
    Size() int
    Available() int
}

AI写代码
1234567891011121314
4.2、又为何选择放弃sync.pool
4.2.1、存储驱动通常是无状态的

比如: 七牛云驱动使用相同的AccessKey和SecretKey,每个实例执行相同的操作,没有必要维护多个实例。实际上,一个驱动实例就可以处理所有请求,而且通常驱动本身是线程安全的(或者可以通过在方法内部分配资源来做到线程安全)

**换句话说就是:**认为每个驱动实例需要频繁创建和销毁,但实际上驱动实例是可以复用的,而且创建成本不高,并且"存储驱动是无状态的" !

复制代码
所以我最终的设计模式是:单例+多驱动模式。

AI写代码
1

五、总结

适合 sync.Pool 的场景:

  • 创建成本高 对象初始化有显著开销
  • 生命周期短 使用后很快就不再需要
  • 使用频率高 大量并发创建销毁
  • 可安全重置 能完全清理之前的状态

不适合:

go 复制代码
// 1、存储驱动 - 创建成本低,生命周期长
var driverPool = sync.Pool{
    New: func() interface{} { return &QiniuDriver{} },
}

// 2、 数据库连接 - 需要连接池,不是对象池
var dbPool = sync.Pool{
    New: func() interface{} { return sql.Open(...) },
}

// 3、配置对象 - 长期存在,不需要频繁创建
var configPool = sync.Pool{
    New: func() interface{} { return loadConfig() },
}

AI写代码
1234567891011121314

在结尾处,我在声明一下:

sync.Pool 的核心作用,不是资源管理。

而是通过保存和复用临时对象,减少内存分配,降低GC压力!

相关推荐
二川bro2 小时前
第41节:第三阶段总结:打造一个AR家具摆放应用
后端·restful
aiopencode2 小时前
苹果应用商店上架全流程 从证书体系到 IPA 上传的跨平台方法
后端
百***86052 小时前
Spring BOOT 启动参数
java·spring boot·后端
wei_shuo2 小时前
基于Linux平台的openGauss一主两备高可用集群部署与运维实践研究
后端
by__csdn3 小时前
微服务与单体那些事儿
java·后端·微服务·云原生·架构
天草二十六_简村人3 小时前
dify中级入门示例--使用知识库搭建智能客服机器人
后端·ai·云原生·ai编程
optimistic_chen4 小时前
【Java EE进阶 --- SpringBoot】AOP原理
spring boot·笔记·后端·java-ee·开源·aop
IT_陈寒4 小时前
Vue3性能优化实战:我从这5个技巧中获得了40%的渲染提升
前端·人工智能·后端
IUGEI4 小时前
Websocket、HTTP/2、HTTP/3原理解析
java·网络·后端·websocket·网络协议·http·https