Improving Go code quality through CI static validation

在 CI 中通过自动化静态验证提升 Go 代码质量

前言

最近开发 Go 项目时发现提交流程有摩擦,例如本地用 Tab 在 GitLab 上显示宽度异常或对齐问题分散注意力,所以通过把一些原生的 Go 静态检查工具放到 CI 上执行,确保统一的开发体验。

Go 生态真的很棒

早期我在处理前端项目要添加静态分析会需要处理 TypeScript 的执行环境、ESLint、Prettier⋯⋯还有扩充有的没的插件避免它们之间打架,而在 Go 已经有官方现成的静态分析工具帮助开发者整理代码的外观与正确性:

就连开发环境也是很简单只要装个 VSCode Go🔗 插件就能直接上场。而今天我只需要把相同的流程迁移到 CI 上确保「单一真相来源」的代码可以被完善的验证过即可。

把静态分析放到 GitLab CI 上

我的项目使用 GitLab,流程基本上是:

  1. 用 golang alpine 镜像可大幅节省容器尺寸,要留意 apt 与 bash 不存在要找替代或安装
  2. 执行脚本
  3. 设定一下 git commit 自动推送(需相关 PAT 权限环境变量)
.gitlab-ci.yml
stages:
- check
lint-govet:
image: golang:1.26-alpine
stage: check
script:
- go vet ./...
lint-gofmt:
image: golang:1.26-alpine
stage: check
before_script:
- apk add --no-cache git bash
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
script:
- chmod +x scripts/gofmt.sh
- |
./scripts/gofmt.sh format || FORMAT_EXIT_CODE=$?
[ "${FORMAT_EXIT_CODE:-0}" -eq 2 ] && { echo "No files need formatting"; exit 0; }
if [ -n "$(git status --porcelain)" ]; then
git add -A
git commit -m "refactor: Auto gofmt [skip ci]"
git push "https://project_access_token:${GOFMT_DOC_AUTO_GEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:main
else
echo "No formatting changes detected."
fi
rules:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_MESSAGE !~ /\[skip ci\]/

因为之前没有 gofmt 整理的习惯,所以如果一次全部自动改动合并冲突会解不完,所以暂且有改动到的文件才执行,这里使用了 gofmt.sh 的 bash 脚本如下:

gofmt.sh
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
show_help() {
echo -e "${BLUE}Go 代码格式化工具${NC}"
echo ""
echo "用法: $0 [命令]"
echo ""
echo "可用命令:"
echo " format 格式化 PR 修改的 Go 档案(预设)"
echo " check 检查但不修改档案"
echo " help 显示此帮助资讯"
echo ""
echo "范例:"
echo " $0 format # 格式化 PR 修改的档案"
echo " $0 check # 仅检查格式"
}
check_dependencies() {
if ! command -v go &> /dev/null; then
echo -e "${RED}错误: 未找到 Go 编译器${NC}"
exit 1
fi
}
get_target_branch() {
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
echo "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
elif [ -n "$CI_COMMIT_SHA" ]; then
echo "HEAD"
else
echo "origin/master"
fi
}
get_modified_files() {
cd "$PROJECT_ROOT"
local target_branch="${1:-origin/master}"
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
git diff --name-only "$CI_MERGE_REQUEST_DIFF_BASE_SHA" HEAD -- '*.go' 2>/dev/null || echo ""
elif git rev-parse "$target_branch" &>/dev/null; then
git diff --name-only "$target_branch" HEAD -- '*.go' 2>/dev/null || echo ""
else
git diff --name-only --cached -- '*.go' 2>/dev/null
git diff --name-only -- '*.go' 2>/dev/null
fi
}
format_go_files() {
local mode="${1:-write}"
echo -e "${BLUE}正在取得修改的 Go 档案...${NC}"
cd "$PROJECT_ROOT"
local modified_files
modified_files=$(get_modified_files)
if [ -z "$modified_files" ]; then
echo -e "${YELLOW}未发现修改的 Go 档案${NC}"
return 0
fi
echo -e "${BLUE}发现修改的档案:${NC}"
echo "$modified_files"
echo ""
local files_to_format=""
while IFS= read -r file; do
[ -z "$file" ] && continue
if [ -f "$file" ]; then
files_to_format="$files_to_format $file"
fi
done <<< "$modified_files"
if [ -z "$files_to_format" ]; then
echo -e "${YELLOW}没有需要格式化的档案${NC}"
return 0
fi
if [ "$mode" = "check" ]; then
echo -e "${BLUE}检查 Go 档案格式(仅检查)...${NC}"
local diff_output
diff_output=$(gofmt -d $files_to_format 2>&1)
if [ -n "$diff_output" ]; then
echo -e "${YELLOW}以下档案格式不符合 gofmt 标准:${NC}"
echo "$diff_output"
echo ""
echo -e "${RED}格式检查未通过${NC}"
return 1
else
echo -e "${GREEN}✓ 所有档案格式检查通过${NC}"
return 0
fi
else
echo -e "${BLUE}格式化 Go 档案...${NC}"
gofmt -w $files_to_format
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ Go 档案格式化完成!${NC}"
local changes
changes=$(git status --porcelain $files_to_format 2>/dev/null)
if [ -n "$changes" ]; then
echo -e "${YELLOW}以下档案被格式化:${NC}"
echo "$changes"
return 0
else
echo -e "${YELLOW}没有档案需要格式化(已符合格式)${NC}"
return 2
fi
else
echo -e "${RED}✗ Go 档案格式化失败${NC}"
return 1
fi
fi
}
main() {
check_dependencies
case "${1:-format}" in
format|f)
format_go_files "write"
;;
check|c)
format_go_files "check"
;;
help|h|--help|-h)
show_help
;;
*)
echo -e "${RED}未知命令: $1${NC}"
echo ""
show_help
exit 1
;;
esac
}
main "$@"

VSCode 保存时自动格式化

.vscode/setting.json
{
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
}