前言
近期在開發 Go 專案時發現到提交代碼流程總是有些摩擦,像是開發時習慣用 Tab 到 GitLab 上寬度就會變得很奇怪,或是一些簡單的對齊問題都在無形消耗專注力,所以透過把一些原生的 Go 靜態檢查工具搬到 CI 上執行,確保統一的開發體驗。
Go 生態系真的很棒
早期我在處理前端專案要添加靜態分析會需要處理 TypeScript 的執行環境、ESLint、Prettier⋯⋯還有擴充有的沒的插件避免它們之間打架,而在 Go 已經有官方現成的靜態分析工具幫助開發者整理代碼的外貌與正確性:
就連開發環境也是很簡單只要裝個 VSCode Go 插件就能直接上場。而今天我只需要把相同的流程遷移到 CI 上確保「單一真相來源」的代碼可以被完善的驗證過即可。
把靜態分析放上 GitLab CI
我的專案使用 GitLab,流程基本上是:
- 用 golang alpine 映像可大幅節省容器尺寸,要留意 apt 與 bash 不存在要找替代或安裝
- 執行腳本
- 設置一下 git commit 自動推送(需相關 PAT 權限環境變數)
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 腳本如下:
#!/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 儲存時自動格式化
{ "[go]": { "editor.formatOnSave": true, "editor.defaultFormatter": "golang.go" },}