0%

Golang 代码质量工具 - golangci-lint 不完全指南

Linting 是识别和报告代码中发现的模式的过程,旨在提高一致性,并在开发周期的早期捕捉错误。 在团队合作时特别有用,因为它有助于使所有代码看起来都一样,无论是谁写的,这减少了复杂性,使代码更易于维护。 在本文中,将演示针对 Go 程序的全面 linting 设置,并讨论将其引入现有项目的最佳方法。

代码检查是确保项目中一致的编码规范的最基本的事情之一。 Go语言已经比大多数其他编程语言走得更远,它捆绑了一个格式化工具(也就是 gofmt),确保所有的Go代码看起来都一样,但它只处理代码的格式。 go vet 工具也可用于帮助检测可能不会被编译器捕捉到的可疑结构,但它只能捕捉有限数量的潜在问题。

开发更全面的代码检查工具的任务已交给更广泛的社区,这产生了大量的代码检查工具,每个工具都有特定的目的。其中一些著名的例子包括:

  • unused - 检查 Go 代码中未使用的常量、变量、函数和类型。
  • goconst - 查找可以用常量替换的重复字符串。
  • gocyclo - 计算并检查函数的圈复杂度。
  • errcheck - 检测Go程序中未检查的错误。

拥有如此多独立的代码检查工具的问题在于你必须自己下载每个单独的代码检查工具并管理它们的版本。 此外,依次运行每一个可能会太慢。因此,golangci-lint,一个Go代码检查工具聚合器,可以并行运行代码检查工具,重用 Go 构建缓存,并缓存分析结果,从而在后续运行中大大提高性能,是在 Go 项目中设置代码检查的首选方式。

该项目是为了方便和提高性能而开发的,可以同时聚合和运行多个单独的代码检查工具。安装该程序后,您将获得约 48 个代码检查工具,您可以选择其中对您的项目重要的工具。除了在开发过程中本地运行外,您还可以将其设置为持续集成(CI)工作流程的一部分。

安装 golangci-lint

你可以通过下面的命令将 golangci-lint 安装到你的系统中:

1
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

安装完成后,您应该检查已安装的版本:

1
2
➜  ~ golangci-lint version
golangci-lint has version v1.55.2 built with go1.21.6 from (unknown, mod sum: "h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8=") on (unknown)

您还可以通过以下命令查看所有可用的代码检查器:

1
golangci-lint help linters

输出:

1
2
3
4
5
Enabled by default linters:
errcheck: errcheck is a program for checking for unchecked errors in Go code. 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]
......

也就是说 golangci-lint 默认已经启用了 errcheckgovet 等代码检查器。

默认启用小部分,大部分默认处于禁用状态。

如果在项目目录的根目录运行启用的代码检查工具,可能会看到一些错误。 每个问题都会报告所有您需要修复它的上下文,包括问题的简短描述,以及它发生的文件和行号。

进入项目目录,执行下面的命令:

1
golangci-lint run

输出:

1
2
3
main.go:1: : # gopprof
./main.go:31:2: err declared and not used (typecheck)
package main

golangci-lint 提供了带颜色、源代码行和标识符的良好输出,以便您可以轻松地找到问题所在。

您还可以通过传递一个或多个目录或文件路径来选择要分析的目录和文件。

1
golangci-lint run dir1 dir2 dir3/main.go

配置 golangci-lint

golangci-lint 旨在尽可能灵活,适用于各种用例。 可以通过命令行选项或配置文件来管理 golangci-lint 的配置,尽管如果同时使用两者,前者的优先级更高。 以下是一个使用命令行选项禁用所有检查器并配置应该运行的特定检查器的示例:

1
golangci-lint run --disable-all -E revive -E errcheck -E nilerr -E gosec

这个命令只会进行 reviveerrchecknilerrgosec 检查。

通过 --disable-all 禁用所有检查器,然后使用 -E 选项启用特定的检查器。

您还可以运行由 golangci-lint 提供的预设。以下是了解可用预设的方法:

1
golangci-lint help linters | sed -n '/Linters presets:/,$p'

输出:

1
2
3
4
5
...
error: errcheck, errorlint, goerr113, wrapcheck
format: decorder, gci, gofmt, gofumpt, goimports, sloglint, tagalign
import: depguard, gci, goimports, gomodguard
...

然后,您可以通过将其名称传递给 --preset-p 标志来运行预设:

1
golangci-lint run -p bugs -p error

golangci-lintpreset 可以被理解为预定义的配置集,每个 preset 对应一组特定的配置和规则。通过使用 preset ,用户可以方便地启用一组默认的规则,而无需手动配置每个 linter 的选项。

最好通过配置文件来为项目配置 golangci-lint。这样,您可以配置特定的代码检查器选项,这是通过命令行选项无法实现的。 您可以将配置文件指定为 YAMLTOMLJSON 格式,但我建议坚持使用 YAML 格式(.golangci.yml.golangci.yaml),因为官方文档页面上使用的就是这种格式。

一般来说,你应该在项目目录的根目录中创建特定于项目的配置。程序会自动在待检查文件所在的目录以及一直向上到文件系统根目录的父目录中寻找它们。这意味着你可以通过在 home 目录中放置一个配置文件来实现所有项目的全局配置(不建议)。如果本地范围的配置文件不存在,将使用该文件。

官网上提供了一个示例配置文件,其中包含所有支持的选项、它们的描述和默认值。在创建自己的配置时,您可以将其作为起点。 请记住,一些代码检查工具执行类似的功能,因此您需要有意地启用代码检查工具,以避免重复的条目。 以下是我在个人项目中使用的一般配置(.golangci.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
linters-settings:
errcheck:
check-type-assertions: true
goconst:
min-len: 2
min-occurrences: 3
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
govet:
check-shadowing: true
enable:
- fieldalignment
nolintlint:
require-explanation: true
require-specific: true

linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- exportloopref
- exhaustive
- goconst
- gocritic
- gofmt
- goimports
- gomnd
- gocyclo
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nolintlint
- nakedret
- prealloc
- predeclared
- revive
- staticcheck
- structcheck
- stylecheck
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- varcheck
- whitespace
- wsl

run:
issues-exit-code: 1

抑制 linting 错误

有时需要禁用文件或包中出现的特定代码检查问题。这可以通过两种主要方式实现:通过 nolint 指令和配置文件中的排除规则。让我们依次看看每种方法。

nolint 指令

假设我们有以下代码,它会将伪随机整数打印到标准输出:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int())
}

运行 golangci-lint 时(golangci-lint run --disable-all -E gosec),会看到以下输出:

1
2
3
main.go:11:14: G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
fmt.Println(rand.Int())
^

linter 鼓励使用 crypto/randInt 方法,因为它在密码学上更安全,但它的 API 不太友好,性能较慢。 如果你可以接受速度更快的代价来换取不太安全的伪随机数,你可以通过在必要的行上添加 nolint 指令来忽略错误。

1
2
3
4
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint
}

根据 Go 的约定,机器可读的注释不应包含空格,因此应该使用 //nolint 而不是 // nolint

当您在文件顶部使用 nolint 指令时,它会禁用该文件的所有 linting 问题:

1
2
//nolint:govet,errcheck
package main

您还可以通过在代码块(如函数)的开头使用 nolint 指令来排除问题。

添加 nolint 指令后,建议添加一条注释,解释为什么需要该指令。该注释应放置在与标志本身相同的行上:

1
2
3
4
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint:gosec // for faster performance
}

排除规则

在配置文件中可以指定排除规则,以更精细地控制对哪些文件进行代码检查,以及报告哪些问题。 例如,您可以禁用某些代码检查器在测试文件上的运行,或者可以禁用某个代码检查器在整个项目中产生特定的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
#.golangci.yml
issues:
exclude-rules:
- path: _test\.go # disable some linters on test files
linters:
- gocyclo
- gosec
- dupl

# Exclude some gosec messages project-wide
- linters:
- gosec
text: "weak cryptographic primitive"

与现有项目集成

在向现有项目添加 golangci-lint 时,可能会出现许多问题,一次性解决所有问题可能会很困难。 但这并不意味着你应该因此放弃对项目进行代码检查的想法。有一个 new-from-rev 设置,允许你仅显示在特定 git 修订版本之后创建的新问题,这样可以轻松地只对新代码进行代码检查,直到有足够的时间来解决旧问题。一旦找到要从中开始进行代码检查的修订版本(使用 git log ),你可以在配置文件中指定如下:

1
2
3
4
#.golangci.yml
issues:
# Show only new issues created after git revision: 02270a6
new-from-rev: 02270a6

这样只会检查 02270a6 版本后的代码。

在你的编辑器中集成 golangci-lint

golangci-lint 支持与多个编辑器集成,以便快速获得反馈。在 Visual Studio Code中,您只需安装 Go 扩展,并将以下行添加到您的 settings.json 文件中:

1
2
3
4
5
6
{
"go.lintTool":"golangci-lint",
"go.lintFlags": [
"--fast"
]
}

持续集成

在每个 PR 上运行项目的代码检查规则,可以防止不符合标准的代码进入代码库。这也可以通过将 golangci-lint 添加到持续集成流程中实现自动化。 比如:

  • Github Actions(如果你使用 Github)
  • Gitlab CI(如果你使用 Gitlab)

下面是一个 Github Action 配置的示例(当然,下面这个例子不太好,没有指定确定的版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout 5m

在设置过程中,请确保固定使用的 golangci-lint 版本,以便在本地环境中产生一致的结果。

总结

本文介绍了 Golang 中代码检查的工具 golangci-lint,并讨论了如何将其集成到现有项目中。 使用 golangci-lint,您可以轻松地在团队协作中保持一致的代码风格,并在开发周期的早期捕捉错误。