【Golang】跟着源码学技巧系列(一)对象池sync.Pool
在源码中学习一些技巧
1.从Run()开始
在go语言的gin框架中,通过.Run()
启动web服务。我们查看源码:
//gin.go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
其中ListenAndServe
是net/http
库中指定的监听地址和处理器
//net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
可以看到gin源码中调用时的是engine *Engine
作为Handler
参数,继续查看一下Handler
源码:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
果然没错Handler
是一个接口,接口方法:ServeHTTP(ResponseWriter, *Request)
那么Engine
必然实现了 **ServeHTTP
**方法
通过VSCode
导航,的确找到了Engine
结构体实现了这个方法:
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
2.sync.Pool登场
我们看上一节的第三行代码:
c := engine.pool.Get().(*Context)
type Engine struct {
//omit code
pool sync.Pool
}
其中pool字段是sync.Pool
类型,那到底什么是sync.Pool
?
原来:这是go语言中典型的对象池的概念,为了减少GC,减少内存申请的频率,把可以重用的对象构造成一个对象池,engine.pool.Get()
就是从池子中捞出一个对象,强制转换为context
指针。这是Go 语言从 1.3 版本开始提供的对象重用的机制。
- 通过对象池,减少每一次临时创建对象的内存申请和垃圾回收的消耗
- 频繁的对象申请与回收,就可以使用对象池优化代码
- 可以避免对象的引用或者其他的干扰,在不影响代码的实际功能,从池子里面去一个对象后,再做初始化
3.使用方法
sync.Pool 的使用方式非常简单:
3.1 声明 New 函数
//gin.go
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
//实现sync.Pool的New函数
engine.pool.New = func() interface{} {
return engine.allocateContext() //engine会生成一个context
}
return engine
}
- 只需要实现 New 函数即可。当对象池中没有对象时,将会调用
New
函数创建。
3.2 Get()
获取
Get()
用于从对象池中获取对象,因为返回值是interface{}
,因此需要类型转换,上面的代码已经有展示。
c := engine.pool.Get().(*Context)
3.3 Put()
放回
Put()
则是在对象使用后,放回对象池。
处理完http请求后,又把context
放回到对象池中:
engine.handleHTTPRequest(c)
engine.pool.Put(c)
3.注意事项
sync.Pool
是可伸缩的,并发安全的。其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。 它的设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。
由于上面加粗字体的原因,所以对象池比较适合用来存储一些临时切状态无关的数据,因为存入对象池的值有可能会在垃圾回收时被删除掉。http请求的context上下文就是这样的类型。
4.再补充一个技巧
在gin
源码中定义Engine
结构体的下面有一句:
var _ IRouter = &Engine{}
一个不起眼的匿名变量:匿名变量不占用内存空间,不会分配内存,那它到底有什么用?不可能是作者写忘了,这么著名的开源库。
原来go语言经常会遇到一个场景,在编写完一个结构体之后,这个结构体会实现很多接口,问题是,代码繁杂以后,谁还记得哪个接口到底实现没有,怎么办?
- 人工查验,嗯,是个方法,
Ctrl+F
比对,找方法接收者,嗯,也是一个方法
但是,更棒的方法,是靠编译器:
定义一个匿名变量 var _ 接口类型=结构体类型指针
,这个主要是确保结构体确实实现了接口,目的就是把问题暴露在编译阶段,很多第三方库和标准库都是这样做的,值得学习。
参考链接
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-04-25-go-coding-tips-pool/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。