Pre-commit Hooks & Review Checklists
Pre-commit hooks run fast quality checks on every commit.
PR review checklists capture what automation cannot see: API shape, failure modes, and operability.
Together they shorten feedback loops without replacing CI as the merge authority.
Recipe
Quick-reference recipe card - copy-paste ready.
# scripts/pre-commit.sh
#!/usr/bin/env bash
set -euo pipefail
changed=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' || true)
[ -z "$changed" ] && exit 0
echo "$changed" | xargs goimports -local example.com/mycorp -w
echo "$changed" | xargs gofmt -s -w
echo "$changed" | xargs go vet
go test ./...chmod +x scripts/pre-commit.sh
ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commitWhen to reach for this:
- Contributors push formatting-only CI failures repeatedly.
- Onboarding a team where
make checkis not yet habitual. - Standardizing PR templates for Go services.
- Splitting "must pass locally" vs "CI only" checks in platform docs.
Working Example
.pre-commit-config.yaml (optional framework):
repos:
- repo: local
hooks:
- id: goimports
name: goimports
entry: goimports -local example.com/mycorp -w
language: system
types: [go]
- id: go-test-quick
name: go test
entry: go test ./...
language: system
pass_filenames: falsePR template excerpt (.github/pull_request_template.md):
## Go quality checklist
- [ ] `make check` passes locally
- [ ] New code has tests for error paths
- [ ] No `context.Background()` in request handlers
- [ ] HTTP/gRPC clients time out and respect cancellation
- [ ] Migrations or config changes documented in PR bodyContributor flow:
git add internal/api/handler.go
git commit -m "add health handler"
# pre-commit runs goimports, gofmt, go vet on staged .go files, then go test ./...What this demonstrates:
- Scope hooks to staged
.gofiles for speed. - PR checklist items target review judgment vet does not cover.
make checkremains the documented full gate before push.
Deep Dive
How It Works
- Git invokes
.git/hooks/pre-commitbefore creating a commit object. - Non-zero exit aborts the commit, keeping bad state local.
- pre-commit framework manages hook versions and sharing via committed YAML.
- CI still runs on clean checkouts with pinned tool versions.
Hook Tiering
| Tier | Checks | Target duration |
|---|---|---|
| Pre-commit | goimports, gofmt, go vet on staged files | < 15s |
| Pre-push | go test ./..., golangci-lint | < 2m |
| CI | race, govulncheck, integration | minutes |
PR Review Checklist Categories
| Area | Reviewer questions |
|---|---|
| Errors | Wrapped with context? Sentinel errors documented? |
| Concurrency | Mutex/channel ownership clear? Context canceled on exit? |
| HTTP/gRPC | Timeouts, body close, status codes correct? |
| Observability | Logs structured with request ID? Metrics for new paths? |
| Security | Input validated? Secrets not logged? SQL parameterized? |
| Tests | Table-driven cases for errors? Race-prone code under -race in CI? |
Go Notes
.PHONY: check hook-install
hook-install:
ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
check:
golangci-lint run ./...
go test -race ./...Document make hook-install in README after clone.
Gotchas
- Running golangci-lint on every commit - Slow hooks get
--no-verifyabuse. Fix: Move heavy lint to pre-push or CI only. - Hooks not installed after clone - New hires skip hooks silently. Fix:
make hook-installin onboarding doc or use pre-commitpre-commit install. - Different goimports
-localthan CI - Commit passes hook, fails pipeline. Fix: Single source of truth inscripts/shared by hook and workflow. - Testing only changed packages incorrectly - Misses breakage in dependents. Fix: Pre-commit runs
go test ./...orgo test $(go list ./...)at minimum. - Checklist items never updated - Rubber-stamp reviews. Fix: Trim to 5-8 high-signal items; link deep rubric to Code Review Standards.
- Auto-format without re-stage - Commit omits formatted lines. Fix: Re-add files in hook or use pre-commit's
stages: [commit]with proper file refresh.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| CI-only gates | Tiny team, always online | Latency-sensitive monorepos |
| pre-push instead of pre-commit | Format noise annoys during WIP commits | You need to block unformatted history |
| Bot auto-fix PRs | High OSS contributor volume | Strict audit trails on main |
| Reviewdog in CI | Surface lint on diff only | Replacing local fast feedback entirely |
FAQs
Should hooks run go test -race?
Usually no on every commit - too slow.
Run race in CI and optionally on pre-push for concurrency-heavy changes.
How do I skip hooks legitimately?
git commit --no-verify for emergencies.
Require incident post-mortem if used on main-bound work.
Do hooks work with go.work monorepos?
Yes.
Run from workspace root; go test ./... respects all modules in the work file.
Should generated files be formatted in hooks?
Format on generation, not in every commit hook.
Exclude *.pb.go from staged checks if codegen already formats.
How does this relate to gopls format on save?
Editor save reduces hook failures.
Hooks still catch contributors using different editors or CLI-only workflows.
What belongs in PR template vs linter config?
Automate objective rules (format, errcheck).
Checklist covers design, operability, and security judgment.
Can hooks call Docker?
Possible but slow.
Prefer native go toolchain on the host for laptop hooks.
How do remote teams share hooks?
Commit scripts/pre-commit.sh or .pre-commit-config.yaml.
Document install step; do not commit .git/hooks directly.
Should reviewers re-run make check?
Spot-check on non-trivial PRs.
Trust CI for authoritative signal; spot-check when touching build tags or go.mod.
How often update the checklist?
Quarterly or when incident retros cite missed review themes.
Fewer, sharper items beat long generic lists.
Related
- Code Quality Basics - minimal hook script
- gofmt & goimports Enforcement - format flags for hooks
- golangci-lint Configuration - what to defer to CI
- Code Review Standards for Go Teams - reviewer rubric
- CI Quality Gates: Lint, Test, Race, Vuln - authoritative merge checks
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).