Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?
Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。
自从上了 Reviewbot 之后,我发现有些 lint 错误,还是很容易出现的。比如
这两个检查,都是圈复杂度相关。
圈复杂度(Cyclomatic complexity)是由 Thomas McCabe 提出的一种度量代码复杂性的指标,用于计算程序中线性独立路径的数量。它通过统计程序控制流中的判定节点(如 if、for、while、switch、&&、|| 等)来计算。圈复杂度越高,表示代码路径越多,测试和维护的难度也就越大。
圈复杂度高的代码,往往意味着代码的可读性和可维护性差,非常容易出 bug。
为什么这么说呢?其实就跟人脑处理信息一样,一件事情弯弯曲曲十八绕,当然容易让人晕。
所以从工程实践角度,我们希望代码的圈复杂度不能太高,毕竟绝大部分代码不是一次性的,是需要人来维护的。
那该怎么做呢?
这里我首先推荐一个简单有效的方法:Early return。
Early return - 逻辑展平,减少嵌套
Early return, 也就是提前返回,是我个人认为最简单,日常很多新手同学容易忽视的方法。
举个例子:
这段代码的逻辑应该挺简单的,但嵌套层级有点多,如果以后再复杂一点,就容易出错。
这种情况就可以使用 early return 模式改写,把这个嵌套展平:
是不是清晰很多,看着舒服多了?
记住这里的诀窍:如果你觉得顺向思维写出的代码有点绕,且嵌套过多的话,就可以考虑使用 early return 来反向展平。
当然,严格意义上讲,early return 只能算是一种小技巧。要想写出高质量的代码,最重要的还是理解 分层、组合、单一职责、高内聚低耦合、SOLID 原则等 这些核心设计理念 和 设计模式了。
Functional Options 模式 - 参数解耦
来看一个场景: 方法参数很多,怎么办?
比如这种:
有一堆参数,而且还是同类型的。如果在调用时,一不小心写错了参数位置,就很麻烦,因为编译器并不能检查出来。
当然,即使不是同类型的,参数多了可能看着也不舒服。
怎么解决?
这种情况,可以选择将参数封装成一个结构体,这样在使用时就会方便很多。封装成结构体后还有一个好处,就是以后增删参数时(结构体的属性),方法签名不需要修改。避免了以前需要改方法签名时,调用方也需要跟着到处改的麻烦。
不过,在 Go 语言中,还有一种更优雅的解决方案,那就是 Functional Options 模式。
不管是 Rob Pike 还是 Dave Cheney 以及 uber 的 go guides 中都有专门的推荐。
https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
https://github.com/uber-go/guide/blob/master/style.md#functional-options
这种模式,本质上就是利用了闭包的特性,将参数封装成一个匿名函数,有诸多妙用。
Reviewbot 自身的代码中,就有相关的使用场景(https://github.com/qiniu/reviewbot/blob/c354fde07c5d8e4a51ddc8d763a2fac53c3e13f6/internal/lint/providergithub.go#L263),比如:
这里的 options
就是 functional options 模式,可以灵活地传入不同的参数。
当时之所以选择这种写法,一个重要的原因是方便单测书写。
为什么这么说呢?
看上述代码能知道,它需要调用 github api 去获取 changed files
, 这种实际依赖外部的场景,在单测时就很麻烦。但是,我们用了 functional options 模式之后,就可以通过 p.PullRequestChangedFiles
是否为 nil 这个条件,灵活的绕过这个问题。
Functional Options 模式的优点还有很多,总结来讲(from dave.cheney):
Functional options let you write APIs that can grow over time.
They enable the default use case to be the simplest.
They provide meaningful configuration parameters.
Finally they give you access to the entire power of the language to initialize complex values.
现在大模型相关的代码,能看到很多 functional options 的影子。比如https://github.com/tmc/langchaingo/blob/238d1c713de3ca983e8f6066af6b9080c9b0e088/llms/ollama/options.go#L25
所以建议大家在日常写代码时,也多有意识的尝试下。
善用 Builder 模式/策略模式/工厂模式,消弭复杂 if-else
Reviewbot 目前已支持两种 provider(github 和 gitlab),以后可能还会支持更多。
而因为不同的 Provider 其鉴权方式还可能不一样,比如:
github 目前支持 Github APP 和 Personal Access Token 两种方式
gitlab 目前仅支持 Personal Access Token 方式
当然,还有 OAuth2 方式,后面 reviewbot 也也会考虑支持。
那这里就有一个问题,比如在 clone 代码时,该使用哪种方式?代码该怎么写?使用 token 的话,还有个 token 过期/刷新的问题,等等。
如果使用 if-else 模式来实现,代码就会变得很复杂,可读性较差。类似这种:
但现在 Reviewbot 的代码中,相关代码仅两行:
怎么做到的呢?
其实是使用了 builder 模式,将 git 的配置和创建过程封装成一个 builder,然后根据不同的 provider 选择不同的 builder,从而消弭了复杂的 if-else 逻辑。
当然内部细节还很多,不过核心思想都是将复杂的逻辑封装起来,在主交互逻辑中,只暴露简单的使用接口,这样代码的可读性和可维护性就会大大提高。
最后
到底如何写出高质量的代码呢?这可能是很多有追求的工程师,一直在思考的问题。
在我看来,可能是没有标准答案的。不过呢,知道一些技巧,并能在实战中灵活运用,总归是好的。
你说是吧?
版权声明: 本文为 InfoQ 作者【大卡尔】的原创文章。
原文链接:【http://xie.infoq.cn/article/14a174a37026c5c5281d6fd2d】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论