在源码中学习一些技巧

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
}

其中ListenAndServenet/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结构体实现了这个方法:

image-20210425003912272

// 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指针。

  1. 通过对象池,减少每一次临时创建对象的内存申请和垃圾回收的消耗
  2. 频繁的对象申请与回收,就可以使用对象池优化代码
  3. 可以避免对象的引用或者其他的干扰,在不影响代码的实际功能,从池子里面去一个对象后,再做初始化

3.使用方法

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

3.1 声明

//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
    //实现New函数
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	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 _ 接口类型=结构体类型指针,这个主要是确保结构体确实实现了接口,目的就是把问题暴露在编译阶段,很多第三方库和标准库都是这样做的,值得学习。

参考链接

https://www.cnblogs.com/sunsky303/p/9706210.html

https://geektutu.com/post/hpg-sync-pool.html