提示:本节课最终代码为:feature/s26

代码开发完之后,除了执行单元测试进行功能性检查外,还要执行静态代码检查以发现功能之外的问题。静态代码分析可以让你无需运行程序即可快速找到代码中潜藏的错误,有助于提高代码质量。通过静态代码检查,通常可以发现以下各类问题:

  • 代码规范: 可以检查出不符合代码规范的代码段,或者不符合最佳实践的代码段;

  • 安全: 可以检查出代码中有安全风险的代码段,例如:SQL 漏洞注入、任意 URL 跳转漏洞、反序列化漏洞等;

  • 潜在的缺陷: 可以检测出一些不规范的代码段,这些不规范的代码段可能会带来一些潜在的缺陷。例如:errcheck 静态代码检查工具会检查是否对函数调用返回的错误进行判错处理,一个没有处理错误的代码段,就可能是一个潜在的缺陷代码段;

  • 性能问题: 可以检测出可能会使程序性能变差的代码段。

  • 圈复杂度: 圈复杂度(Cyclomatic complexity)是一种衡量一个代码模块复杂程度的标准,也叫条件复杂度或循环复杂度。例如:代码可读性很差,单个函数代码很长,各种 if else、case 嵌套等。

  • 重复代码: 可以检测代码仓库中的重复代码,提高代码的复用率。

那么如何进行静态代码检查呢?靠走读代码,显然不行。业界当前标准的方法是通过静态代码检查工具来检查。本节课,就一起来看下如何进行静态代码检查。

如何进行静态代码检查?

这里,我们先来看下如何进行静态代码检查,分为以下 2 部分:

  1. 如何选择静态代码检查工具?

  2. 如何执行静态代码检查工具?

如何选择静态代码检查工具?

我们可以通过静态代码检查工具来检查代码,当前有很多静态代码检查工具,例如:go 命令自带的 vet 工具,可做以下代码检查:

  • 检查 Printf 中参数与格式字符串不对齐的问题;

  • bool 类型一直为 true 或 false 的情况;

  • 循环内异常使用 go 携程的问题;

  • 分支不可达的问题;

  • 未检查 err 就使用的问题。

此外,golangci-lintgo-toolsgo-criticgoimports 等工具也能够进行静态代码检查,你还可以在 Code Analysis 这里发现更多的静态代码检查工具。

此外,很多大公司,例如腾讯会指定公司级别的 Go 代码规范,针对规范中的每一项开发静态代码检查工具,使用工具来保证代码的规范性。在今后的项目开发中,你也可以根据需要开发自己的静态代码检查工具,这里有一篇开发文档可供你参考:Go语言如何自定义linter(静态检查工具)

当然还有一些第三方平台,可以提供静态代码检查,例如:DeepSourceCODING 的代码扫描工具等,你可以根据需要进行选择使用。

可以看到,你可以选择很多种静态代码检查工具,并使用它们的静态代码检查项来检查你的代码。在一开始,我曾经想在我的代码仓库中,集成我能接触到的所有静态代码检查工具,因为我觉得这样会对我的代码进行充分的检查。经过尝试,我失败了,原因如下:

  • 静态代码检查工具很多,每种工具都有自己的配置,维护起来比较麻烦;

  • 不同工具可能包含相同的检查项,导致重复检查;

  • 代码检查项越多,执行检查时耗时越久,这不利于快速集成;

  • 静态代码检查工具质量参差不齐,有些甚至不会再维护。

鉴于以上原因,我调研了一些业界比较受欢迎的静态代码工具,并选择了我认为比较优雅的静态代码检查方案。接下来,我来分享下我选择的静态代码检查工具以及原因。

我是如何进行静态代码检查的?

首先,我需要一款优秀的静态代码检查工具。一如既往,我在 GitHub 上尝试搜索静态代码检查工具,如下图所示:

20.项目管理:如何进行静态代码检查? - 图1

根据搜索结果,发现 golangci-lint 工具居然有 11.6k 的 Star 数,远远领先其他的静态代码检查工具。根据 Star 数,我几乎可以确认 golangci-lint 就是一款最优的静态代码检查工具。但为了能够找到真正的最优工具,我仍然调研了 Star 数靠前的检查工具,例如:go-toolsrevivegometalinter 等。有点强迫症的我,仍然在 Google 上搜索了 golang code analysis tools,并从几十篇文章中, 筛选并认真阅读了关联性比较强、内容优质的文章(不下于10篇)。

最终,我发现 golangci-lint 不仅 Star 数高,还有很多文章作为最佳工具进行推荐。经过这么多调研,我已经可以确信 golangci-lint 是静态代码检查工具中的最优选择。

目前,也有很多公司/项目使用了 golangci-lint 工具作为静态代码检查工具,例如 Google、Facebook、Istio、Red Hat OpenShift 等,这更加使我坚信选择 golangci-lint 是对的。

随后,我安装并测试了 golangci-lint,发现 golangci-lint 安装方便、易用、功能强大。为了能够全面的学习 golangci-lint,以使其发挥最大的作用,我详细阅读了 golangci-lint 的官网文档:Introduction | golangci-lint。发现 golangci-lint 具有以下优点:

  • golangci-lint 官网文档很详细,社区活跃度很高,几乎每隔几周都有一些新的 Linter 被加入,过时的 Linter 被标记移除。也可以说,golangci-lint 在替你维护各种 Linter;

  • golangci-lint 是一个功能强大的 Linter 工具聚合框架,里面内置了很多优秀的 Linter 可以直接使用。这些 Linter 几乎囊括了你能见到的所有优质 Linter,详见 Linters

  • 静态代码检查工具,因为需要分析代码语法,并进行规则检查,是一个很耗时的操作。在实际开发中,一个耗时的静态代码检查体验并不好。但 golangci-lint 具有 cache 机制,可以大大缩短静态代码检查的时间。提高检查效率的同时,也让开发者有一个良好的检查体验;

  • golangci-lint 具有很丰富的配置。这些配置非常有用,可以使你选择需要的检查项和检查深度。拥有一个你不想检查,但工具每次都检查报错的检查项,是一件非常讨厌人的事情,甚至会使你放弃进行静态代码检查;

  • golangci-lint 提供一个非常易读的检查输出,这些输出可以使你更快地定位问题行,并修复。golangci-lint 也支持输出不同的格式,例如:jsonhtmlgithub-actions。你可以选择合适的格式,进行再加工或者直接展示;

  • golangci-lint 支持灵活的 Linter 选择。例如:你可以指定运行某些 Linter,或者禁止运行某些 Linter;

  • golangci-lint 支持非常丰富的检查项,例如:bugscommentcomplexityerrorformatmetalintermoduleperformancesqlstyletestunused。更多的可以执行 golangci-lint help linters 命令查看;

  • golangci-lint 支持添加自定义 Linter。这个特性使 golangci-lint 极具扩展性,你可以根据需要添加任何你期望的 Linter。这个特性也使得 golangci-lint 成为一个公司级的静态代码检查工具;

  • golangci-lint 可以集成在各种 IDE 上,例如:Sublime Text、GoLand、Vim 等,方便你使用。

上面罗列了一些我觉得非常有用的特性,golangci-lint 的功能特性远不至于此,具体你可以详细阅读其官方文档进行学习。

如何执行静态代码检查工具?

选择了一款静态代码检查工具之后,你还要思考如何使用。你可以通过以下几种方式来使用:

  • 命令行运行,例如直接执行 golangci-lint run ./...

  • golangci-lint 集成进 Makefile 中,例如执行 make lint 运行;

  • golangci-lint 集成进公司的 CI/CD 平台中,在持续集成的过程中使用;

  • golangci-lint 集成进类似 GoLand、Vim 等 IDE 中运行。

你可以根据需要,选择你觉得适合的运行方式。在实际开发中,我更倾向于将 golangci-lint 集成进 Makefile 中。原因如下:

  • 集成进 Makefile 中,你可以很方便地在本地执行 make lint 进行静态代码检查,时刻感知到当前的代码状态;

  • 可以将 make lint 集成在 CI/CD 平台中,在持续集成的过程中运行。因为都是执行同一个 make lint 命令,所以和本地检查结果保持一致,不会出现本地检查成功但在 CI/CD 平台执行失败,这种不一致的情况;

  • 集成进 Makefile 中,所有开发者都通过执行 make lint 进行静态代码检查,操作方便,也能保持检查结果的一致性。

我并不习惯将静态代码检查工具集成在 IDE 中,因为在开发中,我更倾向于将所有的操作都通过唯一一个入口来进行,这样不仅能保持一致的执行结果,还能够最大化减少重复开发的工作量,提高开发效率。

golangci-lint 如何使用?

golangci-lint 官方文档非常全,至于怎么使用,你完全可以参考官方文档进行学习:Introduction | golangci-lint。这里我介绍下 miniblog 项目中是如何使用 golangci-lint 的。

要使用 golangci-lint,你首选需要安装它,安装方法如下:

  1. $ go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1

这里建议你选择一个最新的版本,并且指定版本安装(不建议使用 go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 这种方式安装)。因为最新的版本,可能会使你的配置失效或者报错。建议定期查看 golangci-lint 的 release note,根据需要更新版本,并适配配置。

安装完成之后,建议你配置 golangci-lint 自动补全功能,方便你使用。配置命令如下:

  1. $ golangci-lint completion bash > $HOME/.golangci-lint.bash
  2. $ $ if ! grep -q .golangci-lint.bash $HOME/.bashrc; then echo "source $$HOME/.golangci-lint.bash" >> $HOME/.bashrc; fi
  3. $ bash

安装完成后,我们就需要进行 Linter 配置。golangci-lint 提供了非常丰富的配置项,这些配置项可以实现下面几类功能。

  • golangci-lint本身的一些选项,比如超时、并发、是否检查*_test.go文件等。

  • 配置需要忽略的文件和文件夹。

  • 配置启用哪些 linter,禁用哪些 linter。

  • 配置输出格式。

  • golangci-lint 支持很多 linter,其中有些 linter 支持一些配置项,这些配置项可以在配置文件中配置。

  • 设置哪些文件可以忽略哪些 linter。

  • 设置错误严重级别,像日志一样,检查错误也是有严重级别的。

golangci-lint 官方仓库提供了一个示例配置:.golangci.reference.yml,我们可以基于 .golangci.reference.yml 进行修改。

我基于 .golangci.reference.yml 修改后的配置文件见:.golangci.yaml(feature/s26 分支)

miniblog 静态代码检查

上面,我介绍了静态代码检查的方法以及 golangci-lint 的使用方法。这里我们就基于 golangci-lint 来实现 miniblog 项目的静态代码检查功能。

我们可以通过以下几步,实现 miniblog 项目的静态代码检查功能:

  1. 安装 golangci-lint 工具;

  2. 配置 golangci-lint 配置文件;

  3. 执行 golangci-lint 命令对代码进行静态代码检查。为了提高操作效率,我们需要将 golangci-lint 执行命令集成到 Makefile 中;

  4. 根据 golangci-lint 的检查结果,更改修正代码。

接下来,我们根据这些步骤,来一步一步给 miniblog 添加静态代码检查功能。

  1. 安装 golangci-lint 工具。

安装命令如下(上文有提到,为了保证步骤的完整性,这里再提下):

  1. $ go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
  1. 配置 golangci-lint 配置文件。

我们可以根据需要基于 .golangci.reference.yml 文件进行修改,修改后的配置文件见:.golangci.yaml(feature/s26 分支)

  1. 在 Makefile 中集成静态代码检查操作。

在 Makefile 中添加 lint 目标:

  1. .PHONY: lint
  2. lint: ## 执行静态代码检查.
  3. @echo "===========> Run golangci to lint source codes"
  4. @golangci-lint run -c ./.golangci.yaml ./...

为了能在执行 make 时,能够也进行静态代码检查,我们需要将 lint 目标添加到 all 目标的依赖中:

  1. all: add-copyright format lint cover build

这里需要注意 lintall 目标的执行顺序,我个人感觉将 lint 放在 format 后,可能会避免一些格式化的 linter(如有)检查失败,放在 cover 之前,可以确保我们测试的是一个经过静态代码检查的、相对稳定的代码状态。

  1. 根据 golangci-lint 的检查结果,更改修正代码。

上面,我们已经在 Makefile 集成了静态代码检查功能。接下来,我们就可以通过 make 来检查代码,并根据输出修复代码:

  1. $ make lint
  2. ===========> Run golangci to lint source codes
  3. internal/miniblog/miniblog.go:130:2: sigchanyzer: misuse of unbuffered os.Signal channel as argument to signal.Notify (govet)
  4. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
  5. ^
  6. make: *** [Makefile:109: lint] Error 1

上面的输出,告诉我们:internal/miniblog/miniblog.go 文件的第 130 行代码,用了无缓冲的 channel。这里,我们首先理解错误原因,接着修复错误。修复后的代码如下(见 miniblog.go#L126):

  1. // 创建并运行 GRPC 服务器
  2. grpcsrv := startGRPCServer()
  3. // 等待中断信号优雅地关闭服务器(10 秒超时)。
  4. quit := make ( chan os.Signal, 1 )
  5. // kill 默认会发送 syscall.SIGTERM 信号
  6. // kill -2 发送 syscall.SIGINT 信号,我们常用的 CTRL + C 就是触发系统 SIGINT 信号
  7. // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它

接下来,我们可以继续修复其他检查错误。为了提高开发效率,最好阶段性进行静态代码检查,例如:项目发布前。没必要每一次变更都执行一次静态代码检查,过早优化带来的更多是开发效率的降低。

小结

Go 项目开发中,对代码进行静态代码检查是必要的操作。当前有很多优秀的静态代码检查工具,但 golangci-lint 因为具有检查速度快、可配置、少误报、内置了大量 linter 等优点,成为了目前最受欢迎的静态代码检查工具。本节课后半部分给你展示了如何添加静态代码检查功能。