# Modernizing Go code with go fix and Droids

> Go modernization with go fix starts from the rewritten modernizer suite in Go 1.26 and later. Droids handle major-version module upgrades, API changes no fixer covers, and new analyzers, while gofmt, go vet, and tests verify every step with Factory.

URL: https://factory.com/articles/go-modernization-gofix
Published: 2026-11-14
Updated: 2026-11-14

Go modernization with go fix became a first-class workflow in Go 1.26. The rewritten `go fix` command now carries dozens of modernizers that rewrite loops, conditionals, and library calls to current idioms, plus an inliner that lets package authors ship their own API migrations. Droids, the coding agents in Factory, handle the work those fixers do not cover, such as major-version module upgrades, API changes with no inline directive, and new analyzers for patterns specific to a codebase.

Factory has no built-in Go mode. Droids run the Go toolchain the way an engineer would, and Factory's hooks, skills, Droid Exec, and Missions keep `gofmt`, `go vet`, `go fix`, and the tests in every step.

## What go fix does since Go 1.26

The [Go 1.26 release notes](https://go.dev/doc/go1.26) describe `go fix` as completely revamped and now the home of Go's modernizers. It builds on the same analysis framework as `go vet`, so a diagnostic in one can become a fix in the other. The historical fixers, all obsolete, were removed.

Alan Donovan's [post on the new go fix](https://go.dev/blog/gofix) covers the operating details that matter for a migration. `go fix ./...` silently rewrites files. `go fix -diff ./...` previews the change. `go tool fix help` lists the registered analyzers, and each one has a flag, so `-minmax` runs a single fixer and `-any=false` excludes one. Fixes that touch generated files are discarded, because the right change is to the generator. Each run sees one build configuration, so code with platform-specific files needs separate runs with different `GOOS` and `GOARCH` values.

The suite keeps moving. Go 1.27, released in August 2026, added the `atomictypes`, `embedlit`, `slicesbackward`, and `unsafefuncs` modernizers, removed `fmtappendf`, and renamed `waitgroup` to `waitgroupgo`. Teams that script specific analyzer flags should pin the toolchain version in `go.mod` so the fixer set does not change underneath them.

Modernizers also respect the effective Go version of each file, read from the `go` directive and build tags. A module still declaring `go 1.21` gets no fixes that need a later release.

Older tools still have a place. `gofmt -r 'pattern -> replacement'` applies expression rewrites in which single lowercase letters act as wildcards. The `eg` command from `golang.org/x/tools` performs type-aware, example-based refactoring from a template that contains a `before` and an `after` function. `go vet` and staticcheck report problems that no fixer resolves on its own.

## Commands for the deterministic Go pass

These commands use the standard toolchain plus staticcheck, installed with `go install honnef.co/go/tools/cmd/staticcheck@latest`. Run them from the module root on a clean Git tree so the diff contains only tool output.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Raise the language version.</strong> Updates the go directive so modernizers can use features from that release.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Preview.</strong> Prints the modernizer changes as a diff without writing files.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Apply.</strong> Rewrites every package under the current directory, skipping generated files.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Gate.</strong> Static checks plus the full test suite with the race detector.</span>
  </div>
</div>

Commit the output of each step separately. Because the tool is deterministic, a reviewer can rerun `go fix` on the parent commit and compare. When the base branch moves, the fix commit can be regenerated rather than rebased by hand.

## Where Go modernization with go fix stops

Modernizers update idioms within the language and standard library. Most migration effort sits elsewhere.

Dependency upgrades come first. `go get -u ./...` raises dependencies to their latest minor and patch versions, but a new major version is a different module path, such as `/v2`. Moving to it means changing imports and adapting to whatever API the maintainers changed. Deprecated functions in third-party packages rarely carry an inline directive yet, so their callers need manual edits.

Behavior is the second boundary. The Go team excluded a modernizer that rewrote `append([]string{}, s...)` to `slices.Clone(s)` from the `go fix` suite after finding that `Clone` returns nil for an empty slice, a difference that can break callers in rare cases. An agent writing the same rewrite by hand would face the same trap. Every change, mechanical or not, goes through the build and the tests.

Generated code is the third. Because `go fix` discards edits to generated files, code produced by `stringer`, protocol buffer plugins, or mock generators keeps its old idioms until the generator changes. The Droid updates the generator version or its templates, reruns `go generate`, and commits the regenerated output as its own tool commit. Hand edits to generated files would disappear on the next generation run.

This residue is the Droid's work. It reads compiler errors and test failures after each tool commit, updates import paths and call sites for new major versions, and writes the reasoning for each API choice into the pull request. Where a package has thin coverage, it adds table-driven tests against the old code before upgrading, so behavior changes show up as failing cases.

## How Factory runs the go fix loop

Factory is a platform for running a software factory, where Droids take work from a signal to a reviewed pull request. For Go, the loop keeps the toolchain in charge of every change it can make.

Toolchain pass</span><br/><span style='color:#8A8380 !important'>go get, go mod tidy, go fix</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Build and test gate</span><br/><span style='color:#8A8380 !important'>go vet, staticcheck, go test -race</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Gate passes?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Droid handles residue</span><br/><span style='color:#8A8380 !important'>major versions, API changes, new analyzers</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Deterministic re-run</span><br/><span style='color:#8A8380 !important'>empty go fix -diff, gofmt -l, full tests</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Human review</span><br/><span style='color:#8A8380 !important'>tool commits and agent commits</span>"]
    A --> B --> C
    C -- no --> D --> E
    C -- yes --> E
    E --> F
`}
/>

**Formatting and vet on every edit.** Factory [hooks](https://docs.factory.com/harness/hooks) run shell commands at lifecycle events from `.factory/hooks.json`. A `PostToolUse` hook on `Edit`, `Create`, and `ApplyPatch` can run this script after each change. It formats the file with `gofmt`, then runs `go vet` on its package and exits with code `2` on failure, which sends the vet output back to Droid as feedback.

```bash title=".factory/hooks/go-check.sh"
#!/usr/bin/env bash
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" != *.go ]]
then
  exit 0
fi
gofmt -w "$file"
if ! out=$(cd "$(dirname "$file")" && go vet . 2>&1)
then
  echo "$out" >&2
  exit 2
fi
```

**Completion checks.** A `Stop` hook can run `go build ./...`, `go test ./...`, and a check that `go fix -diff ./...` prints nothing. If any fails, Droid gets the output and keeps working instead of reporting success.

**The playbook as a skill.** A [skill](https://docs.factory.com/harness/skills) in `.factory/skills/go-upgrade/SKILL.md` lists the order. Raise the toolchain, tidy modules, run `go fix` per build configuration, run the gate, fix residue, and verify, each in its own commit. The exact commands, including any `GOOS` and `GOARCH` matrix and the test flags, belong in [AGENTS.md](https://docs.factory.com/harness/agents-md).

## Turning repeated fixes into deterministic tools

When the Droid finds itself making the same edit across many call sites, it should write a tool instead. Go offers three levels.

For an internal API that a simple wrapper can replace, the Droid adds a `//go:fix inline` directive to the deprecated function. The [source-level inliner post](https://go.dev/blog/inliner) explains how `go fix` then replaces each call with the function body, substituting arguments for parameters.

```go
package oldlog

import "example.com/platform/log"

// Deprecated: use log.Info.
//
//go:fix inline
func Printf(format string, args ...any) {
	log.Info(format, args...)
}
```

Every consumer that upgrades to the version carrying the directive and runs `go fix ./...` gets the migration, including repositories the platform team never touches. For rewrites that depend on types but not on control flow, the Droid writes an `eg` template. For anything more involved, it writes an analyzer on `golang.org/x/tools/go/analysis` that reports a diagnostic with a suggested fix, and tests it with `analysistest.RunWithSuggestedFixes` against golden files, including inputs that must stay unchanged.

Fan-out across modules uses [Droid Exec](https://docs.factory.com/droid-exec/overview), which runs Droid headless. The pattern matches Factory's [lint autofix recipe](https://docs.factory.com/droid-exec/recipes/lint-autofix), where a deterministic tool lists the failing targets, one `droid exec --auto medium` run handles each target in isolation, and the tool re-verifies.

For upgrades across many repositories with shared internal modules that must move first, Factory [Missions](https://docs.factory.com/missions/overview) plan the work as milestones with validation along the way.

## Frequently asked questions

### What changed in go fix in Go 1.26?

Go 1.26, released in February 2026, replaced the old go fix with a new implementation built on the same analysis framework as go vet. It runs a suite of modernizers that rewrite code to use newer language and library features, plus a source-level inliner driven by go:fix inline directives. The obsolete historical fixers were removed.

### Can go fix change program behavior?

The Go team states that the fixers should not change behavior and asks users to report any case where one does. The team has already excluded at least one candidate modernizer from go fix because of a subtle nil-versus-empty difference. A full build and test run after go fix is still the right gate.

### Does Factory have a built-in Go modernization mode?

No. Factory runs the Go toolchain directly. Hooks run gofmt and go vet after edits, skills hold the upgrade playbook, Droid Exec fans work out per module, and Missions coordinate upgrades across many repositories. Droids also write go:fix inline directives, eg templates, and custom analyzers with tests.

### Why does go fix not update code for every new feature?

Modernizers check the effective Go version of each file, taken from the go directive in go.mod and from build tags, and do not insert features newer than that version. Raise the go directive with go get go@version before running go fix to unlock fixes for a newer release.

## Further reading

- [Deterministic code modernization with Factory](https://factory.com/articles/deterministic-code-modernization)
- [How to run a runtime upgrade with coding agents](https://factory.com/articles/coding-agent-runtime-upgrades)
- [C and C++ modernization with clang-tidy](https://factory.com/articles/cpp-modernization-clang-tidy)
- [Java modernization with OpenRewrite and Droids](https://factory.com/articles/java-modernization-openrewrite)
