Golang 聊聊最经典的 linter—— golangci-lint 怎么用
为什么需要 lint 工具
在国内互联网超高速迭代的研发效率催动下,其实线上服务是很容易出问题的。紧接着就是复盘,跟进 TODO。最后不了了之,唯一能保证的只是把造成事故的直接原因给扼杀掉。
Why?
因为落地难!此前在 一个程序员的成长思考 里我就提过,靠人工,靠意识来保障的制度是非常脆弱的,基本不可能落地。
要改变流程,一定是痛苦的,需要有卡点,需要有自动化流程。大家虽然痛苦,但遵守规范来做,并不断发现工具的问题,修复,迭代,到最后慢慢变熟练,流程才能固化。
Code Review 尤其是如此!
- 发现需要review的代码里 import 没有按照规范,说不说?
- 发现有一堆 magic number,说不说?
- 发现一堆包内使用的小函数全是大写开头的,说不说?
- 发现有 deadcode,说不说?
说!有什么不能说的,reviewer 不就是来指出问题的么?
但是,总要有个但是。像上面这样列举的点,对于没有形成非常好规范的 Gopher 来说,可能一个300行的 Merge Request 里面能有二三十处,reviewer 的关注点全在这些事情上,哪儿还能有精力分配给真正重要的业务逻辑呢?这对 reviewer 来说是巨大的时间浪费。可如果不指出来,就又走入了【破窗效应】的循环里。因为有一个妥协了,就可以再妥协一个,直到最后,慢慢代码变成了【屎山】,放弃吧,咱开个新库,整体大重构。
这样的问题的解决其实很简单:自动化流程,用 lint 卡点来限制 Merge Request,没有过卡点的一律不 review,由 committer 自己来解决代码风格问题。如果觉得某一条规则有问题,提出来,我们调整 lint 规则。如果确实无法达到标准,加上 nolint,都ok,但一定要把这一步交给自动化流程。
这样带来的好处是很鲜明:
- 开发者自身可以学习到很多规范,知道怎样写是最有利于长远维护,有利于可读性的,自身素质提高;
- 降低 reviewer 心智负担,只关注整体代码,接口,流程,测试即可,不必被基础代码风格问题所耽误;
- 长远看维护更容易,把低级问题提前消除,有助于团队合作;
- 形成正循环,有后续规范了,先添加 lint 规则,形成卡点,非常容易落地,有问题了改工具配置即可,而不是靠人工。
Golang 有哪些 lint 工具
作为 Gopher,我们有官方提供的 go fmt 工具能够完成基础的格式调整(当然,这远远不够),开源社区里提供的各种 linter 还是非常多,而且有效的。
这里也推荐一个 linter 列表:github.com/golangci/aw…
- gofmt,Go SDK自带的代码格式检查工具,用于检查缩进、文件尾是否有空行等。
go fmt
命令(即gofmt -l -w
)可使用该工具格式化代码。 - go vet,Go SDK自带的可疑代码检查工具,检查unsafe.Pointer的错误使用、unreacheable code等可能有问题的代码。
- golangci-lint,社区提供的linters聚合运行工具,内置了多个linter(代码检查工具),支持配置化&并行运行。
具体的 lint 规则非常多,而且是一个动态增加的过程,作为业务研发,我们希望能有一个统一的载体,让我们可以直接用一个 linter 就能运行各种规则,并且要可配置。这方面业界公认最出色的还是 golangci-lint,下面我们就来了解一下。
golangci-lint
golangci-lint是一个Go linters aggregator,并不是一个linter,其作用在于将每个独立的linter聚合起来得到更好的体验和满足不同的需求。
特色功能
- 并行跑各个 linter,重复利用 Golang 的构建缓存,并缓存分析结果;
- 提供 yaml 配置,自定义你需要的 linter 以及规则;
- 跟 VSCode,Sublimt Text, Golang, Emacs, Vim, GitHub Actions 进行集成,你可以开箱即用;
- 自带常用 linter,无需开发者再次安装;
- 输出的问题有颜色区分,标识源码所在行以及出问题的标识符。
感兴趣的同学也可以参考官方文档 golangci-lint.run/usage/quick…
安装指南
如果是希望在 Github 用,可以走 Github Action 直接来限制,大大简化接入流程,参考 官方流程
这里我们看一下本地怎样跑代码,在提 MR 之前就发现问题。
- Linux
# binary will be $(go env GOPATH)/bin/golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
- MacOS
brew install golangci-lint
brew upgrade golangci-lint
安装完成后,执行 golangci-lint --version
就可以看到版本信息,代表安装成功。详细流程
支持的linter
使用 golangci-lint help linters
命令就可以看到所有支持的 linter
$ golangci-lint help linters
Enabled by default linters:
deadcode: Finds unused code [fast: false, auto-fix: false]
errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
gosimple (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false]
govet (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
ineffassign: Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
staticcheck (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
typecheck: Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false]
unused (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
varcheck: Finds unused global variables and constants [fast: false, auto-fix: false]
默认配置里生效的规则如下:
建议大家至少对这里涉及的 linter 设置为必须通过,如果连 govet, errcheck 这些都过不了,流到线上一定会给以后埋坑。
在此之外,业务可以根据自身诉求,从 linter 列表中选择符合要求的规则。这里建议大家默认也加一条圈复杂度的规则: gocyclo,它代表圈复杂度,一般我们希望控制在 10-20。再高就说明函数拆分有问题,或者里面的实现比较冗余。
linters-settings:
gocyclo:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 10
更多细节的 linter 配置参照说明文档:golangci-lint.run/usage/linte…
实战用法
golangci-lint run // 等价于 golangci-lint run ./...
golangci-lint run dir1 dir2/... dir3/file1.go // 选择部分目录或者文件执行
golangci-lint linters // 查看开启的 linter
运行 golangci-lint linters
的结果我们来看一下:
如果我们希望自己来指定规则,而不是走默认规则,需要怎么配置呢?
首先,在你的项目根目录添加配置文件,用下面哪一个后缀都ok:
.golangci.yml
.golangci.yaml
配置文件的格式如下(详细参考说明文档)
# Options for analysis running.
run:
# See the dedicated "run" documentation section.
option: value
# output configuration options
output:
# See the dedicated "output" documentation section.
option: value
# All available settings of specific linters.
linters-settings:
# See the dedicated "linters-settings" documentation section.
option: value
linters:
# See the dedicated "linters" documentation section.
option: value
issues:
# See the dedicated "issues" documentation section.
option: value
severity:
# See the dedicated "severity" documentation section.
option: value
这里我们主要关注三个:
-
run:控制的是lint运行的选项,如并发数,超时时间,是否包含test文件,需要忽略的路径,文件,Golang 版本;
-
linters:选择你希望启用的linter,示例:
linters:
# Disable all linters.
# Default: false
disable-all: true
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default-linters
enable:
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- godox
- goerr113
- gofmt
- gofumpt
- tenv
- whitespace
- wrapcheck
- wsl
# Enable all available linters.
# Default: false
enable-all: true
# Disable specific linter
# https://golangci-lint.run/usage/linters/#disabled-by-default-linters--e--enable
disable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- contextcheck
- cyclop
- deadcode
- decorder
- wsl
# Enable presets.
# https://golangci-lint.run/usage/linters
presets:
- bugs
- comment
- complexity
- test
- unused
# Run only fast linters from enabled linters set (first run won't be fast)
# Default: false
fast: true
- linters-settings:具体每个 linter 的配置,类似我们上面对 gocyclo 配置最低圈复杂度。
我们在工作路径增加.golangci.yml
,配置自定义的规则。
linters:
# Disable all linters.
# Default: false
disable-all: true
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default-linters
enable:
# default linter
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- varcheck
# 新增 linter
- gocyclo
- gofmt
- goimports
# Run only fast linters from enabled linters set (first run won't be fast)
# Default: false
fast: true
linters-settings:
gocyclo:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 10
再执行 golangci-lint run
之后,就能看到输出的结果了。
nolint
有些时候因为一些特殊原因,比如某个函数无法更改,或至少在某个时段暂时达不到 linter 标准,我们希望跳过格式检查,怎么办呢?
这个时候可以使用 nolint 命令。
nolint 有两个层面的问题需要考虑:
- 规则范围;
- 生效范围。
前者指的是你需要排除哪些 linter 规则的影响,比如我只想针对 gocyclo 策略忽略 linter,就可以直接加上 //nolint:gocyclo
即可。
后者描述的是规则生效范围,比如是针对一行,还是一个代码块,还是一个文件。事实上 nolint 对这些层级都提供了支持。
- 行级别
var bad_name int //nolint:all
var bad_name int //nolint:golint,unused
注意,这里 all 代表着所有 linter 规则都忽略。你也可以指定要忽略的 linter,用逗号分隔。
- 函数/代码块级别
//nolint:all
func allIssuesInThisFunctionAreExcluded() *string {
// ...
}
//nolint:govet
var (
a int
b int
)
- 文件级别
//nolint:unparam
package pkg
同时,仅仅一个 nolint 很有可能是迷惑性的,就像很多 TODO, FIXME,我们也不知道谁留的,不知道什么时候 DO,FIX。不知道什么时候让这个函数/行/文件达到 linter 要求。
所以,建议在 nolint 之后加上说明,类似这样:
//nolint:gocyclo // This legacy function is complex but the team too busy to simplify it
func someLegacyFunction() *string {
// ...
}
machine-readable comments should have no space by Go convention
最后一个关键点,注意我们上面的 nolint 都是紧跟着 // 注释符号的,这也是 Go 的规范,机器识别的注释不应该有空格(开发者自己看的是需要加一个空格的)。
总结
今天我们对 golangci-lint 的用法,以及lint这件事整体的定位进行了学习。大家只要记住它是一个 linter 的聚合器即可,具体用法大家可以慢慢操练起来。要形成落地的规范,一定要有卡点,原则上需要修改所有报出的问题后才可以 submit。
建议大家关注 golangci-lint 官网 以及 源码仓库 更新,把 lint 用起来,提高组里代码水平,降低 reviewer 心智负担。
同时,golangci-lint 也支持社区成员贡献新的 linter,这里我们不再多说,感兴趣的同学可以来这里看一下:golangci-lint.run/contributin…
感谢阅读!有想法欢迎在评论区交流!
转载自:https://juejin.cn/post/7130188153792495630