Gin框架源码解析
2018-09-18 19:44 by 轩脉刃, ... 阅读, ... 评论, 收藏, 编辑Gin框架源码解析
Gin框架是golang的一个常用的web框架,最近一个项目中需要使用到它,所以对这个框架进行了学习。gin包非常短小精悍,不过主要包含的路由,中间件,日志都有了。我们可以追着代码思考下,这个框架是如何一步一步过来的。
从http包说起
基本上现在的golang的web库都是从http上搭建起来,golang的http包的核心如下:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这里的Handler是一个接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
所以,这里就是我们的入口,这里我们需要有一个类来实现这个接口:Engine。
type Engine struct {
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
}
这里ServeHTTP的方法传递的两个参数,一个是Request,一个是ResponseWriter,Engine中的ServeHTTP的方法就是要对这两个对象进行读取或者写入操作。而且这两个对象往往是需要同时存在的,为了避免很多函数都需要写这两个参数,我们不如封装一个结构来把这两个对象放在里面:Context
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
...
}
type responseWriter struct {
http.ResponseWriter
size int
status int
}
这里有几个需要讨论的点:
Writer是否可以直接使用http包的ResponseWriter接口
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
但是考虑到我们web框架的最重要的就是输出数据给客户端,这里的输出逻辑我们极有可能需要自己封装一些框架自带的方法。所以我们不妨自定义一个结构responseWriter,来实现基本的http.ResponseWriter。并且实现一些具体的其他方法。这些具体的其他方法都有哪些呢?我们使用gin包自带的ResponseWriter接口来说明。
type ResponseWriter interface {
responseWriterBase
Pusher() http.Pusher
}
type responseWriterBase interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
Status() int
Size() int
WriteString(string) (int, error)
Written() bool
WriteHeaderNow()
}
为什么Context有writermem和Writer两个实现了http.Response对象的结构?
首先我们自带的ResponseWriter必须实现比http.ResponseWriter更强大的接口功能,这个是毋庸置疑的。所以,我们不妨考虑下这里如果不是两个writermem和Writer两个的话,只有一个存在是否可能?
如果只有Writer接口存在,这个一定不可能,这个Writer实现的是我们gin自定义的接口,外部serveHTTP传递的是实现了http.ResponseWriter的类,并不能保证实现了gin自带的ResponseWriter。
如果只有writermen结构存在,这个是可能的。外部传递的http.ResponseWriter就被藏在了这个对象里面。但是这样就丢失了接口的灵活性。本质还是对外暴露的是接口还是结构的逻辑,设想一下如果使用这个框架的用户要自己实现一个ResponseWriter,就需要继承这个结构,而不是继承接口。而具体的调用的方法就变成了被继承结构的方法了。例子如下:
package main
func main() {
customResp := new(customResponseWriter)
c := new(context)
c.Writermem = customResp.responseWriter
c.Writermem.call()
}
type context struct {
Writermem responseWriter
}
type customResponseWriter struct {
responseWriter
}
func (r *customResponseWriter)call() {
}
type responseWriter struct{}
func (r *responseWriter)call() {
}
所以这里的Context结构,对外暴露的是接口ResponseWriter,内部的responseWriter结构实现了ResponseWriter接口。在reset()的时候进行拷贝是合理的。
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
}
context就是某个请求的上下文结构,这个结构当然是可以不断new的,但是new这个对象的代价可以使用一个对象池进行服用,节省对象频繁创建和销毁的开销。golang中的sync.Pool就是用于这个用途的。需要注意的是,这里的对象池并不是所谓的固定对象池,而是临时对象池,里面的对象个数不能指定,对象存储时间也不能指定,只是增加了对象复用的概率而已。
type Engine struct {
...
pool sync.Pool
...
}
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)
}
这个Context是gin中最重要的数据结构之一了,它既然已经包了request了,那么从请求中获取参数的各个接口它必然也需要包了。
func (c *Context) Param(key string) string
...
func (c *Context) Query(key string) string
func (c *Context) DefaultQuery(key, defaultValue string) string
...
func (c *Context) PostFormArray(key string) []string
路由
从http请求进来的逻辑理清楚了,下面就进入到了路由部分,路由其实还是分为两个部分,一个是路由设置部分,一个是路由匹配部分。
路由其实并不仅仅是url,还包括HTTP的请求方法,而实现一个REST风格的http请求,需要支持REST支持的方法,比如GET,PUT,POST,DELETE,OPTION等。
路由一定是有很多个路由路径,可以使用数组存储,但更巧妙的是,使用Redix树结构进行存储。这样寻找的方法更为高效。
首先我们会在Engine这个结构中增加树结构,并且提供增加路由的功能
type Engine struct {
...
pool sync.Pool
trees methodTrees
}
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method)
if root == nil {
root = new(node)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)