Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?
Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地。
自从上了 Reviewbot 之后,我发现有些 lint 错误,还是很容易出现的。比如
dao/files_dao.go:119:2: `if state.Valid() || !start.IsZero() || !end.IsZero()` has complex nested blocks (complexity: 6) (nestif)
cognitive complexity 33 of func (*GitlabProvider).Report is high (> 30) (gocognit)
这两个检查,都是圈复杂度相关。
圈复杂度(Cyclomatic complexity)是由 Thomas McCabe 提出的一种度量代码复杂性的指标,用于计算程序中线性独立路径的数量。它通过统计程序控制流中的判定节点(如 if、for、while、switch、&&、|| 等)来计算。圈复杂度越高,表示代码路径越多,测试和维护的难度也就越大。
圈复杂度高的代码,往往意味着代码的可读性和可维护性差,非常容易出 bug。
为什么这么说呢?其实就跟人脑处理信息一样,一件事情弯弯曲曲十八绕,当然容易让人晕。
所以从工程实践角度,我们希望代码的圈复杂度不能太高,毕竟绝大部分代码不是一次性的,是需要人来维护的。
那该怎么做呢?
这里我首先推荐一个简单有效的方法:Early return。
Early return - 逻辑展平,减少嵌套
Early return, 也就是提前返回,是我个人认为最简单,日常很多新手同学容易忽视的方法。
举个例子:
func validate(data *Data) error {
if data != nil {
if data.Field != "" {
if checkField(data.Field) {
return nil
}
}
}
return errors.New("invalid data")
}
这段代码的逻辑应该挺简单的,但嵌套层级有点多,如果以后再复杂一点,就容易出错。
这种情况就可以使用 early return 模式改写,把这个嵌套展平:
func validate(data *Data) error {
if data == nil {
return errors.New("data is nil")
}
if data.Field == "" {
return errors.New("field is empty")
}
if !checkField(data.Field) {
return errors.New("field validation failed")
}
return nil
}
是不是清晰很多,看着舒服多了?
记住这里的诀窍:如果你觉得顺向思维写出的代码有点绕,且嵌套过多的话,就可以考虑使用 early return 来反向展平。
当然,严格意义上讲,early return 只能算是一种小技巧。要想写出高质量的代码,最重要的还是理解 分层、组合、单一职责、高内聚低耦合、SOLID 原则等 这些核心设计理念 和 设计模式了。
Functional Options 模式 - 参数解耦
来看一个场景: 方法参数很多,怎么办?
比如这种:
func (s *Service) DoSomething(ctx context.Context, a, b, c, d int) error {
// ...
}
有一堆参数,而且还是同类型的。如果在调用时,一不小心写错了参数位置,就很麻烦,因为编译器并不能检查出来。
当然,即使不是同类型的,参数多了可能看着也不舒服。
怎么解决?
这种情况,可以选择将参数封装成一个结构体,这样在使用时就会方便很多。封装成结构体后还有一个好处,就是以后增删参数时(结构体的属性),方法签名不需要修改。避免了以前需要改方法签名时,调用方也需要跟着到处改的麻烦。
不过,在 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),比如:
// GithubProviderOption allows customizing the provider creation.
type GithubProviderOption func(*GithubProvider)
func NewGithubProvider(ctx context.Context, githubClient *github.Client, pullRequestEvent github.PullRequestEvent, options ...GithubProviderOption) (*GithubProvider, error) {
// ...
for _, option := range options {
option(p)
}
// ...
if p.PullRequestChangedFiles == nil {
// call github api to get changed files
}
// ...
}
这里的 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
type Option func(*options)
// WithModel Set the model to use.
func WithModel(model string) Option {
return func(opts *options) {
opts.model = model
}
}
// WithFormat Sets the Ollama output format (currently Ollama only supports "json").
func WithFormat(format string) Option {
// ...
}
// If not set, the model will stay loaded for 5 minutes by default
func WithKeepAlive(keepAlive string) Option {
// ...
}
所以建议大家在日常写代码时,也多有意识的尝试下。
善用 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 模式来实现,代码就会变得很复杂,可读性较差。类似这种:
if provider == "github" {
// 使用 Github APP 方式
if githubClient.AppID != "" && githubClient.AppPrivateKey != "" {
// 使用 Github APP 方式
// 可能需要调用 github api 获取 token
} else if githubClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要调用 github api 获取 token
} else {
return err
}
} else if provider == "gitlab" {
// 使用 Personal Access Token 方式
if gitlabClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要调用 gitlab api 获取 token
} else {
return errors.New("gitlab personal access token is required")
}
}
但现在 Reviewbot 的代码中,相关代码仅两行:
func (s *Server) handleSingleRef(ctx context.Context, ref config.Refs, org, repo string, platform config.Platform, installationID int64, num int, provider lint.Provider) error {
// ...
gb := s.newGitConfigBuilder(ref.Org, ref.Repo, platform, installationID, provider)
if err := gb.configureGitAuth(&opt); err != nil {
return fmt.Errorf("failed to configure git auth: %w", err)
}
// ...
}
怎么做到的呢?
其实是使用了 builder 模式,将 git 的配置和创建过程封装成一个 builder,然后根据不同的 provider 选择不同的 builder,从而消弭了复杂的 if-else 逻辑。
当然内部细节还很多,不过核心思想都是将复杂的逻辑封装起来,在主交互逻辑中,只暴露简单的使用接口,这样代码的可读性和可维护性就会大大提高。
最后
到底如何写出高质量的代码呢?这可能是很多有追求的工程师,一直在思考的问题。
在我看来,可能是没有标准答案的。不过呢,知道一些技巧,并能在实战中灵活运用,总归是好的。
你说是吧?
Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?的更多相关文章
- js脚本代码调试小技巧
以前写js代码调试代码查看数据是否正确的时候不知道F12(开发者工具),都是alert(xxx)或者console.log(xxx), 现在知道还可以用document.write或者try...ca ...
- 写漂亮C#代码的小技巧
第一次写博客,不知道代码用什么编辑,直接截图了,哈哈哈.... 我自己不喜欢看随便复制粘贴过来一堆代码的博客,所以,用些简单点的例子吧,希望对大家有帮助... ------------------- ...
- VS开发中的代码编写小技巧——避免重复代码编写的几种方法
上一篇文章中程序员的幸福生活--有你的日子,每天都是情人节,收到了大家的很多好评.鼓励和祝福,非常感动,真诚的谢谢大家.也希望每个朋友都能保持一个积极向上的心态,去迎接丰富多彩的人生. 在开发过程中, ...
- 优化C/C++代码的小技巧
说明: 无意看到一篇小短文,猜测作者应该是一个图形学领域的程序员或专家,介绍了在光线(射线)追踪程序中是如何优化C/C++代码的.倒也有一些参考意义,当然有的地方我并不赞同或者说我也不完全理解,原文在 ...
- 优化C/C++代码的小技巧(转)
源:http://www.cnblogs.com/lizhenghn/p/3969531.html 说明: 无意看到一篇小短文,猜测作者应该是一个图形学领域的程序员或专家,介绍了在光线(射线)追踪程序 ...
- 30段极简Python代码:这些小技巧你都Get了么
学 Python 怎样才最快,当然是实战各种小项目,只有自己去想与写,才记得住规则.本文是 30 个极简任务,初学者可以尝试着自己实现:本文同样也是 30 段代码,Python 开发者也可以看看是不是 ...
- Visual Studio模板代码注释小技巧分享
在日常开发过程中,难免有这样一种需求:就是你所建的每一个类文件或者接口文件都需要标注下作者姓名以及类的用途.如果我们每次创建文件的时候都需要写一遍这些信息是很烦神的.还好Visual Studio给我 ...
- Visual Studio模板代码注释小技巧
在日常开发过程中,难免有这样一种需求:就是你所建的每一个类文件或者接口文件都需要标注下作者姓名以及类的用途.如果我们每次创建文件的时候都需要写一遍这些信息是很烦神的.还好Visual Studio给我 ...
- 写Selenium代码时一些技巧
本文地址: https://www.cnblogs.com/hchengmx/p/10880002.html 1. Chrome插件之"CSS Selector Helper for Chr ...
- phpstorm用正则删除PHP代码空行小技巧
有很多小伙伴会遇到代码空行特别多,但是一行一行删除肯定很烦躁,这时候就需要用到批量删除空行. 怎么批量删除空行呢? 我的办法是用正则把所有空行找到,然后一键全部替换. 首先把Match Case和Re ...
随机推荐
- Nuxt Kit 组件管理:注册与自动导入
title: Nuxt Kit 组件管理:注册与自动导入 date: 2024/9/15 updated: 2024/9/15 author: cmdragon excerpt: Nuxt Kit 为 ...
- SpringBoot系列:使用原生JDBC实现对数据库的增删改查
application.yml spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306 ...
- 报名开启|和你约定在 "Google 应用出海指南针"
Google 谷歌发布的<2021 移动应用全球化指南>报告中提到,在移动互联网高度渗透的当下,全球用户迸发出来的广泛需求为中国新生代创业者带来了巨大的历史机遇.过去十年,依托于技术和人才 ...
- ARMv8 寄存器
本文主要介绍 Armv8/v9 指令集架构中常用部分,详细的还是要看 Arm architecture reference manual. ARMv8 架构 ARMv8 架构支持3种指令集: T32, ...
- Linux 循环设备 loop疑惑
什么是loop设备? loop设备是一种伪设备,是使用文件来模拟块设备的一种技术,文件模拟成块设备后, 就像一个磁盘或光盘一样使用.在使用之前,一个 loop 设备必须要和一个文件进行连接.这种结合方 ...
- 对于ChannelNet的一点理解
主要是为了个人理解,做个笔记 1.Pytorch代码 2.论文出处 3.详细介绍 这篇论文在2018年发出来,而ShuffleNet是从2017年由旷视发出来.起初了解shufflenet的提出,主要 ...
- python中利用变量解压列表、元组、字符串、字典、文件对象、迭代器和生成器等序列
一.如果知道序列中元素的个数,可以直接进行变量赋值. coords = (102, 40) lon, lat = coords print(lon) print(lat) text = "n ...
- (系列六).net8 全局异常捕获机制
说明 该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发). 该系统文章,我会尽量说的非常详细,做到不管新手.老手都能看懂. 说明:OverallAuth2 ...
- dp线段树优化
题目:Potted Flower Description The little cat takes over the management of a new park. There is a larg ...
- 生成文本聚类java实现1
本章主要的学习是中文分词 和两种统计词频(传统词频和TF-IDF算法 ) 的方法. 学习目的:通过N多的新闻标题 or 新闻摘要 or 新闻标签,生成基本的文本聚类,以便统计当天新闻的热点内容. 扩展 ...