golangci-lint 结合 git hook 扫查代码实践

序言

golangci-lint 是一个用于 golang 的代码检查工具,可以检查代码的语法、风格、错误、性能和安全等问题。本文将介绍如何使用
golangci-lint 结合 git hook 扫查代码实践。

golangci-lint 基本使用

安装

1
2
// 执行以下命令进行安装
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3
BASH

如何选择版本:在 https://golangci-lint.run/product/changelog/ 页面搜索适合的版本,比如搜索go1.18.10适配的版本:
在浏览器窗口Ctrl+F搜索 support 找到像 go1.19 support 字样,往后退一个版本就是go1.18.10适配的版本

配置文件

  • 格式:使用.golangci.yml作为配置文件,详细格式参考官方文档
  • 如何指定配置文件:运行代码扫描时,配置文件的位置搜索是从当前目录开始,向上搜索,直到找到配置文件或者找到根目录为止,或者使用
    --config参数指定配置文件位置。

基本命令

  • 运行代码扫描:golangci-lint run。一般在项目根目录执行,后面还可以指定扫描的文件或目录。
  • 输出所有的检查器(linters):golangci-lint linters。该命令在检查当前版本的golangci-lint是否有配置的检查器时很有用。
  • golangci-lint run--fix 参数,可以自动修复一些问题。
  • golangci-lint run-v 可以输出详细日志。
  • 代码中设置绕过检查的方法:在代码中添加注释//no lint:{linter的名称} {原因(可选)}, 添加在代码行后面则是当前行绕过,添加在
    上方则是当前整个代码块绕过。

集成到 git hook

通过权衡,我们可以在 git commit 时进行代码检查,并且只检查修改过的文件(鉴于单个文件检查结果可能不准确,实际上是检查修改过的文件所在的目录)。
要实现这个功能,我们可以在 .git/hooks/pre-commit 文件中添加以下代码:

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
#!/bin/sh

# 获取改动的 .go 文件
CHANGED_FILES=$(git diff --name-only --cached --diff-filter=ACM | grep '\.go$')

# 如果没有改动的 Go 文件,则跳过 lint 检查
if [ -z "$CHANGED_FILES" ]; then
echo "No Go files changed, skipping lint check."
exit 0
fi

# 按目录分组,避免跨目录问题(typechecking error: named files must all be in one directory)
echo "Running golangci-lint on changed files by directory..."
DIRS=$(echo "$CHANGED_FILES" | xargs -n1 dirname | sort -u)

# 对每个目录运行 golangci-lint
LINT_FAILED=0
for dir in $DIRS; do
echo "Linting directory: $dir"
golangci-lint run --fix --concurrency=1 "$dir" || LINT_FAILED=1
done

# 如果有 lint 错误,阻止提交
if [ $LINT_FAILED -ne 0 ]; then
echo "golangci-lint failed, commit aborted."
exit 1
fi

echo "golangci-lint passed."
exit 0
BASH

该脚本首先获取所有改动的 .go 文件,为避免跨目录的问题,然后按目录分组,逐个目录执行 golangci-lint run 命令。(因为
golangci-lint使用指定目录作为参数的方式不允许同时检查多个不同的目录)。
如果有 lint 错误,exit 1 则阻止提交,反之则通过 exit 0 允许提交。

因为 git hook 文件不能随代码提交到远程仓库,为方便团队开发人员使用,我们可以在项目根目录下创建一个 pre-commit.sh
文件以及编写对应的安装脚本。团队开发成员只需执行该脚本即可将该文件复制到本地仓库的 .git/hooks
目录下,并设置该文件为可执行。(该脚本同时也将golangci-lint程序的安装集成到脚本开头)。安装脚本内容如下:

  • linux 版
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
#!/bin/bash

# 安装 golangci-lint
echo "Installing golangci-lint..."
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3
if [ $? -ne 0 ]; then
echo "Failed to install golangci-lint. Exiting."
exit 1
fi

# 确保 .git/hooks 目录存在
if [ ! -d ".git/hooks" ]; then
echo ".git/hooks directory not found. Please ensure this is a Git repository."
exit 1
fi

# 将 pre-commit.sh 的内容复制到 .git/hooks/pre-commit
echo "Writing pre-commit hook..."
cp pre-commit.sh .git/hooks/pre-commit

# 设置脚本为可执行
echo "Setting pre-commit hook as executable..."
chmod +x .git/hooks/pre-commit

echo "Done."

BASH
  • windows版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@echo off
:: 安装 golangci-lint
echo Installing golangci-lint...
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3
if %ERRORLEVEL% neq 0 (
echo Failed to install golangci-lint. Exiting.
exit /b 1
)

:: 确保 .git/hooks 目录存在
if not exist .git\hooks (
echo ".git/hooks directory not found. Please ensure this is a Git repository."
exit /b 1
)

:: 将 pre-commit.sh 的内容复制到 .git/hooks/pre-commit
echo Writing pre-commit hook...
copy /Y pre-commit.sh .git\hooks\pre-commit

:: 设置脚本为可执行
echo Setting pre-commit hook as executable...
attrib +x .git\hooks\pre-commit

echo Done.
SHELL

主要检查器(linters)了解

以下几个检查器包含了大量的子规则,具体可参考官方文档:https://golangci-lint.run/usage/linters/ 进行配置

  • staticcheck:静态检查器,用于检查代码中的错误、警告、性能问题、安全漏洞、代码风格问题、代码质量问题。
  • gosimple:代码简化器,用于检查代码中是否有可以简化的代码。
  • stylecheck:代码风格检查器,用于检查代码中的风格问题,比如空格、缩进、换行、注释风格等。
  • gosec:安全漏洞检查器,用于检查代码中是否有安全漏洞。
  • govet:代码检查器,用于检查代码中的语法错误、类型错误、空接口错误、未使用的变量错误、未使用的函数错误、未使用的包错误。

另外还有以下是单个的检查器, 比如:

  • ineffassign # 检查无效赋值
  • maintidx # 衡量代码可维护性指数,指数越高表示代码越易维护
  • makezero # 确保切片初始化的长度为 0
  • misspell # 拼写错误检查
  • nestif # 检查代码中嵌套的 if 语句是否过多
  • nilerr # 检查是否在if err != nil 时返回 nil,或者反过来
  • nilnil # 检查是否在 err == nil,却还要检查返回的数据是否为nil,这种情况应该重新思考接口的设计是否合理
  • nlreturn # return 语句前面应该空一行,提高代码可读性
  • noctx # 检查没有使用 context.Context 的 http 请求
  • nolintlint # 检查 nolint 命令是否进行了原因解释
  • nosprintfhostport # 禁用Sprintf方法用于生成 host:port 字符串,
    因为IPv6情况下会出错,参考:https://github.com/stbenjam/no-sprintf-host-port
  • prealloc # 初始化 slice 时预先分配空间,减少扩容带来的性能损失
  • predeclared # 避免变量名与语言中的关键字冲突
  • staticcheck # 该检查器提供了一组静态检查的规则
  • stylecheck # 提供代码风格检查建议
  • typecheck # https://github.com/golangci/golangci-lint/discussions/3759
  • unconvert # 移除多余的类型转换
  • whitespace # 检查多余的换行

问题记录

  • 为什么不使用pre-commit脚本工具?

使用pre-commit脚本工具时,全局扫描没问题,但要实现仅检查修改过的文件,添加的配置参数一直不起作用或者出现
typechecking error: named files must all be in one directorypre-commit.pre-commit-config.yaml内容如下:

1
2
3
4
5
6
7
repos:
- repo: https://github.com/pre-commit/mirrors-golangci-lint
rev: v1.54.2 # 使用 golangci-lint 的版本
hooks:
- id: golangci-lint
args: [ "run", "--new-from-rev=HEAD" ] # 仅检查本次修改的代码
language_version: system # 使用系统的 Go 版本
YAML

由于 --new-from-rev=HEAD 参数不起作用,或者会引起typechecking error,所以最终放弃使用这种方式。

  • 首次运行会比较慢。
  • 错误位置打开技巧:使用IDE工具的话,可以选中错误日志的文件名:行号:列号,快捷键Ctrl+Shift+N触发搜索选中的文件,按
    Enter 自动打开到错误的位置。
  • Goland中可以安装go-lint插件配置实时扫查(比较耗电脑资源)

参考文档


golangci-lint 结合 git hook 扫查代码实践
https://blog.ishare.cool/2024/11/30/golangci-lint-with-pre-commit/
作者
吴哥
发布于
2024年11月30日
许可协议