简介

Recovery是Gin官方提供的一个开箱即用的中间件,用于恢复http服务。

有趣的栈打印函数

​ 我们在平常写代码的时候常常和panic函数打交道,每当遇到panic时,总可以捕捉到具体的代码行以及调用链,这是怎么做到的呢?Recovery中间件的stack函数可以给到你答案:

func stack(skip int) []byte {
   buf := new(bytes.Buffer) // the returned data
   // As we loop, we open files and read them. These variables record the currently
   // loaded file.
   var lines [][]byte
   var lastFile string
  // 这里的意思是在框架里已知的调用栈深度就不再打印了
  // 但这里有待提升的点,如果有除0情况,第一行打印的还是panic信息而不是对应panic的第一位置
   for i := skip; ; i++ { // Skip the expected number of frames
      pc, file, line, ok := runtime.Caller(i)
      if !ok {
         break
      }
      // 打印栈信息
      fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
      if file != lastFile {
         data, err := ioutil.ReadFile(file)
         if err != nil {
            continue
         }
         lines = bytes.Split(data, []byte{'\n'})
         lastFile = file
      }
     // 打印方法名和对应的代码行
      fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
   }
   return buf.Bytes()
}

​ 可以看出,stack函数为了打印用户的panic信息做了很多额外的处理,包括拿到panic的文件里对应的方法和对应的行,可以说是更方便了我们debug。

核心函数CustomRecoveryWithWriter

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 {
               var se *os.SyscallError
               if errors.As(ne, &se) {
                  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)
              // dump下http request
               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] + ": *"
                  }
               }
              // 复原request
               headersToStr := strings.Join(headers, "\r\n")
               if brokenPipe {
                 // 就像上面说的,这里不认为broken pipe是一个panic错误,所以这里普通化打印了
                  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 {
              // 用户定义函数处理err
               handle(c, err)
            }
         }
      }()
     // 上面👆只是在注册recover函数,所以不像logger函数,这里直接顺序处理
      c.Next()
   }
}

总结

​ 整体来说,这里的recovery函数写的还是很贴近真实场景的,简洁并且方便用户定位方法以及具体的产生panic的函数。其中还特殊处理了网络相关的(connection reset 对应 tcp 的reset标识位)的错误。


<
Previous Post
Gin的中间件探索之Logger
>
Next Post
Gin探索之旅 Part-I 路由注册