给Go程序打针的指南

1,151 阅读7分钟

给Go程序打针的指南

提示是识别和报告在代码中发现的模式的过程,目的是提高一致性,并在开发周期的早期捕获错误。这在团队工作中特别有用,因为它有助于使所有的代码看起来都一样,不管是谁写的,这就减少了复杂性,并使代码更容易维护。在这篇文章中,我将展示Go程序的全面提示设置,并讨论将其引入现有项目的最佳方式。

为确保项目中的编码实践的一致性,对代码进行标记是最基本的事情之一。Go已经比其他大多数编程语言走得更远了,它捆绑了gofmt ,这个格式化工具可以确保所有Go代码看起来都一样,但它只处理代码的格式化方式。Go vet工具也可以帮助检测可能没有被编译发现的可疑结构,但它只能捕捉到有限的潜在问题。

开发更全面的提示工具的任务被留给了更广泛的社区,这就产生了大量的提示器,每一个都有特定的目的。突出的例子包括。

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

拥有这么多独立的提示工具的问题是,你必须自己下载每一个单独的linter并管理它们的版本。此外,依次运行每一个工具可能会太慢。由于这些原因,golangci-lint,一个Go linters聚合器,它可以并行运行linters,重用Go build cache,并缓存分析结果,以大大提高后续运行的性能,是在Go项目中设置linting的首选方式。

golangci-lint 项目是为了方便和性能的原因而开发的,用于聚合和并行运行几个单独的linters。当你安装该程序时,你会得到大约48个提示器(在写这篇文章时),你可以继续挑选对你的项目来说重要的提示器。除了在开发过程中在本地运行它之外,你还可以将它设置为持续集成工作流程的一部分。

安装golangci-lint

使用下面的命令在任何操作系统上本地安装golangci-lint

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

一旦安装完毕,你应该检查所安装的版本。

$ golangci-lint version
golangci-lint has version v1.40.1 built from (unknown, mod sum: "h1:pBrCqt9BgI9LfGCTKRTSe1DfMjR6BkOPERPaXJYXA6Q=") on (unknown)

你也可以通过下面的命令查看所有可用的提示器。

$ golangci-lint help linters

golangci-lint linters

绝大多数可用的打火机在默认情况下是禁用的。

如果你在你的项目目录的根部运行启用的linters,你可能会看到一些错误。每个问题都会被报告,并提供你需要解决的所有内容,包括对问题的简短描述,以及发生问题的文件和行号。

$ golangci-lint run # equivalent of golangci-lint run ./...

golangci-lint run may spot some problems with the default settings

golangci-lint提供了一个漂亮的输出,有颜色、源代码行和标记的标识符

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

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

配置golangci-lint

GolangCI-Lint被设计成尽可能灵活地适用于广泛的使用情况。golangci-lint 的配置可以通过命令行选项或配置文件来管理,尽管如果两者同时使用,前者比后者更优先。这里有一个例子,它使用命令行选项来禁用所有的linters,并配置应该运行的特定linters。

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

你也可以运行由golangci-lint 提供的预设。 这里是如何找到可用的预设。

$ golangci-lint help linters | sed -n '/Linters presets:/,$p'
Linters presets:
bugs: asciicheck, bodyclose, durationcheck, errcheck, errorlint, exhaustive, exportloopref, gosec, govet, makezero, nilerr, noctx, rowserrcheck, scopelint, sqlclosecheck, staticcheck, typecheck
comment: godot, godox, misspell
complexity: cyclop, funlen, gocognit, gocyclo, nestif
error: errcheck, errorlint, goerr113, wrapcheck
format: gci, gofmt, gofumpt, goimports
import: depguard, gci, goimports, gomodguard
metalinter: gocritic, govet, revive, staticcheck
module: depguard, gomoddirectives, gomodguard
performance: bodyclose, maligned, noctx, prealloc
sql: rowserrcheck, sqlclosecheck
style: asciicheck, depguard, dogsled, dupl, exhaustivestruct, forbidigo, forcetypeassert, gochecknoglobals, gochecknoinits, goconst, gocritic, godot, godox, goerr113, goheader, golint, gomnd, gomoddirectives, gomodguard, goprintffuncname, gosimple, ifshort, importas, interfacer, lll, makezero, misspell, nakedret, nlreturn, nolintlint, paralleltest, predeclared, promlinter, revive, stylecheck, tagliatelle, testpackage, thelper, tparallel, unconvert, wastedassign, whitespace, wrapcheck, wsl
test: exhaustivestruct, paralleltest, testpackage, tparallel
unused: deadcode, ineffassign, structcheck, unparam, unused, varcheck

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

$ golangci-lint run -p bugs -p error

为一个项目配置golangci-lint ,最好通过一个配置文件来完成。这样,你就能配置特定的linter选项,而这是通过命令行选项无法实现的。你可以指定YAML、TOML或JSON格式的配置文件,但我建议坚持使用YAML格式 (.golangci.yml.golangci.yaml),因为官方文档网页上使用的就是这种格式。

一般来说,你应该在你的项目目录的根部创建特定的项目配置。程序会自动在要加注的文件的目录中寻找它们,并在连续的父目录中一直到文件系统的根目录中寻找。这意味着你可以通过在你的主目录下放置一个配置文件来实现对所有项目的全局配置(不推荐)。如果本地范围内的配置文件不存在,就会使用这个文件。

golangci-lint 网站上有一个配置文件样本,其中包括所有支持的选项、它们的描述和默认值。在创建你自己的配置时,你可以把它作为一个起点。请记住,一些翻译器执行类似的功能,所以你需要特意启用翻译器,以避免多余的条目。下面是我在个人项目中使用的一般配置

.golangci.yml

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
  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

抑制提示性错误

有时有必要禁止在文件或包中出现的特定的提示性问题。这可以通过两种主要方式实现:通过nolint 指令,以及通过配置文件中的排除规则。让我们依次看一下这两种方法。

nolint 指令

让我们假设我们有以下代码,在标准输出中打印一个伪随机整数。

main.go

package main

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

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

在这个文件上执行golangci-lint run ,会产生以下错误,前提是启用了gosec linter。

$ golangci-lint run -E gosec
main.go:11:14: G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
	fmt.Println(rand.Int())
	            ^

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

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

根据Go的惯例,机器可读的注释不应该有空格,所以使用//nolint ,而不是// nolint

nolint 的内联用法会导致该行检测到的所有提示性问题被禁用。你可以通过在指令中指定一个特定的linter的名字来禁用其问题(推荐)。这将允许其他的提示器在该行提出的问题得到解决。

main.go

func main() {
	rand.Seed(time.Now().UnixNano())
	fmt.Println(rand.Int()) //nolint:gosec
}

当你在一个文件的顶部使用nolint 指令时,它会禁用该文件的所有提示问题。

main.go

//nolint:govet,errcheck
package main

你也可以通过在一个代码块的开头使用nolint 指令来排除该代码块(比如一个函数)的问题。

main.go

//nolint
func aFunc() {

}

在添加nolint 指令后,建议你添加一个注释,解释为什么需要它。这个注释应该和标志本身放在同一行。

main.go

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

你可以通过启用nolintlint linter来强制执行你的团队应该遵循的关于nolint 注释的惯例。它可以报告有关使用nolint 的问题,而不需要指明被压制的具体linter,也不需要解释为什么需要它的注释。

$ golangci-lint run
main.go:11:26: directive `//nolint` should mention specific linter such as `//nolint:my-linter` (nolintlint)
	fmt.Println(rand.Int()) //nolint
	                        ^

排除规则

可以在配置文件中指定排除规则,以便更细化地控制哪些文件会被加载,以及哪些问题会被报告。例如,你可以禁止某些linter在测试文件上运行(_test.go),或者你可以禁止某个linter在整个项目中产生某些错误。

.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 ),你可以在你的配置文件中指定它,如下所示。

.golangci.yml

issues:
  # Show only new issues created after git revision: 02270a6
  new-from-rev: 02270a6

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

GolangCI-Lint 支持与多个编辑器的集成,以获得快速反馈。在Visual Studio Code中,你需要做的是安装Go扩展,并在你的settings.json 文件中加入以下几行。

settings.json

{
  "go.lintTool":"golangci-lint",
  "go.lintFlags": [
    "--fast"
  ]
}

golangCI-Lint in action in Visual Studio Code

Vim用户可以将golangci-lint ,包括vim-goALESyntastic等多种插件。你也可以在golangci-lint-langserver的帮助下将其与coc.nvimvim-lspnvim.lspconfig集成。下面是我如何在我的编辑器中用coc.nvim 集成golangci-lint 。首先,安装语言服务器。

$ go install github.com/nametake/golangci-lint-langserver@latest

接下来,用:CocConfig 打开coc.nvim 的配置文件,并添加以下几行。

coc-settings.json

{
  "languageserver": {
    "golangci-lint-languageserver": {
      "command": "golangci-lint-langserver",
      "filetypes": ["go"],
      "initializationOptions": {
        "command": ["golangci-lint", "run", "--out-format", "json"]
      }
    }
  }
}

保存配置文件,然后用:CocRestart 重新启动coc.nvim ,或者打开一个新的Vim实例。只要在编辑器中打开一个Go文件,它就应该开始工作。

golangCI-Lint in action in Neovim

voila

设置一个预提交钩子

运行golangci-lint 作为 Git 预提交钩子的一部分,是确保所有被检查到源码控制的 Go 代码被正确提示的好方法。如果你还没有为你的项目设置预提交钩子,下面是如何用pre-commit设置的,这是一个与语言无关的工具,用于管理Git钩子脚本。

按照本页的说明安装pre-commit 软件包管理器,然后在项目的根目录下创建一个.pre-commit-config.yaml 文件,并在其中填入以下内容。

.pre-commit-config.yaml

repos:
-   repo: https://github.com/tekwizely/pre-commit-golang
    rev: v0.8.3 # change this to the latest version
    hooks:
    -   id: golangci-lint

这个配置文件扩展了pre-commit-golang资源库,它支持Go项目的各种钩子。golangci-lint 钩子只针对阶段性文件,这对于在现有项目中引入golangci-lint 时很方便,这样就不会一下子被这么多的linting问题所淹没了。一旦你保存了文件,运行pre-commit install 来设置当前版本库中的git钩子脚本。

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

在随后的提交中,指定的钩子将在所有分阶段的.go 文件上运行,如果发现错误,将停止提交过程。在允许提交之前,你需要修复所有的linting问题。如果你想在不提交的情况下测试预提交钩子,你也可以使用pre-commit run 命令。

golangci-lint running via Git pre-commit hook

持续集成(CI)工作流程

在每个拉动请求上运行你的项目的提示规则,可以防止不符合标准的代码溜进你的代码库。这也可以通过在持续集成过程中添加golangci-lint 来实现自动化。如果你使用GitHub动作,出于性能考虑,官方动作应该比简单的二进制安装更受欢迎。设置好之后,你会得到一个内嵌的拉动请求问题的显示。

golangci-lint inline displays issues on pull requests

在设置过程中,确保钉住正在使用的golangci-lint 版本,以便它与你的本地环境产生一致的结果。该项目正在积极开发中,所以更新后可能会废除一些提示器,或报告比以前检测到的相同源代码的更多错误。

结论

提示你的程序是确保一个项目的所有贡献者有一致的编码实践的一个可靠方法。通过采用本文所讨论的工具和流程,你就可以很好地做到这一点。