一、对象复用
在高并发的场景下使用golang,优化GC都会无法回避的问题。搜索「golang 垃圾回收优化」出来的结果基本上都会提及对象复用的方式,在实践中也确实很多高性能的开源库大量使用对象复用来优化程序,比如fasthttp。
简单的例子
使用对象复用分为三个环节,1.初始化pool;2.获取对象;3.清空对象+归还对象。
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
}
func (p *Person)Reset() {
p.Name = ""
}
func initPool() {
pool = &sync.Pool {
New: func()interface{} {
fmt.Println("Creating a new Person")
return new(Person)
},
}
}
func main() {
// 1. 初始化池
initPool()
// 2. 获取对象
p := pool.Get().(*Person)
p.Name = "first"
// 3. 清空对象 + 归还对象
p.Reset()
pool.Put(p)
}
二、对象复用的坑
在上面的例子中,如果我们把Reset函数注释了,那么放回池子里的Person对象的Name是不干净的,很有可能会影响下一个使用的地方。不过实际使用中忘记调用Reset方法的比较少,这是对象复用的基本意识,真正容易遗漏的是Reset方法中的实现。
在项目实践中,某些结构体定义的字段会比较多,而项目又一直在迭代,结构体的字段会发生新增、修改或删除,如果新增了字段,但Reset方法漏了新增对应的Reset语句,那么程序就很可能会出现数据篡乱的问题,而且这种问题不是稳定必现的,如果未意识到是字段漏了重置的问题,定位起来让人很头疼。
三、工具比人靠谱
想要避免出现这种问题,完全依赖个人注意肯定是不靠谱的,一个项目可能有多个人维护,团队也可能会有新人,难以确保每个人都能顾及到所有的对象复用。于是我开发了一个静态代码检查的工具 structreset,用于检查对象复用的结构体的Reset方法,是否包含所有结构体字段的重置。当然这里的方法名Reset是我这里定义的,也可以叫别的名字:free/release等等。
代码仓库里的实现是:识别带有refcount
字段的结构体,然后遍历其Reset
方法体语句。这个可以根据项目需求进行修改。
var refCountTypeName = map[string]uint8 {"refcount": 1}
安装
按照文档操作即可。
git clone git@git.xxx.cn:beckjiang/structreset.git
cd structreset
./install.sh
使用
检查项目testdata
structresetx -d ./analysis/passes/structreset/testdata