第9节 静态代码检测
❤️💕💕During the winter vacation, I followed up and learned two projects: tiktok project and IAM project, and summarized and practiced the CloudNative project and Go language. I learned a lot in the process.Myblog:http://nsddd.top
[TOC]
选择 golangci-lint 做静态代码检查
在做 Go 项目开发的过程中,我们肯定需要对 Go 代码做静态代码检查。虽然 Go 命令提供了 go vet 和 go tool vet,但是它们检查的内容还不够全面,我们需要一种更加强大的静态代码检查工具。
golangci-lint
,是目前使用最多,也最受欢迎的静态代码检查工具。
优点:
- 速度非常快:golangci-lint 是基于 gometalinter 开发的,但是平均速度要比 gometalinter 快 5 倍。golangci-lint 速度快的原因有三个:可以并行检查代码;可以复用 go build 缓存;会缓存分析结果。
- 可配置:支持 YAML 格式的配置文件,让检查更灵活,更可控。
- IDE 集成:可以集成进多个主流的 IDE,例如 VS Code、GNU Emacs、Sublime Text、Goland 等。
- linter 聚合器:1.41.1 版本的 golangci-lint 集成了 76 个 linter,不需要再单独安装这 76 个 linter。并且 golangci-lint 还支持自定义 linter。
- 最小的误报数:golangci-lint 调整了所集成 linter 的默认设置,大幅度减少了误报。
- 良好的输出:输出的结果带有颜色、代码行号和 linter 标识,易于查看和定位。
下图是一个 golangci-lint 的检查结果:
你可以看到,输出的检查结果中包括如下信息:
- 检查出问题的源码文件、行号和错误行内容。
- 出问题的原因,也就是打印出不符合检查规则的原因。
- 报错的 linter。
通过查看 golangci-lint 的输出结果,可以准确地定位到报错的位置,快速弄明白报错的原因,方便开发者修复。
除此之外,golangci-lint 还有一个非常大的优点:当前更新迭代速度很快,不断有新的 linter 被集成到 golangci-lint 中。有这么全的 linter 为你的代码保驾护航,你在交付代码时肯定会更有自信。
命令
在使用之前,首先需要安装 golangci-lint。golangci-lint 的安装方法也很简单,你只需要执行以下命令,就可以安装了。
$ go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1
$ golangci-lint version # 输出 golangci-lint 版本号,说明安装成功
golangci-lint has version v1.50.1 built from (unknown, mod sum: "h1:C829clMcZXEORakZlwpk7M4iDw2XiwxxKaG504SZ9zY=") on (unknown)
这里注意,为了避免安装失败,强烈建议你安装 golangci-lint releases page
中的指定版本,例如 v1.41.1。
除此之外,我们应该定期的检查 golangCI 的版本。
golangci-link
支持的子命令如下:
此外,golangci-lint
还支持一些全局选项。全局选项是指适用于所有子命令的选项,golangci-lint
支持的全局选项如下:
run 命令
run 命令执行 golangci-lint
,对代码进行检查,是 golangci-lint
最为核心的一个命令。run 没有子命令,但有很多选项。run 命令的具体使用方法,我会在讲解如何执行静态代码检查的时候详细介绍。
cache 命令
cache 命令用来进行缓存控制,并打印缓存的信息。它包含两个子命令:
clean 用来清除 cache,当我们觉得 cache 的内容异常,或者 cache 占用空间过大时,可以通过
golangci-lint cache clean
清除cache
。status 用来打印 cache 的状态,比如 cache 的存放目录和 cache 的大小,例如:
$ golangci-lint cache status Dir: /home/colin/.cache/golangci-lint Size: 773.4KiB
completion 命令
completion 命令包含 4 个子命令 bash、fish、powershell 和 zsh,分别用来输出 bash、fish、powershell 和 zsh 的自动补全脚本。
下面是一个配置 bash 自动补全的示例:
$ golangci-lint completion bash > ~/.golangci-lint.bash
$ echo "source '$HOME/.golangci-lint.bash'" >> ~/.bashrc
$ source ~/.bashrc
执行完上面的命令,键入如下命令,即可自动补全子命令:
$ golangci-lint comp<TAB>
上面的命令行会自动补全为 golangci-lint completion
。
下面是一个 zsh 自动补全的示例:
$ golangci-lint completion zsh > ~/.golangci-lint.zsh
$ echo "source '$HOME/.golangci-lint.zsh'" >> ~/.zshrc
$ source ~/.zshrc
config 命令
config 命令可以打印 golangci-lint 当前使用的配置文件路径,例如:
$ golangci-lint config path
.golangci.yaml
linters 命令
linters 命令可以打印出 golangci-lint
所支持的 linter,并将这些 linter 分成两类,分别是配置为启用的 linter 和配置为禁用的 linter,例如:
$ golangci-lint linters
Enabled by your configuration linters:
...
deadcode: Finds unused code [fast: true, auto-fix: false]
...
Disabled by your configuration linters:
exportloopref: checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
...
golangci-lint 配置
和其他 linter 相比,golangci-lint 一个非常大的优点是使用起来非常灵活,这要得益于它对自定义配置的支持。
golangci-lint 支持两种配置方式,分别是命令行选项和配置文件。如果 bool/string/int 的选项同时在命令行选项和配置文件中被指定,命令行的选项就会覆盖配置文件中的选项。如果是 slice 类型的选项,则命令行和配置中的配置会进行合并。
golangci-lint run 支持很多命令行选项,可通过golangci-lint run -h查看,这里选择一些比较重要的选项进行介绍,见下表:
选项 | 简介 |
---|---|
--config | 指定golangci-lint的配置文件路径 |
--deadline | 指定lint的最大执行时间,超时则退出 |
--disable | 禁用指定的lint检查 |
--enable | 启用指定的lint检查 |
--exclude | 排除指定的文件或目录 |
--fix | 修复lint能够自动修复的问题 |
--issues-exit-code | 指定lint存在问题时的退出码 |
--print-issued-lines | 打印具体哪一行存在问题 |
--skip-dirs | 跳过指定的目录 |
--skip-files | 跳过指定的文件 |
--timeout | 指定lint的最大执行时间,超时则退出 |
--tests | 包括测试文件 |
--vendor | 包括vendor目录下的代码 |
--verbose | 显示详细的输出信息 |
以上是一些常用的golangci-lint命令行选项,包括指定配置文件路径、控制lint执行的时间和范围、禁用或启用某些检查、修复能够自动修复的问题等等。这些选项可以帮助我们更好地控制lint的行为,提高代码质量和开发效率。
此外,我们还可以通过 golangci-lint
配置文件进行配置,默认的配置文件名为.golangci.yaml
、.golangci.toml
、.golangci.json
,可以通过-c
选项指定配置文件名。通过配置文件,可以实现下面几类功能:
- golangci-lint 本身的一些选项,比如超时、并发,是否检查*_test.go文件等。
- 配置需要忽略的文件和文件夹。
- 配置启用哪些 linter,禁用哪些 linter。
- 配置输出格式。
- golangci-lint 支持很多 linter,其中有些 linter 支持一些配置项,这些配置项可以在配置文件中配置。
- 配置符合指定正则规则的文件可以忽略的 linter。
- 设置错误严重级别,像日志一样,检查错误也是有严重级别的。
更详细的配置内容,你可以参考Configuration。另外,你也可以参考 IAM 项目的 golangci-lint 配置.golangci.yaml
。.golangci.yaml
里面的一些配置,我建议你一定要设置,具体如下:
run:
skip-dirs: # 设置要忽略的目录
- util
- .*~
- api/swagger/docs
skip-files: # 设置不需要检查的go源码文件,支持正则匹配,这里建议包括:_test.go
- ".*\\.my\\.go$"
- _test.go
linters-settings:
errcheck:
check-type-assertions: true # 这里建议设置为true,如果确实不需要检查,可以写成`num, _ := strconv.Atoi(numStr)`
check-blank: false
gci:
# 将以`github.com/marmotedu/iam`开头的包放在第三方包后面
local-prefixes: github.com/marmotedu/iam
godox:
keywords: # 建议设置为BUG、FIXME、OPTIMIZE、HACK
- BUG
- FIXME
- OPTIMIZE
- HACK
goimports:
# 设置哪些包放在第三方包后面,可以设置多个包,逗号隔开
local-prefixes: github.com/marmotedu/iam
gomoddirectives: # 设置允许在go.mod中replace的包
replace-local: true
replace-allow-list:
- github.com/coreos/etcd
- google.golang.org/grpc
- github.com/marmotedu/api
- github.com/marmotedu/component-base
- github.com/marmotedu/marmotedu-sdk-go
gomodguard: # 下面是根据需要选择可以使用的包和版本,建议设置
allowed:
modules:
- gorm.io/gorm
- gorm.io/driver/mysql
- k8s.io/klog
domains: # List of allowed module domains
- google.golang.org
- gopkg.in
- golang.org
- github.com
- go.uber.org
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- github.com/marmotedu/errors
reason: "`github.com/marmotedu/errors` is the log package used by marmotedu projects."
versions:
- github.com/MakeNowJust/heredoc:
version: "> 2.0.9"
reason: "use the latest version"
local_replace_directives: false
lll:
line-length: 240 # 这里可以设置为240,240一般是够用的
importas: # 设置包的alias,根据需要设置
jwt: github.com/appleboy/gin-jwt/v2
metav1: github.com/marmotedu/component-base/pkg/meta/v1
run:
skip-dirs: # 设置要忽略的目录
- util
- .*~
- api/swagger/docs
skip-files: # 设置不需要检查的go源码文件,支持正则匹配,这里建议包括:_test.go
- ".*\\.my\\.go$"
- _test.go
linters-settings:
errcheck:
check-type-assertions: true # 这里建议设置为true,如果确实不需要检查,可以写成`num, _ := strconv.Atoi(numStr)`
check-blank: false
gci:
# 将以`github.com/marmotedu/iam`开头的包放在第三方包后面
local-prefixes: github.com/marmotedu/iam
godox:
keywords: # 建议设置为BUG、FIXME、OPTIMIZE、HACK
- BUG
- FIXME
- OPTIMIZE
- HACK
goimports:
# 设置哪些包放在第三方包后面,可以设置多个包,逗号隔开
local-prefixes: github.com/marmotedu/iam
gomoddirectives: # 设置允许在go.mod中replace的包
replace-local: true
replace-allow-list:
- github.com/coreos/etcd
- google.golang.org/grpc
- github.com/marmotedu/api
- github.com/marmotedu/component-base
- github.com/marmotedu/marmotedu-sdk-go
gomodguard: # 下面是根据需要选择可以使用的包和版本,建议设置
allowed:
modules:
- gorm.io/gorm
- gorm.io/driver/mysql
- k8s.io/klog
domains: # List of allowed module domains
- google.golang.org
- gopkg.in
- golang.org
- github.com
- go.uber.org
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- github.com/marmotedu/errors
reason: "`github.com/marmotedu/errors` is the log package used by marmotedu projects."
versions:
- github.com/MakeNowJust/heredoc:
version: "> 2.0.9"
reason: "use the latest version"
local_replace_directives: false
lll:
line-length: 240 # 这里可以设置为240,240一般是够用的
importas: # 设置包的alias,根据需要设置
jwt: github.com/appleboy/gin-jwt/v2
metav1: github.com/marmotedu/component-base/pkg/meta/v1
需要注意的是,golangci-lint 不建议使用 enable-all: true
选项,为了尽可能使用最全的 linters,我们可以使用以下配置:
linters:
disable-all: true
enable: # enable下列出 <期望的所有linters>
- typecheck
- ...
<期望的所有linters> = - <不期望执行的linters>
,我们可以通过执行以下命令来获取:
$ ./scripts/print_enable_linters.sh
- asciicheck
- bodyclose
- cyclop
- deadcode
- ...
将以上输出结果替换掉.golangci.yaml
配置文件中的 linters.enable
部分即可。
如何使用 golangci-lint 进行静态代码检查?
要对代码进行静态检查,只需要执行 golangci-lint run
命令即可。接下来,我会先给你介绍 5 种常见的 golangci-lint 使用方法。
对当前目录及子目录下的所有 Go 文件进行静态代码检查:
$ golangci-lint run
命令等效于golangci-lint run ./...
。
对指定的 Go 文件或者指定目录下的 Go 文件进行静态代码检查:
$ golangci-lint run dir1 dir2/... dir3/file1.go
这里需要你注意:上述命令不会检查 dir1 下子目录的 Go 文件,如果想递归地检查一个目录,需要在目录后面追加/...
,例如:dir2/...
。
根据指定配置文件,进行静态代码检查:
$ golangci-lint run -c .golangci.yaml ./...
运行指定的 linter:
golangci-lint 可以在不指定任何配置文件的情况下运行,这会运行默认启用的 linter,你可以通过golangci-lint help linters查看它。
你可以传入参数-E/--enable
来使某个 linter 可用,也可以使用-D/--disable
参数来使某个 linter 不可用。下面的示例仅仅启用了 errcheck linter:
$ golangci-lint run --no-config --disable-all -E errcheck ./...
这里你需要注意,默认情况下,golangci-lint
会从当前目录一层层往上寻找配置文件名.golangci.yaml、.golangci.toml、.golangci.json直到根(/)目录。如果找到,就以找到的配置文件作为本次运行的配置文件,所以为了防止读取到未知的配置文件,可以用 --no-config
参数使 golangci-lint 不读取任何配置文件。
禁止运行指定的 liner:
如果我们想禁用某些 linter,可以使用-D选项。
$ golangci-lint run --no-config -D godot,errcheck
在使用 golangci-lint 进行代码检查时,可能会有很多误报。所谓的误报,其实是我们希望 golangci-lint 的一些 linter 能够容忍某些 issue。那么如何尽可能减少误报呢?golangci-lint 也提供了一些途径,我建议你使用下面这三种:
- 在命令行中添加-e参数,或者在配置文件的issues.exclude部分设置要排除的检查错误。你也可以使用issues.exclude-rules来配置哪些文件忽略哪些 linter。
- 通过run.skip-dirs、run.skip-files或者issues.exclude-rules配置项,来忽略指定目录下的所有 Go 文件,或者指定的 Go 文件。
- 通过在 Go 源码文件中添加
//nolint
注释,来忽略指定的代码行。
因为 golangci-lint 设置了很多 linters,对于一个大型项目,启用所有的 linter 会检查出很多问题,并且每个项目对 linter 检查的粒度要求也不一样,所以 glangci-lint使用 nolint 标记来开关某些检查项,不同位置的 nolint 标记效果也会不一样。接下来我想向你介绍 nolint 的几种用法。
忽略某一行所有 linter 的检查:
var bad_name int //nolintvar bad_name int //nolint
忽略某一行指定 linter 的检查,可以指定多个 linter,用逗号 , 隔开。
var bad_name int //nolint:golint,unused
忽略某个代码块的检查:
//nolint
func allIssuesInThisFunctionAreExcluded() *string {
// ...
}
//nolint:govet
var (
a int
b int
)
忽略某个文件的指定 linter 检查。
在 package xx 上面一行添加//nolint注释。
//nolint:unparam
package pkg
...
在使用 nolint 的过程中,有 3 个地方需要你注意。
- 首先,如果启用了 nolintlint,你就需要在//nolint后面添加 nolint 的原因// xxxx。
- 其次,你使用的应该是//nolint而不是// nolint。因为根据 Go 的规范,需要程序读取的注释 // 后面不应该有空格。
- 最后,如果要忽略所有 linter,可以用//nolint;如果要忽略某个指定的 linter,可以用
//nolint:<linter1>,<linter2>
,。
golangci-lint 使用技巧
技巧 1:第一次修改,可以按目录修改。
如果你第一次使用 golangci-lint 检查你的代码,一定会有很多错误。为了减轻修改的压力,可以按目录检查代码并修改。这样可以有效减少失败条数,减轻修改压力。
当然,如果错误太多,一时半会儿改不完,想以后慢慢修改或者干脆不修复存量的 issues,那么你可以使用 golangci-lint 的 --new-from-rev 选项,只检查新增的 code,例如:
$ golangci-lint run --new-from-rev=HEAD~1
技巧 2:按文件修改,减少文件切换次数,提高修改效率。
如果有很多检查错误,涉及很多文件,建议先修改一个文件,这样就不用来回切换文件。可以通过 grep 过滤出某个文件的检查失败项,例如:
$ golangci-lint run ./...|grep pkg/storage/redis_cluster.go
pkg/storage/redis_cluster.go:16:2: "github.com/go-redis/redis/v7" imported but not used (typecheck)
pkg/storage/redis_cluster.go:82:28: undeclared name: `redis` (typecheck)
pkg/storage/redis_cluster.go:86:14: undeclared name: `redis` (typecheck)
...
技巧 3:把 linters-setting.lll.line-length 设置得大一些。
在 Go 项目开发中,为了易于阅读代码,通常会将变量名 / 函数 / 常量等命名得有意义,这样很可能导致每行的代码长度过长,很容易超过lll linter 设置的默认最大长度 80。这里建议将linters-setting.lll.line-length
设置为 120/240。
技巧 4:尽可能多地使用 golangci-lint 提供的 linter。
golangci-lint 集成了很多 linters,可以通过如下命令查看:
$ golangci-lint linters
Enabled by your configuration linters:
deadcode: Finds unused code [fast: true, auto-fix: false]
...
varcheck: Finds unused global variables and constants [fast: true, auto-fix: false]
Disabled by your configuration linters:
asciicheck: Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
...
wsl: Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
这些 linter 分为两类,一类是默认启用的,另一类是默认禁用的。每个 linter 都有两个属性:
- fast:true/false,如果为 true,说明该 linter 可以缓存类型信息,支持快速检查。因为第一次缓存了这些信息,所以后续的运行会非常快。
- auto-fix:true/false,如果为 true 说明该 linter 支持自动修复发现的错误;如果为 false 说明不支持自动修复。
如果配置了 golangci-lint 配置文件,则可以通过命令golangci-lint help linters查看在当前配置下启用和禁用了哪些 linter。golangci-lint 也支持自定义 linter 插件
- https://golangci-lint.run/contributing/new-linters/
在使用 golangci-lint 的时候,我们要尽可能多的使用 linter。使用的 linter 越多,说明检查越严格,意味着代码越规范,质量越高。如果时间和精力允许,建议打开 golangci-lint 提供的所有 linter。
技巧 5:每次修改代码后,都要执行 golangci-lint。
每次修改完代码后都要执行 golangci-lint,一方面可以及时修改不规范的地方,另一方面可以减少错误堆积,减轻后面的修改压力。
技巧 6:建议在根目录下放一个通用的 golangci-lint 配置文件。
在/目录下存放通用的 golangci-lint 配置文件,可以让你不用为每一个项目都配置 golangci-lint。当你需要为某个项目单独配置 golangci-lint 时,只需在该项目根目录下增加一个项目级别的 golangci-lint 配置文件即可。
总结
Go 项目开发中,对代码进行静态代码检查是必要的操作。当前有很多优秀的静态代码检查工具,但 golangci-lint 因为具有检查速度快、可配置、少误报、内置了大量 linter 等优点,成为了目前最受欢迎的静态代码检查工具。
golangci-lint 功能非常强大,支持诸如 run、cache、completion、linters 等命令。其中最常用的是 run 命令,run 命令可以通过以下方式来进行静态代码检查:
$ golangci-lint run # 对当前目录及子目录下的所有Go文件进行静态代码检查
$ golangci-lint run dir1 dir2/... dir3/file1.go # 对指定的Go文件或者指定目录下的Go文件进行静态代码检查
$ golangci-lint run -c .golangci.yaml ./... # 根据指定配置文件,进行静态代码检查
$ golangci-lint run --no-config --disable-all -E errcheck ./... # 运行指定的 errcheck linter
$ golangci-lint run --no-config -D godot,errcheck # 禁止运行指定的godot,errcheck liner
此外,golangci-lint 还支持 //nolint 、//nolint:golint,unused 等方式来减少误报。
END 链接
✴️版权声明 © :本书所有内容遵循CC-BY-SA 3.0协议(署名-相同方式共享)©