Improving Go code quality through CI automated 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 "[email protected]"
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"
},
}