如何使你的 golang 项目达到 awesome go 的入选标准

这两天想把之前写的工具提交到 awesome go 的 repo 里,所以特意研究了研究 awesome go 的入选 quality standard。

首先你的代码应该要在 goreportcard 上跑个结果:
goreport

goreportcard 其实就是几种 lint 工具跑出来的一个结果集合。先不说代码必须要过 golint 和 go vet,这两个 lint 大概一般有点节操的程序员都会上。这里面比较有意思而且陌生的主要是 gocyclo。

说到 gocyclo,要先讲讲代码的圈复杂度 Cyclomatic complexity 。圈复杂度是一种衡量代码复杂度的概念,具体的定义参考这里:圈复杂度wiki。wiki 上讲的比较偏理论,如果你觉得 TL;DR 也没关系,只要自己简单写一些代码实验一下就好。总的来说,其实就是你的代码里有 if,那么复杂度 +1;如果有 switch,那么每多一个 case,复杂度 +1。一般比较公认的是 cyclo <= 10 是比较好的代码。这个复杂度实际上也不是整体代码的圈复杂度,而只是指代函数的圈复杂度。这一点你从 gocyclo 跑出来的结果列表应该也看得出来。不过对于 golang 来说有一点比较特殊,因为 golang 本身没有异常处理这种东西,所以很多时候在函数里:

if err != nil {
}

这种事情是免不了的,如果你一个函数里调用了十个其它函数,那显然对于圈复杂度的定义来说,这个函数就被掐死了,因为 cyclo > 10。所以 gocyclo 很明显地在新版本里把这个默认的标准改成了 15,相对来说还算合理。

说完了概念,来说疑问。如果从来没有接触过这个概念的话应该会比较纳闷为什么会有圈复杂度这个概念。为什么呢?我觉得应该是为了代码的可读性和可测试性而做出的一种考量。

如果说一段代码的圈复杂度很高,那只有两种可能性:

*1 函数太长

*2 if/switch case 太多

无论哪种情况都会导致这段代码非常地难读,而且非常地难以进行单元测试的编写。

怎么解决?

很简单,抽函数啊!

switch x:= i.(type) {
  case sqlparser.ComparisonExpr:
     if xxxx
     if yyyy
     if zzzz
     if aaaa
     switch Operator {
       case +:
       case -:
       case *:
       case /:
     }
  case sqlparser.ParenBool:
  ....
}

就像上面这样的代码,看着很不起眼,实际上已经为你的函数圈复杂度贡献了 8 以上的点数。解决办法就像上面说的,把 ComparisonExpr 的处理独立为函数,再从原来的 case 里进行调用。

如果这个抽出来的函数还是非常的复杂呢?那就继续抽。大多数的程序逻辑都可以抽象为多个 step 的,只要稍经思考。

虽说圈复杂度是个好东西,不过也不用过度吹毛求疵。比如说某种语言的二元表达式本身操作符就有几十个(夸张一下)的话,那 switch 的 case 你怎么写都逃不掉的。非要满足定义的话,那我可以用类似下面的“查表”的方式来解决:

type handler func(string, string)

var handlerMap = map["string"]handler {
  "+" : plusHandler,
  "-" : minusHandler,
  "=" : equalHandler,
}

call:
handler := handlerMap[inputStr]
handler(a,b)

不过实际上我觉得这样写和去写一个switch case也没有什么太多的区别。。不用过度的吹毛求疵~如果你的handler也不多,没必要非得去查表。对于一般的业务代码,圈复杂度的死限确实是个好东西~可以逼着你把逻辑的粒度拆细,再组合起来。这样阅读代码的时候心智负担会小不少。

上面光说了好看的问题,那圈复杂度高了不好测的问题呢?

这个也是我自己的理解,每多一个 if,实际上你的测试 case 就可能是原来的 2 倍,这样的话你的圈复杂度高了以后可能在理论上都没有办法覆盖到所有的 case (虽然一般也没有人这么做)。理论的意思,例如你在一个函数里有 64 个 if,那么只从数学的角度讲需要 2 ^ 64 个 case,已经接近天文数字了。

说完了 goreportcard,再来说说 awesome go 里的另一个必要标准:测试覆盖率。

现在可以集成的测试覆盖率有 coveralls,或者gocover,两个东西其实都差不多,想集成 coveralls 直接改一下 travis 的 yml 文件就好,没什么难度。

所谓的测试覆盖率和测试的 case 完善度其实根本不是一回事,awesome go 的人也提到了,如果你只是为了 coverage 的
benchmark 指标去写测试的话,也可以很轻易地写出 100% 的覆盖率,但这样不行,他们会检查你的 test case。说白了,测试覆盖率就只是说你的 test case 时不是在 test 期间遍历了所有的代码(ast 的 node)。一段 if else 你完全可以用两个 case 来将所有的代码都覆盖到。但是覆盖到了之后能说明你的代码是正确的么?

并不能。

但是 test case 的完善度又是一件很难用指标去衡量的事情。这里其实挺矛盾的。

如果说我们为自己的系统写 test 的话,还是应该优先保证代码逻辑上的正确性,然后再去追求覆盖率。

达成了 80% 以上的测试覆盖率,保证测试都是正常的逻辑测试,并且解决了代码的圈复杂度。基本上你的项目就可以达到他们所谓的 quality standard 了。剩下的就自求多福吧~

Xargin

Xargin

If you don't keep moving, you'll quickly fall behind
Beijing