可用于 Go 语言代码分析的工具有很多,比如 golintgofmtmisspell 等,如果一一引用配置,就会比较烦琐,所以通常我们不会单独地使用它们,而是使用 golangci-lint

官网地址:
https://github.com/golangci/golangci-linthttps://golangci-lint.run/

golangci-lint 是一个集成工具,它集成了很多静态代码分析工具,便于我们使用。有以下特点:

  • 速度非常快:golangci-lint 是基于 gometalinter 开发的,但是平均速度要比 gometalinter 快 5 倍。速度快的原因有三个:可以并行检查代码;可以复用 go build 缓存;会缓存分析结果。
  • 可配置:支持 YAML 格式的配置文件,让检查更灵活,更可控。
  • IDE 集成:可以集成进多个主流的 IDE,例如 VS CodeGNU EmacsSublime TextGoland 等。
  • linter 聚合器:1.41.1 版本集成了 76 个 linter,并且还支持自定义 linter
  • 最小的误报数:调整了所集成 linter 的默认设置,大幅度减少了误报。
  • 良好的输出:输出的结果带有颜色、代码行号和 linter 标识,易于查看和定位。
  • 当前更新迭代速度很快,不断有新的 linter 被集成。

1. 安装

如果要使用 golangci-lint,首先需要安装。因为 golangci-lint 本身就是 Go 语言编写的,所以我们可以从源代码安装它,打开终端,输入如下命令即可安装。

go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35.2

使用这一命令安装的是 v1.35.2 版本的 golangci-lint

如不能成功安装,则使用下面命令:

# 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 v1.35.2
golangci/golangci-lint info checking GitHub for tag 'v1.35.2'
golangci/golangci-lint info found version: 1.35.2 for v1.35.2/linux/amd64
golangci/golangci-lint info installed /home/wohu/gocode/bin/golangci-lint

或者参考:

如果还是安装失败,采用以下方法,下载源码,执行安装脚本

cd $GOPATH/src/github.com
git clone https://github.com/golangci/golangci-lint.git
cd golangci-lint/
chmod +x install.sh
sh install.sh -b $(go env GOPATH)/bin v1.38.0

安装完成后,在终端输入如下命令,检测是否安装成功。

$ golangci-lint --version
golangci-lint has version 1.35.2 built from 1da5701 on 2021-01-11T02:54:03Z

2. 支持命令和全局选项

可以通过执行 golangci-lint -h 查看其用法,golangci-lint 支持的子命令见下表

go语言 静态文件 go代码静态分析_golangci-lint


此外,golangci-lint 还支持一些全局选项。全局选项是指适用于所有子命令的选项,golangci-lint 支持的全局选项如下:

go语言 静态文件 go代码静态分析_github_02

2.1 run

run 命令执行 golangci-lint,对代码进行检查,是 golangci-lint 最为核心的一个命令。run 没有子命令,但有很多选项。

golangci-lint run 支持很多命令行选项,可通过golangci-lint run -h查看,这里选择一些比较重要的选项进行介绍,见下表:

go语言 静态文件 go代码静态分析_github_03

2.2 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

2.3 completion

completion 命令包含 4 个子命令 bashfishpowershellzsh,分别用来输出这四种环境的自动补全脚本。

$ golangci-lint completion bash > ~/.golangci-lint.bash
$ echo "source '$HOME/.golangci-lint.bash'" >> ~/.bashrc
$ source ~/.bashrc

执行完上面的命令,键入如下命令,即可自动补全子命令:

$ golangci-lint comp<TAB>

2.4 config

config 命令可以打印当前使用的配置文件路径,例如:

$ golangci-lint config path
.golangci.yaml

2.5 linters

linters 命令可以打印出所支持的 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]
...

3. 配置

支持两种配置方式,分别是命令行选项和配置文件。

  • 如果 bool/string/int 的选项同时在命令行选项和配置文件中被指定,命令行的选项就会覆盖配置文件中的选项。
  • 如果是 slice 类型的选项,则命令行和配置中的配置会进行合并。

默认的配置文件名为 .golangci.yaml.golangci.toml.golangci.json,可以通过 -c 选项指定配置文件名。通过配置文件,可以实现下面几类功能:

  • 本身的一些选项,比如超时、并发,是否检查 *_test.go 文件等。
  • 配置需要忽略的文件和文件夹。
  • 配置启用哪些 linter,禁用哪些 linter
  • 配置输出格式。
  • 有些 linter 支持一些配置项,这些配置项可以在配置文件中配置。
  • 配置符合指定正则规则的文件可以忽略的 linter
  • 设置错误严重级别,像日志一样,检查错误也是有严重级别的。

更详细的配置内容,你可以参考 configuration

golangci-lint 的配置比较灵活,比如你可以自定义要启用哪些 lintergolangci-lint 默认启用的 linter,包括这些:

  • deadcode - 死代码检查
  • errcheck - 返回错误是否使用检查
  • gosimple - 检查代码是否可以简化
  • govet - 代码可疑检查,比如格式化字符串和类型不一致
  • ineffassign - 检查是否有未使用的代码
  • staticcheck - 静态分析检查
  • structcheck - 查找未使用的结构体字段
  • typecheck - 类型检查
  • unused - 未使用代码检查
  • varcheck - 未使用的全局变量和常量检查

小提示:golangci-lint 支持的更多 linter,可以在终端中输入 golangci-lint linters 命令查看,并且可以看到每个 linter 的说明。

如果要修改默认启用的 linter,就需要对 golangci-lint 进行配置。即在项目根目录下新建一个名字为 .golangci.yml 的文件,这就是 golangci-lint 的配置文件。在运行代码规范检查的时候,golangci-lint 会自动使用它。假设我只启用 unused 检查,可以这样配置:

.golangci.yml

linters:
  disable-all: true
  enable:
    - unused

在团队多人协作开发中,有一个固定的 golangci-lint 版本是非常重要的,这样大家就可以基于同样的标准检查代码。要配置 golangci-lint 使用的版本也比较简单,在配置文件中添加如下代码即可:

service:
  golangci-lint-version: 1.32.2 # use the fixed version to not introduce new linters unexpectedly

此外,你还可以针对每个启用的 linter 进行配置,比如要设置拼写检测的语言为 US,可以使用如下代码设置:

linters-settings:
  misspell:
    locale: US

golangci-lint 的配置比较多,你自己可以灵活配置。关于 golangci-lint 的更多配置可以参考官方文档,这里我给出一个常用的配置,代码如下:

配置文件 .golangci.yml

linters-settings:
  golint:
    min-confidence: 0
  misspell:
    locale: US
linters:
  disable-all: true
  enable:
    - typecheck
    - goimports
    - misspell
    - govet
    - golint
    - ineffassign
    - gosimple
    - deadcode
    - structcheck
    - unused
    - errcheck
service:
  golangci-lint-version: 1.32.2 # use the fixed version to not introduce new linters unexpectedly

比较重要的配置

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

4. 运行

常用的使用方法:

  1. 对当前目录及子目录下的所有 Go 文件进行静态代码检查
$ golangci-lint run

等效于 golangci-lint run ./...

  1. 对指定的 Go 文件或者指定目录下的 Go 文件进行静态代码检查:
$ golangci-lint run dir1 dir2/... dir3/file1.go

这里需要你注意:上述命令不会检查 dir1 下子目录的 Go 文件,如果想递归地检查一个目录,需要在目录后面追加 /... ,例如:dir2/...

  1. 根据指定配置文件,进行静态代码检查
$ golangci-lint run -c .golangci.yaml ./...
  1. 运行指定的 linter:

可以传入参数 -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 不读取任何配置文件。

  1. 禁止运行指定的 liner:
    如果我们想禁用某些 linter,可以使用 -D 选项。
$ golangci-lint run --no-config -D godot,errcheck

安装成功 golangci-lint 后,就可以使用它进行代码检查了,示例代码

package main

import "os"

const name string = "wohu"
func main() {
    os.Mkdir("demo_test", 0755)
}

运行命令:

wohu@ubuntu:~/gocode/src$ golangci-lint run demo.go 
demo.go:5:7: `name` is unused (deadcode)
const name string = "wohu"
      ^
demo.go:7:13: Error return value of `os.Mkdir` is not checked (errcheck)
    os.Mkdir("demo_test", 0755)
            ^

通过代码检测结果可以看到,两个代码规范问题都被检测出来了。检测出问题后,你就可以修复它们,让代码更加符合规范。

5. 减少误报

golangci-lint 也提供了一些途径,我建议你使用下面这三种:

  • 在命令行中添加 -e 参数,或者在配置文件的 issues.exclude 部分设置要排除的检查错误。你也可以使用 issues.exclude-rules 来配置哪些文件忽略哪些 linter
  • 通过 run.skip-dirsrun.skip-files 或者 issues.exclude-rules 配置项,来忽略指定目录下的所有 Go 文件,或者指定的 Go 文件。
  • 通过在 Go 源码文件中添加 //nolint 注释,来忽略指定的代码行。

nolint 用法:

  1. 忽略某一行所有 linter 的检查
var bad_name int //nolint
  1. 忽略某一行指定 linter 的检查,可以指定多个 linter,用逗号 , 隔开。
var bad_name int //nolint:golint,unused
  1. 忽略某个代码块的检查。
//nolint
func allIssuesInThisFunctionAreExcluded() *string {
  // ...
}

//nolint:govet
var (
  a int
  b int
)
  1. 忽略某个文件的指定 linter 检查
    package xx 上面一行添加 //nolint 注释。
//nolint:unparam
package pkg
...

在使用 nolint 的过程中,有 3 个地方需要你注意。

  • 首先,如果启用了 nolint,你就需要在 //nolint 后面添加 nolint 的原因 // xxxx
  • 其次,你使用的应该是 //nolint 而不是 // nolint。因为根据 Go 的规范,需要程序读取的注释 // 后面不应该有空格。
  • 最后,如果要忽略所有 linter,可以用 //nolint;如果要忽略某个指定的 linter,可以用 //nolint:<linter1>,<linter2>,。

6. 使用技巧

6.1 按目录修改

如果你第一次检查你的代码,一定会有很多错误。为了减轻修改的压力,可以按目录检查代码并修改。这样可以有效减少失败条数,减轻修改压力。

当然,如果错误太多,一时半会儿改不完,那么你可以使用 --new-from-rev 选项,只检查新增的 代码,例如:

$ golangci-lint run --new-from-rev=HEAD~1

6.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)
...

6.3 调整每行最大字符数

在项目开发中,为了易于阅读代码,通常会将变量名 / 函数 / 常量等命名得有意义,这样很可能导致每行的代码长度过长,很容易超过 lll linter 设置的默认最大长度 80。这里建议将 linters-setting.lll.line-length 设置为 120/240。

6.4 尽可能多地使用提供的 linter

golangci-lint 集成了很多 linters,可以通过命令 golangci-lint linters查看,这些 linter 分为两类,一类是默认启用的,另一类是默认禁用的。每个 linter 都有两个属性:

  • fasttrue/false,如果为 true,说明该 linter 可以缓存类型信息,支持快速检查。因为第一次缓存了这些信息,所以后续的运行会非常快。
  • auto-fixtrue/false,如果为 true 说明该 linter 支持自动修复发现的错误;如果为 false 说明不支持自动修复。

如果配置了 golangci-lint 配置文件,则可以通过命令 golangci-lint help linters 查看在当前配置下启用和禁用了哪些 lintergolangci-lint 也支持自定义 linter 插件,具体你可以参考 New linters

在使用 golangci-lint 的时候,我们要尽可能多的使用 linter。使用的 linter 越多,说明检查越严格,意味着代码越规范,质量越高。如果时间和精力允许,建议打开 golangci-lint 提供的所有 linter

6.5 建议在根目录下放一个通用的 golangci-lint 配置文件

/ 目录下存放通用的 golangci-lint 配置文件,可以让你不用为每一个项目都配置 golangci-lint。当你需要为某个项目单独配置 golangci-lint 时,只需在该项目根目录下增加一个项目级别的 golangci-lint 配置文件即可。

7. 集成到 CI

代码检查一定要集成到 CI 流程中,效果才会更好,这样开发者提交代码的时候,CI 就会自动检查代码,及时发现问题并进行修正。

不管你是使用 Jenkins,还是 Gitlab CI,或者 Github Action,都可以通过 Makefile 的方式运行 golangci-lint 。现在我在项目根目录下创建一个 Makefile 文件,并添加如下代码:

getdeps:
   @mkdir -p ${GOPATH}/bin
   @which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.32.2)
lint:
   @echo "Running $@ check"
   @GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
   @GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
verifiers: getdeps lint