【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 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。