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. Consider:
-
interface{}->any - direct loop searches → slices.Contains
- Or cumbersome
sort.Slicecalls →slices.Sort
How it works
Run it like you’d run go teston 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 it’s empty, your code is already modern. If not, something is up for attention.
The tool is version-aware. It will read the go directive from your go.mod, and propose fixes only relevant to that version. A project on go 1.21 ill get min/max and slices.Contains rewrites, but not for range int (that’s 1.22+). Update your go.mod, and new modernizations will be enabled automatically.
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 brings (or leaves behind) code that could be refactored, the test will fail and print the diff exactly. The dev will run go fix ./... locally, commit, and push. Done.
A few things to note for multi-platform repos: go fix will only scan one GOOS/GOARCH per run. If you have platform-specific files, you might want to run it with different 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?
Here’s a realistic before/after to give you a feel for the kind of changes go fix makes:
// 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 not only cosmetic changes slices.Contains is easier to understand and avoids a whole class of off-by-one errors in manually written loops. min/max are built-ins since Go 1.21and convey meaning directly.
Other typical changes include replacing interface{} with any, swapping context.WithCancel(contest.Background()) for t.Context() in tests, and deleting the no-longer-needed x:=x loop variable capture that Go 1.22 made redundant.
Tip: run it twice
Some fixes introduce new patterns that other analyzers can then optimize. Running go fix ./... a second time often reveals these follow-up optimizations. In reality, two passes is 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), introducing over 24 modernizer analyzers and a new //go:fix inline directive that enables library authors to mark functions for automatic call-site inlining during migrations. If you’re using an earlier version of Go, you’ll have fewer analyzers at your disposal, but the CI pattern above remains the same. For the full scoop, see the official blog post.
Start today
The cost of entry is zero. You already have go fix installed, it comes with the Go toolchain. Add the CI step, run go fix ./... once on your codebase to flush out 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.
Top comments (0)