Community程式設計與開發github.com

tene/qum/go-dev

一套有主見的 Go 開發環境,整合 golangci-lint v2、gofumpt、gotestsum、golang-migrate 與 just。適合用於啟動 Go 專案、配置 lint、格式化、測試、覆蓋率或 CI、撰寫 Justfile、建立資料庫遷移或取代 Makefile 工作流程。

qum/go-dev 是什麼?

這是一個為 Go 開發者設計的專業技能集(Skill),提供一套現代化且經過實戰驗證的開發工具鏈,每個工具專注於單一職責,避免功能重疊。它整合了 golangci-lint v2(作為元 linter)、gofumpt(嚴格的格式化工具)、gotestsum(具可讀性輸出的測試執行器)、just(任務執行器,可取代 Makefile)以及 golang-migrate(資料庫遷移工具)。該技能集支援 Claude Code、Codex 等 AI 編碼代理,並提供完整的設定範例,包括 `.golangci.yml` 設定檔、Justfile、Lefthook Git hooks 設定以及最佳實務說明。它明確指定了最低版本要求,並解釋為何這些版本約束是關鍵的。此外,它也涵蓋了常見的陷阱與注意事項,例如 golangci-lint 不應透過 Go 工具模式安裝、`go mod init` 的版本行為,以及如何在 CI 環境中固定 toolchain 版本。對於正在建立新 Go 專案、需要標準化 lint/format/test 流程,或希望從 Makefile 遷移至 Justfile 的開發者來說,這是一個理想的一站式解決方案。

相容平台✓Claude Code✓Codex CLI✓Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/go-dev

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Go 開發技術棧

一套有主見、現代化的 Go 開發環境。每個工具專注於單一職責,零功能重疊。

何時使用

  • 從零開始建立新的 Go 專案
  • 新增 lint、格式化或測試基礎設施
  • 為 Go 服務或函式庫設定 CI/CD
  • 建立 Justfile 以取代 Makefile
  • 新增資料庫遷移工具
  • 從分散的 gofmt/govet/staticcheck 調用遷移到統一的設定

不適用於定期合併上游程式碼的分支:替換其 Makefile 並使用 gofumpt 重新格式化整個目錄樹會在每次合併時產生衝突,並將實際的改動淹沒在重新格式化的 diff 中。請保留上游的 gofmt 和建置工具,僅新增檢查項目。

工具棧

工具版本角色取代
Go1.27+語言、工具鏈、go mod、go fix-
golangci-lintv2.14+元 linter(100+ 個 linter + 格式化工具 + fmt 指令)分開執行的 gofmt, govet, staticcheck, errcheck
gofumptv0.12+嚴格的格式化工具(gofmt 的超集,19 條預設規則)gofmt
gotestsumv1.13+測試執行器,提供可讀性輸出、監聽模式、JUnit XML原始的 go test
just1.58+任務執行器Makefile
golang-migratev4.20+資料庫遷移(CLI + 函式庫 + embed.FS)手動 SQL 腳本
lefthookv2.1+Git hooks(單一二進位檔,平行執行)pre-commit (Python)

版本下限至關重要。 golangci-lint「支援低於或等於其編譯時所用 Go 版本的 Go 版本」——如果指定的版本比你的 Go 工具鏈還舊,它就會直接失敗。Go 1.27 的支援在 golangci-lint v2.13.0 中加入,因此 v2.13 是 Go 1.27 專案的下限;本技能指定 v2.14.0,這是第一個捆綁 gofumpt v0.12.0 的發行版。最近還有兩個版本下限發生變動:gofumpt v0.12.0「基於 Go 1.27 的 gofmt,並且需要 Go 1.26 或更新版本」,而 lefthook 的 go install 路徑現在要求 Go 1.26+。

快速入門:新專案

# 1. 建立模組
mkdir myapp && cd myapp
go mod init github.com/yourorg/myapp

# 2. 建立資料夾結構
mkdir -p cmd/myapp internal migrations

# 3. 以二進位檔安裝 golangci-lint,而不是作為模組工具(見下方說明)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0

# 4. 將其餘工具記錄在 go.mod 中(Go 1.24+ 的 tool 指令)。指定版本——切勿使用 @latest,
#    因為這會在每次 CI 執行時重新編譯工具,並導致不同機器間的版本漂移。
go get -tool mvdan.cc/[email protected]
go get -tool gotest.tools/[email protected]

# golang-migrate 需要一個建置標籤,因此直接安裝
# 注意:此處為示範,實際安裝請依官方文件確認
# go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/[email protected]

# 5. 建立設定檔(範本如下)
# 6. 執行:just check

不要透過工具模式安裝 golangci-lint。 上游明確指出:「使用 go install/go get、『工具模式』以及 tool 指令/指示的安裝方式無法保證正常運作。我們建議使用二進位檔安裝。」這在共用儲存庫中之所以重要,是因為「一個工具的依賴項可能會修改另一個工具或你專案的依賴項」。如果你必須將其納入 go.mod,請使用自己的 -modfile 將其隔離——請參閱 golangci-lint 參考文件。

go get -tool 負責追蹤;go tool 負責執行。 tool 指令會將依賴項記錄在 go.mod 中,但不會將其加入 PATH。你可以透過工具鏈來呼叫它——go tool gofumpt -l .、go tool gotestsum --format testname——或者執行 go install tool 一次,將其安裝到 $(go env GOPATH)/bin。下方的 Justfile 直接呼叫二進位檔,因此它假設使用了 go install tool 路徑(或透過 Homebrew 進行系統安裝——但「Homebrew 可能使用意外版本的 Go 來編譯二進位檔」,而且它無法鎖定版本)。請注意,go tool 會根據當前目錄的模組進行解析——「其他工具可以在當前模組的 go.mod 中定義」——因此在 monorepo 中,如果 recipe 沒有設定 [working-directory(...)],它會失敗並顯示 go: no such tool "..."。

在第一次提交之前,有兩個 Go 指令的行為值得了解:

  • go mod init 在 1.N 工具鏈下會寫入 go 1.(N-1).0,而不是 1.N——「使用版本 1.N.X 的工具鏈執行 go mod init 會建立一個指定 Go 版本為 go 1.(N-1).0 的 go.mod 檔案。」如果你想要使用 1.N 的語言功能,請有意識地提升此指令。
  • 在 go.mod 中設定 toolchain go1.27.1 行,但要知道它是一個下限,而非鎖定:在預設的 GOTOOLCHAIN=auto 下,go 指令僅在「如果 <tname> 比預設的 Go 工具鏈更新」時才會切換,因此本地較新的 go 將會勝出。若要鎖定確切的工具鏈,請設定 GOTOOLCHAIN=go1.27.1(在 CI 或使用 go env -w);GODEBUG=toolchaintrace=1 go version 會顯示實際選擇了哪個工具鏈。請將這一行保持在目前的修補版本,而不是 .0:它會選擇 go,任何機器上若其自身的 go 版本較舊,govulncheck 就會根據該版本掃描標準函式庫的建議,因此一個過時的修補版本會自行讓 CI 失敗——請參閱下方的「陷阱」章節。

.golangci.yml

version: "2"

run:
  timeout: 5m
  build-tags:
    - integration   # 否則 Justfile 中 integration 標籤後面的檔案永遠不會被 lint

linters:
  default: standard
  enable:
    - bodyclose
    - copyloopvar
    - dupl
    - durationcheck
    - err113
    - errname
    - errorlint
    - exhaustive
    - exptostd
    - fatcontext
    - goconst
    - gocritic
    - gosec
    - intrange
    - misspell
    - modernize
    - musttag
    - nakedret
    - nestif
    - nilerr
    - noctx
    - nolintlint
    - nonamedreturns
    - perfsprint
    - prealloc
    - revive
    - sqlclosecheck
    - testifylint
    - thelper
    - unconvert
    - unparam
    - usestdlibvars
    - usetesting
    - wastedassign
    - whitespace
    - wrapcheck
  settings:
    govet:
      enable:
        - shadow
    gocritic:
      enabled-checks:
        - nestingReduce
    revive:
      enable-all-rules: true
      rules:
        # enable-all-rules 會開啟 `unhandled-error`,它會標記 main 中的 `fmt.Println`。
        # 在 enable-all-rules 下,規則的 `arguments` 會被忽略(規則會註冊兩次),
        # 因此白名單在這裡不起作用——只有 `disabled` 有效。
        - name: unhandled-error
          disabled: true
    errcheck:
      check-type-assertions: true
  exclusions:
    generated: strict
    presets:
      - comments
      - std-error-handling
      - common-false-positives
    rules:
      - path: _test\.go
        linters:
          - errcheck
          - dupl
          - gosec
          - wrapcheck

formatters:
  enable:
    - gofumpt
    - goimports
  settings:
    gofumpt:
      # 單獨選擇規則。`extra-rules: true` 已棄用,而且它還會
      # 開啟 `balance_calls`,而 gofumpt 本身已將其降級為有爭議的規則。
      extra:
        group-params: true
        clothe-returns: true
        balance-calls: false
  exclusions:
    generated: strict
    paths:
      - vendor/

output:
  formats:
    text:
      path: stdout
      print-linter-name: true
      colors: true
  sort-order:
    - linter
    - file
  show-stats: true

Justfile

set shell := ["bash", "-euo", "pipefail", "-c"]
set dotenv-load

binary := "myapp"

[private]
default:
    @just --list --unsorted

# ── 程式碼品質 ──────────────────────────────────────────

# 格式化所有 Go 程式碼
[group('quality')]
fmt:
    golangci-lint fmt ./...

# 檢查格式而不修改(適用於 CI)
[group('quality')]
fmt-check:
    golangci-lint fmt --diff ./...

# 執行 linter
[group('quality')]
lint:
    golangci-lint run ./...

# 執行 linter 並自動修復
[group('quality')]
lint-fix:
    golangci-lint run --fix ./...

# 執行漏洞檢查
[group('quality')]
vuln:
    govulncheck ./...

# ── 測試 ───────────────────────────────────────────────

# 執行所有測試,包含競態檢測
[group('test')]
test *args="./...":
    gotestsum --format testname -- -race {{ args }}

# 執行測試並產生覆蓋率報告
[group('test')]
test-cov:
    gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...
    go tool cover -func=coverage.out

# 在瀏覽器中開啟覆蓋率報告
[group('test')]
coverage: test-cov
    go tool cover -html=coverage.out

# 執行整合測試
[group('test')]
test-integration:
    gotestsum --format testname -- -race -tags=integration ./...

# 開發期間監控測試
[group('test')]
test-watch:
    gotestsum --watch --watch-clear --format testname

# 執行效能基準測試
[group('test')]
bench:
    go test -bench=. -benchmem ./...

# ── 建置 ─────────────────────────────────────────────────

# 建置二進位檔
[group('build')]
build:
    go build -o {{ binary }} ./cmd/{{ binary }}

# 建置最佳化的發行版二進位檔
[group('build')]
build-release:
    CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o {{ binary }} ./cmd/{{ binary }}

# ── 依賴項 ──────────────────────────────────────────

# 整理並驗證模組
[group('deps')]
tidy:
    go mod tidy
    go mod verify

# 如果 go.mod/go.sum 未整理好則失敗,但不修改它們(適用於 CI)
[group('deps')]
tidy-check:
    go mod tidy -diff

# 執行程式碼產生器
[group('deps')]
generate:
    go generate ./...

# ── 資料庫 ──────────────────────────────────────────────

# 套用所有待處理的遷移
[group('db')]
migrate-up:
    migrate -path migrations -database "$DATABASE_URL" up

# 復原上一次遷移
[group('db')]
migrate-down:
    migrate -path migrations -database "$DATABASE_URL" down 1

# 建立新的遷移
[group('db')]
migrate-create name:
    migrate create -ext sql -dir migrations -seq {{ name }}

# ── CI ────────────────────────────────────────────────────

# 完整的 CI 閘道(格式檢查 + lint + 測試)
[group('ci')]
check: fmt-check lint test
    @echo "所有檢查通過"

# 清理建置產物
[group('ci')]
clean:
    go clean
    rm -f {{ binary }} coverage.out

check 刻意不包含 vuln:govulncheck 會連線到網路,而一個漏洞資料庫的更新可能會在一個未改動任何內容的變更上將閘道變成紅色。請將其作為獨立的 CI 任務來執行。golangci-lint run 也會執行已啟用的格式化工具並報告它們的問題,因此 fmt-check 會重複該檢查——但仍然保留它以獲得可讀的 diff。

Lefthook 設定

對於 Go 專案,Lefthook 優於 pre-commit——它是一個單一的 Go 二進位檔,可以平行執行 hooks,並且不需要 Python。

go install github.com/evilmartians/lefthook/[email protected]   # 需要 Go 1.26+
lefthook install
# lefthook.yml
assert_lefthook_installed: true   # 大聲失敗,而不是跳過所有規則

pre-commit:
  piped: true   # 快速失敗——在第一個失敗的任務處停止
  commands:
    fmt:
      glob: "*.go"
      run: golangci-lint fmt {staged_files}
      stage_fixed: true
    lint:
      glob: "*.go"
      # 永遠不要將純檔案列表傳遞給 `golangci-lint run`:跨越兩個
      # 目錄的列表會直接被拒絕,而一個多檔案套件中的單一檔案
      # 會報告幽靈般的 `undefined:` 型別檢查錯誤。請改為對套件進行 lint。
      run: printf '%s\n' {staged_files} | xargs -n1 dirname | sort -u | xargs golangci-lint run --fix
      stage_fixed: true
    mod-tidy:
      glob: "*.{go,mod,sum}"
      run: go mod tidy

pre-push:
  commands:
    test:
      run: go test -race ./...

piped: true 是快速失敗,而非排序——lefthook「預設依序執行指令和腳本」,而 piped 新增了「如果其中一個失敗則停止執行指令和腳本。」它不能與 parallel: true 結合使用。

jobs:(在 lefthook 1.10.0 中新增)是較新的原始語法,與 commands:/scripts: 拆分並存——「Jobs 提供了一種靈活的方式來定義任務,同時支援指令和腳本。Jobs 可以分組以實現進階的流程控制。」commands: 並未被棄用,並且仍然有完整的文件說明;當你需要分組、巢狀流程控制或在一個 hook 中混合使用內聯指令和腳本時,可以考慮使用 jobs:。

還有四個值得串接的項目:

  • 上方的 assert_lefthook_installed: true 是解決下方休眠陷阱的解毒劑:「如果無法在 $PATH 中找到 lefthook 可執行檔,則失敗(退出碼 1)」。
  • 在 CI 中使用 lefthook validate 可以在一個格式錯誤的 lefthook.yml 悄悄停用 hooks 之前將其捕獲;當一個 hook 的行為與編寫的不符時,lefthook dump 會列印合併後的有效設定。
  • 一個被 gitignore 的 lefthook-local.yml 允許開發者新增或跳過任務,而不會強加給隊友——「當你想在本機使用 lefthook 而不強加給隊友時,這非常有用。」
  • 在 monorepo 中,為每個 job 指定一個指向其模組目錄的 root:;否則 go mod tidy 和 go tool 會針對 repo 根目錄執行並失敗。
  • 在 mod-tidy 和 lint 任務上使用 skip: [merge, rebase] 可以讓它們在進行合併或 rebase 期間不參與提交,這樣解決衝突時也不會同時重新整理 tidy 改寫或 --fix 編輯。

pre-push 的測試套件會重複 CI 已經執行過的內容。一旦它需要幾分鐘,就將其刪除或將其範圍限定為已更改的套件——否則開發者會學會使用 LEFTHOOK=0 來推送。從 hook 執行的測試也會繼承 git 的 hook 環境(GIT_DIR、GIT_INDEX_FILE、GIT_WORK_TREE),因此在 t.TempDir() 中執行 git init 或 git commit 的測試輔助函式會作用於真實的儲存庫——請在 cmd.Env 中清除這些變數。

一個無衝突的 git merge 永遠不會執行 pre-commit:git 會改為呼叫 pre-merge-commit,並且僅在你使用 git commit 完成一個有衝突的合併時才會執行 pre-commit。請在 pre-merge-commit: 鍵下鏡像這些檢查,否則合併會導致未經 lint 的程式碼落地。

Hook 指令會從呼叫者的 PATH 解析工具,因此缺少的二進位檔會以純粹的 exit 127 導致提交失敗。將它們作為 go tool <name>(tool 指令)執行可以使 hook 像建置過程一樣具有可重現性。

處於 Beta 階段,但值得了解的是:ai: 可以在同一個檔案中宣告 LLM 代理 hook——「在 lefthook install 期間,lefthook 會產生提供者特定的設定檔,以便代理在觸發 hook 時呼叫 lefthook run <hook>。」

陷阱

在嘗試找出工作流程中的問題時,請檢查這些項目:

  • 休眠的 lefthook:一個團隊成員從未執行 lefthook install,因此 Git hooks 從未執行。assert_lefthook_installed: true 會讓 git commit 失敗。
  • 沉默的 lefthook:執行 lefthook install 會設定預提交 hook,但 lefthook uninstall 不會觸發任何錯誤;向 git 詢問哪個 hook 處於作用中狀態:ls -laF .git/hooks/pre-commit。
  • 文明衝突:pre-commit hook 中的 go mod tidy 可能會修改 go.sum,從而導致一個部分提交,其中索引與工作目錄不同步。stage_fixed: true 會重新暫存檔案,但前提是返回的檔案與提交內容相符——如果 tidy 新增了一個先前未暫存的條目,則提交會因 dirty index 而失敗。
  • golangci-lint fmt 與目錄:golangci-lint fmt {staged_files} 會在檔案列表跨越兩個目錄時失敗。
  • Go 工具鏈政策:設定 toolchain go1.27.0 意味著 go 指令永遠不會自行降級到該版本以下——如果本機的 go 是 1.27.3,它就會使用 1.27.3。如果 policy 想要 1.27.0,請使用 GOTOOLCHAIN=go1.27.0。只要 go.mod 指定 go 1.27,任何 1.27.x 工具鏈都可以編譯程式碼;但 govulncheck 會根據 toolchain 行中提到的特定補丁來掃描 CVE。一個在 1.27.0 上通過但在 1.27.3 上失敗的 CVE 警報是真實的——go.mod + toolchain 應該始終是最新的補丁。
  • macOS 上來自 Homebrew 的 golangci-lint:根據上游的說法,「在 macOS 上,Homebrew 可能會使用意外版本的 Go 來建置 golangci-lint」——這表示 lint 結果可能因為 Go 版本而有所不同。在 CI 中使用 install.sh,並考慮將其用於本機開發。
  • Go workspaces:golangci-lint 僅在檔案層級運作,並且不會在 workspace 模式下自動將子模組視為套件——你需要為每個 module 單獨呼叫它。go tool 指令遵循 go.work 來解析工具。gotestsum 作為一個獨立的二進位檔,不受影響。
  • golangci-lint 快取:golangci-lint 會快取 lint 結果;一個毀損的快取(例如,由於 Go 版本變更)可能會導致持續的誤判。golangci-lint cache clean 可以清除它。在本機上使用 GOLANGCI_LINT_CACHE 來隔離不同專案。
  • go mod tidy -diff 在空白儲存庫中:如果 go.sum 不存在,則會失敗並顯示 error verifying module... sum is missing。go mod verify 也依賴於一個存在的 go.sum。順序應該是 go mod tidy && go mod verify,而不是反過來。
  • golangci-lint v2 fmt 子指令:不同於 gofumpt,golangci-lint fmt 不會匯入重新排序——它只格式化。如果你的工作流程依賴於 goimports 來管理匯入,請保持 gofumpt + golangci-lint fmt 的組合。
  • 從 Makefile 遷移:如果你從一個已存在的 Makefile 遷移,請先備份它——just 是一個任務執行器,它本身不能執行完整的 shell,也沒有內建的條件式。just recipe 是純執行緒的,而不是聲明式的。
  • golangci-lint fmt 與 --fix:golangci-lint fmt 是格式化工具的前端。golangci-lint run --fix 是 linter 修正的前端。fmt 子指令不會執行 linter,反之亦然。如果一個 linter 和一個格式化工具報告了相同的問題(例如,gofumpt 在兩者中都已啟用),你會看到重複的報告——解決方案是在 linters.settings 中停用格式化工具的重複報告,或者讓一個工具承擔該責任。
  • pre-commit 與 pre-push:如果 pre-push hook 需要很長時間(例如,執行完整的測試套件),開發者會找到繞過的方法。建議將 pre-commit hook 保持輕量(格式化 + lint),並將 pre-push hook 限制為僅檢查已更改的套件。

啟動腳本

以下是一個用於引導新 Go 專案的範例腳本:

#!/usr/bin/env bash
set -euo pipefail

PROJECT=$1
MODULE=${2:-"github.com/yourorg/$PROJECT"}

mkdir -p "$PROJECT/cmd/$PROJECT" "$PROJECT/internal" "$PROJECT/migrations"
cd "$PROJECT"
go mod init "$MODULE"

# 根據需要複製設定檔……
cat > .golangci.yml << 'EOF'
version: "2"
# ... 在此處貼上上方完整的設定……
EOF

cat > Justfile << 'JUST'
# ... 在此處貼上上方完整的 Justfile……
JUST

echo "Project $PROJECT 已初始化"

參考資料

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

相關技能