以net/http为底层的Gin架构
首先看一下Gin是如何启动一个http web server服务的:
func main() {
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
router.GET("/someGet", getting)
// By default it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
}
Gin启动一个web server非常简单,一个gin.Default()把默认的配置设置获取,一个GET()把处理函数注册到框架内,一个Run()就可以把将Server绑定指定端口幷跑起来。Gin server最重要的也是上面提到的3个部分。
首先我们先看下gin.Default()做了些什么工作:
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
我们通过Default接口返回一个启动Gin server的默认配置,即New出来一个egine对象,幷注册了Logger(), Recovery()两个中间件,即默认使用了日志记录,以及利用recovery来捕捉panic,打印出函数调用栈。
我们看一下New一个engine对象时,会初始化哪些成员变量
/ New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
TrustedPlatform: defaultPlatform,
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
engine.pool.New = func() interface{} { //注意这里,使用了syc.pool来缓存context对象
return engine.allocateContext()
}
return engine
}
我们继续看下router.GET(“/someGet”, getting)具体做了哪些工作,GET的作用就是将我们的处理函数注册到指定的URL上,而注册的过程是这样的:GET->handle->combineHandlers
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
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)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
}
GET方法是对group.handle的封装,group.handle方法中:
- group.calculateAbsolutePath 计算得到绝对路径的url
- group.combineHandlers 返回需要执行的handler列表,其中包括中间件+注册到该URI的handler
- engine.addRoute 将handlers跟URL注册绑定到路由树的节点上,方便后续快速搜索访问。
函数combineHandlers中,使用了两次deep copy操作,第一deep copy是为了把已有的中间件handlers拷贝一份到mergedHandlers;第二次deep copy把本次要注册的handler拷贝放置到mergedHandlers后面。这里注意:
两次拷贝是有序的,即中间件handlers在前,新注册的handler在后,如果我们设置了中间件,我们的请求是先走中间件handler,再走GET等方法注册的handler。
我们最后看下Run()函数的处理逻辑:
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
err = engine.parseTrustedProxies()
if err != nil {
return err
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
Run方法的本质是使用了net/http中的http.ListenAndServe启动一个http web server来监听端口,注意http.ListenAndServe第二个参数,Gin传入的是engine对象,我们知道http.ListenAndServe要求第二个参数传入的是一个实现了ServeHTTP对象,而Gin的engine也确实实现这个ServeHTTP方法。
// 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() // context对象使用前先reset,初始化
engine.handleHTTPRequest(c)
engine.pool.Put(c) // context用完了,还回给对象池
}
这里注意到,context是存储到engine的sync.pool中的,这种做法的好处是复用已经使用过的对象,来达到优化内存使用和回收的目的。因为每处理一个请求都会要求分配一个contex对象,因此分配和回收context对象是个很频繁的操作,十分消耗性能。因此Gin利用sync.pool对象池来优化这个对象分配的操作。一开始这个池子会初始化一些contex对象供Gin使用,如果不够了,自己会通过new产生一些,当你放回去了之后这些对象会被别人进行复用。
这里再仔细分析下Gin的context结构体,这是Gin中最重要的结构体之一,另一个同样重要的是engine结构体。engine结构体负责管理整个Gin web server,而context结构体时管理每一个请求,一个http请求到达服务器,就会有一个新的context产生,这个context因请求的到来而创建,因请求的结束而消亡。
注意Gin自己实现的context跟Go自带的context是不一样的,Go的context常用于做请求的生命周期管理,比如超时控制等;而Gin实现的context用于请求的各个handler间传递数据。
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain // 我们注册的处理函数
index int8
fullPath string
engine *Engine
params *Params
// This mutex protect Keys map
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{} // 用于各个中间件handler之间传递数据,需要使用sync.RWMutex保证读写安全
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
}
engine.handleHTTPRequest(c)是处理单个请求的入口,传入参数就是我们从对象池中取出的已初始化了的context对象。
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method // GET/POST/PUT/DELETED等方法
rPath := c.Request.URL.Path // URL
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method
t := engine.trees // engine.trees的类型:type methodTrees []methodTree
// 至于这里为什么engine.trees是个slice,这是因为slice中每个元素是每个方法(GET/POST等)的tree
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape) // 从对应的节点取出处理函数
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() // 开始处理handlers列表的所有函数
c.writermem.WriteHeaderNow()
return
}
if httpMethod != http.MethodConnect && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
看了上面的代码,会产生一个疑问,为什么engine.trees是个slice?为什么会有多个路由树呢?原因是Gin的路由树是每个HTTP方法一棵树,比如GET方法是一棵树,POST方法也是单独一颗树。当一个请求过来了,需要遍历每棵方法树,所以看代码也可以发现,遍历时第一个判断也是判断if t[i].method != httpMethod
,不是请求的方法就跳过。而方法树的建立是惰性建立,是在注册GET/POST等方法时就建立的,如果我们没有为某个方法注册handler,那么就不会预先建立对应的树。具体函数是下面的addRoute。
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)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
}
middleware对请求做前置后置处理
Gin相对于Go自带的http框架一个很大的改进是引入了中间件的功能,也就是说,我们可以在执行我们请求处理函数的前后,可以自定义插入各种函数,这个技术其实就是我们常说的函数狗子hook,在一个处理前后加函数钩子,比如在执行指定函数前,我们先执行A,B,C函数,在执行后会触发执行X,Y,Q函数。
Gin添加中间件是这样的写法:
func TestMiddlewareGeneralCase(t *testing.T) {
signature := ""
router := New()
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
})
router.GET("/", func(c *Context) {
signature += "D"
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.NoMethod(func(c *Context) {
signature += " XX "
})
// RUN
w := performRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ACDB", signature)
}
利用Use方法将中间件处理函数注册到中间件handlers列表里。中间件处理函数是以slice的形式存在,使用Use方法把所有middleware处理函数append到slice中。因此先位于Use前面的中间件是优先执行的。
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
Next方法的本质是对slice中注册的所有处理函数进行遍历和执行。当一个函数体内,调用Next(),就会执行下一个中间件注册好的handler,Next()后的逻辑会在所有中间件handler都执行完毕后再执行。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
看看这个例子
func Middleware(c *gin.Context) {
fmt.Println("Hello Before;")
c.Next()
fmt.Println("Hello After;")
}
- c.Next() 之前的操作是在 Handler 执行之前就执行;
- c.Next() 之后的操作是在 Handler 执行之后再执行;
之前的操作一般用来做验证处理,访问是否允许之类的。
之后的操作一般是用来做总结处理,比如格式化输出、响应结束时间,响应时长计算之类的。
如果使用gin.New来new一个engine,那么我们起的这个server是不带任何中间件的;如果使用的是gin.Default来new一个engine,那默认使用的中间件有Logger和Recovery,用户新加的中间件只会append到Logger和Recovery之后,遍历执行时也是按照Logger、Recovery、Middleware1、Middleware2…注册顺序来执行。因此可以理解为,在处理一个请求时,由于有
Recovery是首先执行的,因此后面的handler即使触发了panic也能被捕捉和recovery,因此不需要担心进程会因为panic而crash。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
前缀树路由
net/http web server用了一个非常简单的map结构存储了路由表,使用map存储键值对,key是请求的URI,value是handler处理函数。这种map维护的路由,索引非常高效,当然这个路由map是加锁的,具体代码:https://github.com/golang/go/blob/5f3dabbb79fb3dc8eea9a5050557e9241793dce3/src/net/http/server.go#L2453
但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于/hello/:name或者/user*/login这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。
实现动态路由最常用的数据结构,是前缀树(Trie树)。前缀树的特点:每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。Gin采取了前缀树作为路由查找的数据结构,支持了动态路由功能。
HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
这里Gin的前缀树路由实现没有特别之处,所以大概看下代码就好了。
https://github.com/gin-gonic/gin/blob/3a6f18f32f22d7978bbafdf9b81d3a568b7a5868/tree.go
请求数据binding为对象
请求转结构体的本质调用是github.com/go-playground/validator/v10来做利用validator做json/xml/protobuf到对象的转换。
在请求处理时我很喜欢使用ShouldBind来将http请求body转换为结构体,我们常用的写法如下:
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}
type formB struct {
Bar string `json:"bar" xml:"bar" binding:"required"`
}
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
} else if errB := c.ShouldBind(&objB); errB == nil {
c.String(http.StatusOK, `the body should be formB`)
} else {
...
}
}
func (c *Context) ShouldBind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType()) // 从这里就已经知道需要使用哪个binding方法了(json/xml/form等)
return c.ShouldBindWith(obj, b)
}
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj) // 根据body的协议来选择调用响应的bind方法来解码
}
比如请求body的协议是json,那就使用json的bind decoder来将body翻译为结构体。
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return errors.New("invalid request")
}
return decodeJSON(req.Body, obj)
}
func decodeJSON(r io.Reader, obj interface{}) error {
decoder := json.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}
if EnableDecoderDisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj) // 这里body转struct的核心步骤,用了validator库
}
我们注意到,这个ValidateStruct使用到了反射机制,我们利用reflect.ValueOf和value.Kind()来判断当前的对象的类型,这里处理的类型有三种:指针、结构体和slice/array。
func validate(obj interface{}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj)
switch value.Kind() {
case reflect.Ptr:
return v.ValidateStruct(value.Elem().Interface())
case reflect.Struct:
return v.validateStruct(obj)
case reflect.Slice, reflect.Array:
count := value.Len()
validateRet := make(sliceValidateError, 0)
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}
}
if len(validateRet) == 0 {
return nil
}
return validateRet
default:
return nil
}
}
// validateStruct receives struct type
func (v *defaultValidator) validateStruct(obj interface{}) error {
v.lazyinit()
return v.validate.Struct(obj)
}
func (v *defaultValidator) lazyinit() {
v.once.Do(func() {
v.validate = validator.New()
v.validate.SetTagName("binding")
})
}
异常处理
Gin默认使用过的中间件有Logger和Recovery,用户新加的中间件只会append到Logger和Recovery之后,遍历执行时也是按照Logger、Recovery、Middleware1、Middleware2…注册顺序来执行。因此可以理解为,在处理一个请求时,由于有
Recovery是首先执行的,因此后面的handler即使触发了panic也能被捕捉和recovery,因此不需要担心进程会因为panic而crash。如果没有特殊的需求,请使用gin.Default()而非gin.New()来获取一个engine对象。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
https://github.com/gin-gonic/gin/blob/3a6f18f32f22d7978bbafdf9b81d3a568b7a5868/recovery.go#L51
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), headersToStr, err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
if brokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
handle(c, err)
}
}
}()
c.Next()
}
}
recovery中间件的实现简单而言就是recovery语句+Next语句,因为实现了recovery来捕捉panic,因此不需要担心因为panic而到最后程序crash,并且当panic发生时,recovery中间件还把函数调用栈打印出来,方便我们定位问题。