以net/http为底层的Gin架构

首先看一下Gin是如何启动一个http web server服务的:

  1. func main() {
  2. // Creates a gin router with default middleware:
  3. // logger and recovery (crash-free) middleware
  4. router := gin.Default()
  5. router.GET("/someGet", getting)
  6. // By default it serves on :8080 unless a
  7. // PORT environment variable was defined.
  8. router.Run()
  9. // router.Run(":3000") for a hard coded port
  10. }

Gin启动一个web server非常简单,一个gin.Default()把默认的配置设置获取,一个GET()把处理函数注册到框架内,一个Run()就可以把将Server绑定指定端口幷跑起来。Gin server最重要的也是上面提到的3个部分。

首先我们先看下gin.Default()做了些什么工作:

  1. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
  2. func Default() *Engine {
  3. debugPrintWARNINGDefault()
  4. engine := New()
  5. engine.Use(Logger(), Recovery())
  6. return engine
  7. }

我们通过Default接口返回一个启动Gin server的默认配置,即New出来一个egine对象,幷注册了Logger(), Recovery()两个中间件,即默认使用了日志记录,以及利用recovery来捕捉panic,打印出函数调用栈。

我们看一下New一个engine对象时,会初始化哪些成员变量

  1. / New returns a new blank Engine instance without any middleware attached.
  2. // By default the configuration is:
  3. // - RedirectTrailingSlash: true
  4. // - RedirectFixedPath: false
  5. // - HandleMethodNotAllowed: false
  6. // - ForwardedByClientIP: true
  7. // - UseRawPath: false
  8. // - UnescapePathValues: true
  9. func New() *Engine {
  10. debugPrintWARNINGNew()
  11. engine := &Engine{
  12. RouterGroup: RouterGroup{
  13. Handlers: nil,
  14. basePath: "/",
  15. root: true,
  16. },
  17. FuncMap: template.FuncMap{},
  18. RedirectTrailingSlash: true,
  19. RedirectFixedPath: false,
  20. HandleMethodNotAllowed: false,
  21. ForwardedByClientIP: true,
  22. RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
  23. TrustedProxies: []string{"0.0.0.0/0"},
  24. TrustedPlatform: defaultPlatform,
  25. UseRawPath: false,
  26. RemoveExtraSlash: false,
  27. UnescapePathValues: true,
  28. MaxMultipartMemory: defaultMultipartMemory,
  29. trees: make(methodTrees, 0, 9), // 路由树,我们注册的函数都会放在这里,每个节点都记录着注册的函数
  30. delims: render.Delims{Left: "{{", Right: "}}"},
  31. secureJSONPrefix: "while(1);",
  32. }
  33. engine.RouterGroup.engine = engine
  34. engine.pool.New = func() interface{} { //注意这里,使用了syc.pool来缓存context对象
  35. return engine.allocateContext()
  36. }
  37. return engine
  38. }

我们继续看下router.GET(“/someGet”, getting)具体做了哪些工作,GET的作用就是将我们的处理函数注册到指定的URL上,而注册的过程是这样的:GET->handle->combineHandlers

  1. // GET is a shortcut for router.Handle("GET", path, handle).
  2. func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
  3. return group.handle(http.MethodGet, relativePath, handlers)
  4. }
  5. func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  6. absolutePath := group.calculateAbsolutePath(relativePath)
  7. handlers = group.combineHandlers(handlers)
  8. group.engine.addRoute(httpMethod, absolutePath, handlers)
  9. return group.returnObj()
  10. }
  11. func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
  12. finalSize := len(group.Handlers) + len(handlers)
  13. assert1(finalSize < int(abortIndex), "too many handlers")
  14. mergedHandlers := make(HandlersChain, finalSize)
  15. copy(mergedHandlers, group.Handlers)
  16. copy(mergedHandlers[len(group.Handlers):], handlers)
  17. return mergedHandlers
  18. }
  19. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  20. assert1(path[0] == '/', "path must begin with '/'")
  21. assert1(method != "", "HTTP method can not be empty")
  22. assert1(len(handlers) > 0, "there must be at least one handler")
  23. debugPrintRoute(method, path, handlers)
  24. root := engine.trees.get(method)
  25. if root == nil {
  26. root = new(node)
  27. root.fullPath = "/"
  28. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  29. }
  30. root.addRoute(path, handlers)
  31. // Update maxParams
  32. if paramsCount := countParams(path); paramsCount > engine.maxParams {
  33. engine.maxParams = paramsCount
  34. }
  35. }

GET方法是对group.handle的封装,group.handle方法中:

  1. group.calculateAbsolutePath 计算得到绝对路径的url
  2. group.combineHandlers 返回需要执行的handler列表,其中包括中间件+注册到该URI的handler
  3. engine.addRoute 将handlers跟URL注册绑定到路由树的节点上,方便后续快速搜索访问。

函数combineHandlers中,使用了两次deep copy操作,第一deep copy是为了把已有的中间件handlers拷贝一份到mergedHandlers;第二次deep copy把本次要注册的handler拷贝放置到mergedHandlers后面。这里注意:
两次拷贝是有序的,即中间件handlers在前,新注册的handler在后,如果我们设置了中间件,我们的请求是先走中间件handler,再走GET等方法注册的handler。

我们最后看下Run()函数的处理逻辑:

  1. // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
  2. // It is a shortcut for http.ListenAndServe(addr, router)
  3. // Note: this method will block the calling goroutine indefinitely unless an error happens.
  4. func (engine *Engine) Run(addr ...string) (err error) {
  5. defer func() { debugPrintError(err) }()
  6. err = engine.parseTrustedProxies()
  7. if err != nil {
  8. return err
  9. }
  10. address := resolveAddress(addr)
  11. debugPrint("Listening and serving HTTP on %s\n", address)
  12. err = http.ListenAndServe(address, engine)
  13. return
  14. }

Run方法的本质是使用了net/http中的http.ListenAndServe启动一个http web server来监听端口,注意http.ListenAndServe第二个参数,Gin传入的是engine对象,我们知道http.ListenAndServe要求第二个参数传入的是一个实现了ServeHTTP对象,而Gin的engine也确实实现这个ServeHTTP方法。

  1. // ServeHTTP conforms to the http.Handler interface.
  2. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3. c := engine.pool.Get().(*Context)
  4. c.writermem.reset(w)
  5. c.Request = req
  6. c.reset() // context对象使用前先reset,初始化
  7. engine.handleHTTPRequest(c)
  8. engine.pool.Put(c) // context用完了,还回给对象池
  9. }

这里注意到,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间传递数据。

  1. // Context is the most important part of gin. It allows us to pass variables between middleware,
  2. // manage the flow, validate the JSON of a request and render a JSON response for example.
  3. type Context struct {
  4. writermem responseWriter
  5. Request *http.Request
  6. Writer ResponseWriter
  7. Params Params
  8. handlers HandlersChain // 我们注册的处理函数
  9. index int8
  10. fullPath string
  11. engine *Engine
  12. params *Params
  13. // This mutex protect Keys map
  14. mu sync.RWMutex
  15. // Keys is a key/value pair exclusively for the context of each request.
  16. Keys map[string]interface{} // 用于各个中间件handler之间传递数据,需要使用sync.RWMutex保证读写安全
  17. // Errors is a list of errors attached to all the handlers/middlewares who used this context.
  18. Errors errorMsgs
  19. // Accepted defines a list of manually accepted formats for content negotiation.
  20. Accepted []string
  21. // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
  22. queryCache url.Values
  23. // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
  24. // or PUT body parameters.
  25. formCache url.Values
  26. // SameSite allows a server to define a cookie attribute making it impossible for
  27. // the browser to send this cookie along with cross-site requests.
  28. sameSite http.SameSite
  29. }

engine.handleHTTPRequest(c)是处理单个请求的入口,传入参数就是我们从对象池中取出的已初始化了的context对象。

  1. func (engine *Engine) handleHTTPRequest(c *Context) {
  2. httpMethod := c.Request.Method // GET/POST/PUT/DELETED等方法
  3. rPath := c.Request.URL.Path // URL
  4. unescape := false
  5. if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  6. rPath = c.Request.URL.RawPath
  7. unescape = engine.UnescapePathValues
  8. }
  9. if engine.RemoveExtraSlash {
  10. rPath = cleanPath(rPath)
  11. }
  12. // Find root of the tree for the given HTTP method
  13. t := engine.trees // engine.trees的类型:type methodTrees []methodTree
  14. // 至于这里为什么engine.trees是个slice,这是因为slice中每个元素是每个方法(GET/POST等)的tree
  15. for i, tl := 0, len(t); i < tl; i++ {
  16. if t[i].method != httpMethod {
  17. continue
  18. }
  19. root := t[i].root
  20. // Find route in tree
  21. value := root.getValue(rPath, c.params, unescape) // 从对应的节点取出处理函数
  22. if value.params != nil {
  23. c.Params = *value.params
  24. }
  25. if value.handlers != nil {
  26. c.handlers = value.handlers
  27. c.fullPath = value.fullPath
  28. c.Next() // 开始处理handlers列表的所有函数
  29. c.writermem.WriteHeaderNow()
  30. return
  31. }
  32. if httpMethod != http.MethodConnect && rPath != "/" {
  33. if value.tsr && engine.RedirectTrailingSlash {
  34. redirectTrailingSlash(c)
  35. return
  36. }
  37. if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
  38. return
  39. }
  40. }
  41. break
  42. }
  43. if engine.HandleMethodNotAllowed {
  44. for _, tree := range engine.trees {
  45. if tree.method == httpMethod {
  46. continue
  47. }
  48. if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
  49. c.handlers = engine.allNoMethod
  50. serveError(c, http.StatusMethodNotAllowed, default405Body)
  51. return
  52. }
  53. }
  54. }
  55. c.handlers = engine.allNoRoute
  56. serveError(c, http.StatusNotFound, default404Body)
  57. }

看了上面的代码,会产生一个疑问,为什么engine.trees是个slice?为什么会有多个路由树呢?原因是Gin的路由树是每个HTTP方法一棵树,比如GET方法是一棵树,POST方法也是单独一颗树。当一个请求过来了,需要遍历每棵方法树,所以看代码也可以发现,遍历时第一个判断也是判断if t[i].method != httpMethod,不是请求的方法就跳过。而方法树的建立是惰性建立,是在注册GET/POST等方法时就建立的,如果我们没有为某个方法注册handler,那么就不会预先建立对应的树。具体函数是下面的addRoute。

  1. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  2. assert1(path[0] == '/', "path must begin with '/'")
  3. assert1(method != "", "HTTP method can not be empty")
  4. assert1(len(handlers) > 0, "there must be at least one handler")
  5. debugPrintRoute(method, path, handlers)
  6. root := engine.trees.get(method)
  7. if root == nil {
  8. root = new(node)
  9. root.fullPath = "/"
  10. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  11. }
  12. root.addRoute(path, handlers)
  13. // Update maxParams
  14. if paramsCount := countParams(path); paramsCount > engine.maxParams {
  15. engine.maxParams = paramsCount
  16. }
  17. }

截屏2021-09-22 下午10.48.32.png

middleware对请求做前置后置处理

Gin相对于Go自带的http框架一个很大的改进是引入了中间件的功能,也就是说,我们可以在执行我们请求处理函数的前后,可以自定义插入各种函数,这个技术其实就是我们常说的函数狗子hook,在一个处理前后加函数钩子,比如在执行指定函数前,我们先执行A,B,C函数,在执行后会触发执行X,Y,Q函数。

Gin添加中间件是这样的写法:

  1. func TestMiddlewareGeneralCase(t *testing.T) {
  2. signature := ""
  3. router := New()
  4. router.Use(func(c *Context) {
  5. signature += "A"
  6. c.Next()
  7. signature += "B"
  8. })
  9. router.Use(func(c *Context) {
  10. signature += "C"
  11. })
  12. router.GET("/", func(c *Context) {
  13. signature += "D"
  14. })
  15. router.NoRoute(func(c *Context) {
  16. signature += " X "
  17. })
  18. router.NoMethod(func(c *Context) {
  19. signature += " XX "
  20. })
  21. // RUN
  22. w := performRequest(router, "GET", "/")
  23. // TEST
  24. assert.Equal(t, http.StatusOK, w.Code)
  25. assert.Equal(t, "ACDB", signature)
  26. }

利用Use方法将中间件处理函数注册到中间件handlers列表里。中间件处理函数是以slice的形式存在,使用Use方法把所有middleware处理函数append到slice中。因此先位于Use前面的中间件是优先执行的。

  1. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
  2. group.Handlers = append(group.Handlers, middleware...)
  3. return group.returnObj()
  4. }

Next方法的本质是对slice中注册的所有处理函数进行遍历和执行。当一个函数体内,调用Next(),就会执行下一个中间件注册好的handler,Next()后的逻辑会在所有中间件handler都执行完毕后再执行。

  1. func (c *Context) Next() {
  2. c.index++
  3. for c.index < int8(len(c.handlers)) {
  4. c.handlers[c.index](c)
  5. c.index++
  6. }
  7. }

看看这个例子

  1. func Middleware(c *gin.Context) {
  2. fmt.Println("Hello Before;")
  3. c.Next()
  4. fmt.Println("Hello After;")
  5. }
  • 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。

  1. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
  2. func Default() *Engine {
  3. debugPrintWARNINGDefault()
  4. engine := New()
  5. engine.Use(Logger(), Recovery())
  6. return engine
  7. }

前缀树路由

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请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。

截屏2021-09-22 下午10.48.22.png

这里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转换为结构体,我们常用的写法如下:

  1. type formA struct {
  2. Foo string `json:"foo" xml:"foo" binding:"required"`
  3. }
  4. type formB struct {
  5. Bar string `json:"bar" xml:"bar" binding:"required"`
  6. }
  7. func SomeHandler(c *gin.Context) {
  8. objA := formA{}
  9. objB := formB{}
  10. // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
  11. if errA := c.ShouldBind(&objA); errA == nil {
  12. c.String(http.StatusOK, `the body should be formA`)
  13. // Always an error is occurred by this because c.Request.Body is EOF now.
  14. } else if errB := c.ShouldBind(&objB); errB == nil {
  15. c.String(http.StatusOK, `the body should be formB`)
  16. } else {
  17. ...
  18. }
  19. }
  20. func (c *Context) ShouldBind(obj interface{}) error {
  21. b := binding.Default(c.Request.Method, c.ContentType()) // 从这里就已经知道需要使用哪个binding方法了(json/xml/form等)
  22. return c.ShouldBindWith(obj, b)
  23. }
  24. func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
  25. return b.Bind(c.Request, obj) // 根据body的协议来选择调用响应的bind方法来解码
  26. }

比如请求body的协议是json,那就使用json的bind decoder来将body翻译为结构体。

  1. func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
  2. if req == nil || req.Body == nil {
  3. return errors.New("invalid request")
  4. }
  5. return decodeJSON(req.Body, obj)
  6. }
  7. func decodeJSON(r io.Reader, obj interface{}) error {
  8. decoder := json.NewDecoder(r)
  9. if EnableDecoderUseNumber {
  10. decoder.UseNumber()
  11. }
  12. if EnableDecoderDisallowUnknownFields {
  13. decoder.DisallowUnknownFields()
  14. }
  15. if err := decoder.Decode(obj); err != nil {
  16. return err
  17. }
  18. return validate(obj) // 这里body转struct的核心步骤,用了validator库
  19. }

我们注意到,这个ValidateStruct使用到了反射机制,我们利用reflect.ValueOf和value.Kind()来判断当前的对象的类型,这里处理的类型有三种:指针、结构体和slice/array。

  1. func validate(obj interface{}) error {
  2. if Validator == nil {
  3. return nil
  4. }
  5. return Validator.ValidateStruct(obj)
  6. }
  7. // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
  8. func (v *defaultValidator) ValidateStruct(obj interface{}) error {
  9. if obj == nil {
  10. return nil
  11. }
  12. value := reflect.ValueOf(obj)
  13. switch value.Kind() {
  14. case reflect.Ptr:
  15. return v.ValidateStruct(value.Elem().Interface())
  16. case reflect.Struct:
  17. return v.validateStruct(obj)
  18. case reflect.Slice, reflect.Array:
  19. count := value.Len()
  20. validateRet := make(sliceValidateError, 0)
  21. for i := 0; i < count; i++ {
  22. if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
  23. validateRet = append(validateRet, err)
  24. }
  25. }
  26. if len(validateRet) == 0 {
  27. return nil
  28. }
  29. return validateRet
  30. default:
  31. return nil
  32. }
  33. }
  34. // validateStruct receives struct type
  35. func (v *defaultValidator) validateStruct(obj interface{}) error {
  36. v.lazyinit()
  37. return v.validate.Struct(obj)
  38. }
  39. func (v *defaultValidator) lazyinit() {
  40. v.once.Do(func() {
  41. v.validate = validator.New()
  42. v.validate.SetTagName("binding")
  43. })
  44. }

异常处理

Gin默认使用过的中间件有Logger和Recovery,用户新加的中间件只会append到Logger和Recovery之后,遍历执行时也是按照Logger、Recovery、Middleware1、Middleware2…注册顺序来执行。因此可以理解为,在处理一个请求时,由于有
Recovery是首先执行的,因此后面的handler即使触发了panic也能被捕捉和recovery,因此不需要担心进程会因为panic而crash。如果没有特殊的需求,请使用gin.Default()而非gin.New()来获取一个engine对象。

  1. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
  2. func Default() *Engine {
  3. debugPrintWARNINGDefault()
  4. engine := New()
  5. engine.Use(Logger(), Recovery())
  6. return engine
  7. }

https://github.com/gin-gonic/gin/blob/3a6f18f32f22d7978bbafdf9b81d3a568b7a5868/recovery.go#L51

  1. // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
  2. func Recovery() HandlerFunc {
  3. return RecoveryWithWriter(DefaultErrorWriter)
  4. }
  5. // CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
  6. func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
  7. var logger *log.Logger
  8. if out != nil {
  9. logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
  10. }
  11. return func(c *Context) {
  12. defer func() {
  13. if err := recover(); err != nil {
  14. // Check for a broken connection, as it is not really a
  15. // condition that warrants a panic stack trace.
  16. var brokenPipe bool
  17. if ne, ok := err.(*net.OpError); ok {
  18. if se, ok := ne.Err.(*os.SyscallError); ok {
  19. if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
  20. brokenPipe = true
  21. }
  22. }
  23. }
  24. if logger != nil {
  25. stack := stack(3)
  26. httpRequest, _ := httputil.DumpRequest(c.Request, false)
  27. headers := strings.Split(string(httpRequest), "\r\n")
  28. for idx, header := range headers {
  29. current := strings.Split(header, ":")
  30. if current[0] == "Authorization" {
  31. headers[idx] = current[0] + ": *"
  32. }
  33. }
  34. headersToStr := strings.Join(headers, "\r\n")
  35. if brokenPipe {
  36. logger.Printf("%s\n%s%s", err, headersToStr, reset)
  37. } else if IsDebugging() {
  38. logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
  39. timeFormat(time.Now()), headersToStr, err, stack, reset)
  40. } else {
  41. logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
  42. timeFormat(time.Now()), err, stack, reset)
  43. }
  44. }
  45. if brokenPipe {
  46. // If the connection is dead, we can't write a status to it.
  47. c.Error(err.(error)) // nolint: errcheck
  48. c.Abort()
  49. } else {
  50. handle(c, err)
  51. }
  52. }
  53. }()
  54. c.Next()
  55. }
  56. }

recovery中间件的实现简单而言就是recovery语句+Next语句,因为实现了recovery来捕捉panic,因此不需要担心因为panic而到最后程序crash,并且当panic发生时,recovery中间件还把函数调用栈打印出来,方便我们定位问题。