DEV Community

Cover image for Add`go fix` to Your CI Pipeline
Jose Corral
Jose Corral

Posted on

Add`go fix` to Your CI Pipeline

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.Slice calls → slices.Sort

How it works

Run it like you’d run go teston packages, not files:

# Apply fixes in-place
go fix ./...
Enter fullscreen mode Exit fullscreen mode
# Preview changes without applying them
go fix -diff ./...
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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)