08 自研or借力(上):集成Gin替换已有核心

我们的框架和这些顶级的框架相比,差了什么呢?如何才能快速地把我们的框架可用性,和这些框架提升到同一个级别?我们做这个框架除了演示每个实现细节,它的优势是什么呢?

不妨带着这些问题,把我们新出炉的框架和 GitHub 上 star 数最高的Gin 框架比对一下,思考下之间的差距到底是什么。

和Gin对比

Gin 框架无疑是现在最火的框架,你能想出很多它的好处,但是在我看来,它之所以那么成功,最主要的原因在于两点:细节和生态

其实框架之间的实现原理都差不多,但是生产级别的框架和我们写的示例级别的框架相比,差别就在于细节,这个细节是需要很多人、很多时间去不断打磨的。

如果你的 Golang 经验积累到一定时间,那肯定能很轻松实现一个示例级别的框架,但是往往是没有开源市场的,因为你的框架,在细节上的设计没有经过很多人验证,也没有经过在生产环境中的实战。这些都需要一个较为庞大的使用群体和较长时间才能慢慢打磨出来。

Recovery的错误捕获

之前我们的错误捕获:


// recovery 机制,将协程中的函数异常进行捕获
func Recovery() framework.ControllerHandler {
  // 使用函数回调
  return func(c *framework.Context) error {
    // 核心在增加这个 recover 机制,捕获 c.Next()出现的 panic
    defer func() {
      if err := recover(); err != nil {
        c.Json(500, err)
      }
    }()
    // 使用 next 执行具体的业务逻辑
    c.Next()

    return nil
  }
}

异常类型可能是底层连接出错,这种情况已经不同通过设置http状体码让浏览器感知,因为消息都发不出去了。这种情况下应该直接中断后续逻辑,gin的处理如下:


return func(c *Context) {
    defer func() {
      if err := recover(); err != nil {
        // 判断是否是底层连接异常,如果是的话,则标记 brokenPipe
        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 brokenPipe {
          // 如果有标记位,我们不能写任何的状态码
          c.Error(err.(error)) // nolint: errcheck
          c.Abort()
        } else {
          handle(c, err)
        }
      }
    }()
    c.Next()
  }

先判断了底层抛出的异常是否是网络异常(net.OpError),如果是的话,再根据异常内容是否包含“broken pipe”或者“connection reset by peer”,来判断这个异常是否是连接中断的异常。如果是,就设置标记位,并且直接使用 c.Abort() 来中断后续的处理逻辑。

这个处理异常的逻辑可以说是非常细节了,区分了网络连接错误的异常和普通的逻辑异常,并且进行了不同的处理逻辑。这一点,可能是绝大多数的开发者都没有考虑到的。

Recovery 的日志打印

  1. logger.Printf打印异常内容

logger.Printf("%s\n%s%s", err, ...)
  1. 异常是由某个请求触发的,所以触发这个异常的请求内容,也是必要的调试信息,需要打印

httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
// 如果 header 头中有认证信息,隐藏,不打印。
for idx, header := range headers {
current := strings.Split(header, ":")
  if current[0] == "Authorization" {
      headers[idx] = current[0] + ": *"
  }
}
headersToStr := strings.Join(headers, "\r\n")

使用DumpRequest输出请求内容,包含header和body。
为了安全考虑 Gin 还注意到了,如果请求头中包含 Authorization 字段,即包含 HTTP 请求认证信息,在输出的地方会进行隐藏处理,不会由于 panic 就把请求的认证信息输出在日志中。

  1. 堆栈打印
    我们打印堆栈一般是使用 runtime 库的 Caller 来打印:

// 打印堆栈信息,是否有这个堆栈
func Caller(skip int) (pc uintptr, file string, line int, ok bool)

caller 方法返回的是堆栈函数所在的函数指针、文件名、函数所在行号信息。但是在使用过程中你就会发现,使用 Caller 是打印不出真实代码的。比如下面这个例子:


// 在 prog.go 文件,main 库中调用 call 方法
func call(skip int) bool {   //24 行
  pc,file,line,ok := runtime.Caller(skip) //25 行
  pcName := runtime.FuncForPC(pc).Name()  //26 行
  fmt.Println(fmt.Sprintf("%v %s %d %s",pc,file,line,pcName)) //27 行
  return ok //28 行
} //29 行

打印出的第一层堆栈函数的信息:

4821380 /tmp/sandbox064396492/prog.go 25 main.call

这个堆栈信息并不友好,它告诉我们,第一层信息所在地址为 prog.go 文件的第 25 行,在 main 库的 call 函数里面。所以如果想了解下第 25 行有什么内容,用这个堆栈信息去源码中进行文本查找,是做不到的。这个时候就非常希望信息能打印出具体的真实代码。

在 Gin 中,打印堆栈的时候就有这么一个逻辑:先去本地查找是否有这个源代码文件,如果有的话,获取堆栈所在的代码行数,将这个代码行数直接打印到控制台中。


// 打印具体的堆栈信息
func stack(skip int) []byte {
  ...
    // 循环从第 skip 层堆栈到最后一层
  for i := skip; ; i++ { 
    pc, file, line, ok := runtime.Caller(i)
    // 获取堆栈函数所在源文件
    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()
}

这样,打印出来的堆栈信息形如:


/Users/yejianfeng/Documents/gopath/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0x1385b5a)
        (*Context).Next: c.handlers[c.index](c)

这个堆栈信息就友好多了,它告诉我们,这个堆栈函数发生在文件的 165 行,它的代码为 c.handlersc.index, 这行代码所在的父级别函数为 (*Context).Next。

最终,Gin 打印出来的 panic 信息形式如下:


2021/08/15 14:18:57 [Recovery] 2021/08/15 - 14:18:57 panic recovered:
GET /first HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/ *;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
...
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36



%!s(int=121321321)
/Users/yejianfeng/Documents/UGit/gindemo/main.go:19 (0x1394214)
        main.func1: panic(121321321)
/Users/yejianfeng/Documents/gopath/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0x1385b5a)
        (*Context).Next: c.handlers[c.index](c)
/Users/yejianfeng/Documents/gopath/pkg/mod/github.com/gin-gonic/gin@v1.7.2/recovery.go:99 (0x1392c48)
        CustomRecoveryWithWriter.func1: c.Next()
/Users/yejianfeng/Documents/gopath/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0x1385b5a)
        (*Context).Next: c.handlers[c.index](c)
...
/usr/local/Cellar/go/1.15.5/libexec/src/net/http/server.go:1925 (0x12494ac)
        (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s:1374 (0x106bb00)
        goexit: BYTE    $0x90   // NOP

这里可以参考log日志包的实现,看日志包里是怎么打印的

路由对比

之前我们将url按斜杠分隔,每段保存在一个节点中。Gin的路由选择是一种压缩后的基数树(radix tree), 它把整个URL当作字符串,尽可能匹配相同的字符串作为公共前缀。

在这里插入图片描述
radix tree 和 trie 树相比,最大的区别就在于它节点的压缩比例最大化。直观比较上面两个图就能看得出来,对于 URL 比较长的路由规则,trie 树的节点数就比 radix tree 的节点数更多,整个数的层级也更深。

针对路由这种功能模块,创建路由树的频率远低于查找路由点频率,那么减少节点层级,无异于能提高查找路由的效率,整体路由模块的性能也能得到提高,所以 Gin 用 radix tree 是更好的选择。

另外,gin的路由查找中,每个节点有indices 字符串,这个字符串的每个字符代表子节点的第一个字符,这个查找子节点不用遍历,可以节省时间。

生态

其实不然。一个开源项目的成功,最重要的是两个事情,第一个是质量,开源项目的代码质量是摆在第一位的,但是还有一个是不能被忽略的:生态的完善。

一个好的开源框架项目,不仅仅只有代码,还需要围绕着核心代码打造文档、框架扩展、辅助命令等。这些代码周边的组件和功能的打造,虽然从难度上看,并没有核心代码那么重要,但是它是一个长期推进和完善的过程。

【小结】:

  1. 现代框架的理念不在于实现,而更多在于组合。基于某些基础组件或者基础实现,不断按照自己或者公司的需求,进行二次改造和二次开发,从而打造出适合需求的形态。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值