Community编程与开发github.com

tenequm/go-dev

一套有主见的现代Go开发环境,集成了golangci-lint v2、gofumpt、gotestsum、golang-migrate和just。适用于新Go项目启动、配置lint/格式化/测试/覆盖率/CI、编写Justfile、接入数据库迁移,或告别Makefile工作流。

go-dev 是什么?

该技能提供了一个高度集成且观点鲜明的Go开发工具栈,旨在消除工具碎片化,实现每个关注点只用一个工具、零重叠。它标准化了从代码格式化(gofumpt,gofmt的超集)、元Lint(golangci-lint v2)、测试(gotestsum)、任务编排(just替代Makefile)到数据库迁移(golang-migrate)和Git钩子(lefthook)的全链路。支持Claude Code、Cursor、Codex等主流AI代理平台,其核心交付物包括一个战斗级别的.golangci.yml配置、一个配备完整质量/测试/构建/数据库/CI组的Justfile,以及一份同样精密的lefthook.yml。所有工具都指定了最低版本号,这些版本号是功能依赖的关键(如golangci-lint v2.14.0才开始捆绑gofumpt v0.12.0),并提供了丰富的最佳实践说明、版本踩坑指南和Monorepo适配建议,确保开发环境在不同机器和CI上的高度可重现性。

兼容平台✓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 调用迁移到统一环境

不适合定期从上游合并的 fork:替换其 Makefile 并用 gofumpt 重新格式化整个代码树会在每次合并时产生冲突,并将真正的变更淹没在格式化差异中。应该保留上游的 gofmt 和构建工具,仅添加检查。

工具栈

工具版本作用替代品
Go1.27+语言、工具链、go mod、go fix-
golangci-lintv2.14+元 linter(100+ linter + formatter + fmt 命令)分别运行 gofmt、govet、staticcheck、errcheck
gofumptv0.12+严格格式化工具(gofmt 的超集,19 条默认规则)gofmt
gotestsumv1.13+测试运行器,输出可读、支持 watch 模式、JUnit XML原始 go test
just1.58+任务运行器Makefile
golang-migratev4.20+数据库迁移(CLI + 库 + embed.FS)手动 SQL 脚本
lefthookv2.1+Git 钩子(单个二进制,并行执行)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

不要通过 tools 模式安装 golangci-lint。 上游明确说明:"使用 go install/go get、tools pattern 和 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 来构建二进制文件" 用于 golangci-lint,并且它无法固定版本)。注意,go tool 会解析当前目录模块中的定义 — "额外的工具可以在当前模块的 go.mod 中定义" — 因此在 monorepo 中,如果 recipe 没有设置 [working-directory(...)],它会失败并报错 go: no such tool "..."。

在第一次提交之前,有两个 Go 命令行为值得了解:

  • 在 1.N 工具链下运行 go mod init 会写入 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 版本更旧的机器上,govulncheck 扫描 stdlib 安全公告时所针对的 go 版本,因此一个过时的补丁会自行导致 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 是重复的检查 — 但为了可读的差异输出,保留它。

Lefthook 配置

对于 Go 项目,lefthook 优于 pre-commit — 它是一个单一的 Go 二进制文件,可以并行运行钩子,并且不需要 Python。

go install github.com/evilmartians/lefthook/[email protected]   # 需要 Go 1.26+
lefthook install
# lefthook.yml
assert_lefthook_installed: true   # 如果 lefthook 未安装,则大声失败,而不是跳过所有规则

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 提供了一种灵活的方式来定义任务,同时支持 commands 和 scripts。Jobs 可以分组以实现更高级的流程控制。” commands: 并未废弃,并且仍然有完整的文档说明;当你需要分组、嵌套控制流或在一个钩子中混合使用内联命令和脚本时,可以使用 jobs:。

还有四个值得配置的:

  • 上面的 assert_lefthook_installed: true 是解决懒散陷阱的良药:"如果 $PATH 中找不到 lefthook 可执行文件,则失败(退出码 1)"。
  • CI 中的 lefthook validate 可以在畸形 lefthook.yml 导致钩子被静默禁用之前捕获它;当钩子行为与预期不符时,lefthook dump 可以打印合并后的有效配置。
  • 一个被 gitignore 的 lefthook-local.yml 允许开发者添加或跳过任务,而不会强加给团队成员 — “当你想要在本地使用 lefthook 但又不想强加给队友时,这很有用。”
  • 在 monorepo 中,为每个任务指定 root: 指向其模块目录;否则 go mod tidy 和 go tool 会在仓库根目录下运行并失败。
  • 在 mod-tidy 和 lint 任务上添加 skip: [merge, rebase],以防止它们在合并或变基过程中的提交中运行,从而避免解决冲突时同时重新暂存 tidy 重写或 --fix 编辑。

pre-push 中的测试套件与 CI 运行的内容重复。一旦它耗时数分钟,就删除它,或者将其范围限制在更改的包上 — 否则开发者会学会使用 LEFTHOOK=0 来推送。从钩子运行的测试也会继承 git 的钩子环境(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: 键下镜像这些检查,否则合并可能会带着未格式化的代码落地。

钩子命令从调用者的 PATH 中解析工具,因此缺少的二进制文件会以 bare exit 127 使提交失败。将它们运行为 go tool <name>(使用 tool 指令)可以使钩子像构建过程一样可重现。

Beta 特性,但值得了解:ai: 在同一个文件中声明 LLM 代理钩子 — “在 lefthook install 期间,lefthook 会生成特定于提供商的设置文件,以便代理在需要时调用 lefthook run <hook>。

...

(原文被截断)

Individual skills in this repo

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

相关技能