Add`go fix` to Your CI Pipeline
Source: Dev.to
Introduction
Most Go programmers have never invoked go fix in their CI pipeline. It’s been a dormant command for over a decade, originally designed for pre‑Go 1.0 migrations, then left to rust. But it still works, and when hooked up to your CI pipeline, it becomes a stealthy enforcer that prevents your codebase from falling into antiquated ways.
The concept is simple: go fix will automatically refactor your code to conform to more modern Go idioms. Examples:
interface{}→any- Direct loop searches →
slices.Contains - Cumbersome
sort.Slicecalls →slices.Sort
How it works
Run it like you’d run go test on packages, not files:
# Apply fixes in‑place
go fix ./...
# Preview changes without applying them
go fix -diff ./...
The -diff flag is the magic for CI integration. It prints a unified diff of what would change without modifying any files. If the output is empty, your code is already modern. If not, something needs attention.
The tool is version‑aware. It reads the go directive from your go.mod and proposes fixes only relevant to that version. A project on Go 1.21 will get min/max and slices.Contains rewrites, but not the range int rewrite that’s available in Go 1.22+. Updating go.mod automatically enables new modernizations.
The CI step
Here’s a GitHub Actions job that will fail if go fix finds modernization opportunities. Add it to your existing workflow:
name: Go Fix Check
on:
pull_request:
push:
branches: [main, develop]
jobs:
gofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check for go fix suggestions
run: |
OUTPUT=$(go fix -diff ./... 2>&1)
if [ -n "$OUTPUT" ]; then
echo "::error::go fix found modernization opportunities"
echo ""
echo "$OUTPUT"
echo ""
echo "Run 'go fix ./...' locally and commit the changes."
exit 1
fi
echo "✓ No go fix suggestions - code is up to date."
When a PR introduces (or leaves behind) code that could be refactored, the job fails and prints the diff. The developer runs go fix ./... locally, commits the changes, and pushes again.
Multi‑platform repos
go fix scans one GOOS/GOARCH per run. For projects with platform‑specific files, you can check multiple build modes:
- name: Check go fix (multi‑platform)
run: |
for PAIR in "linux/amd64" "darwin/arm64" "windows/amd64"; do
GOOS="${PAIR%/*}" GOARCH="${PAIR#*/}" go fix -diff ./... 2>&1
done
What actually gets fixed?
A realistic before/after illustrates typical changes:
// Before
func contains(s []string, target string) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
// After: go fix ./...
func contains(s []string, target string) bool {
return slices.Contains(s, target)
}
// Before
for i := 0; i < len(items); i++ {
fmt.Println(i, items[i])
}
// After: go fix ./...
for i := range len(items) {
fmt.Println(i, items[i])
}
These are more than cosmetic; slices.Contains is easier to understand and avoids off‑by‑one errors. min/max are built‑ins since Go 1.21 and convey intent directly.
Other typical changes include:
- Replacing
interface{}withany - Swapping
context.WithCancel(context.Background())fort.Context()in tests - Removing redundant loop variable captures made unnecessary in Go 1.22
Tip: run it twice
Some fixes introduce patterns that other analyzers can then optimize. Running go fix ./... a second time often reveals follow‑up optimizations. In practice, two passes are usually sufficient to reach a fixed point.
Go 1.26 makes this even better
Go 1.26 rewrote go fix from scratch on top of the Go analysis framework (the same one used by go vet), adding over 24 modernizer analyzers and a new //go:fix inline directive that lets library authors mark functions for automatic call‑site inlining during migrations. If you’re on an earlier version, you’ll have fewer analyzers, but the CI pattern remains the same. See the official blog post for the full scoop.
Start today
The cost of entry is zero. go fix comes with the Go toolchain. Add the CI step, run go fix ./... once on your codebase to clear the backlog, and let the CI pipeline keep things tidy from there on out.
Your future self, browsing a PR diff that lacks a manually written contains loop, will thank you.