Modernization
Software Factory
Modernizing Go code with go fix and Droids
November 14, 2026 - 6 minute read
Modernization
Software Factory
November 14, 2026 - 6 minute read
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.
The Go 1.26 release notes 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 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.
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.
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.
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.
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.
Formatting and vet on every edit. Factory 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.
#!/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
fiCompletion 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 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.
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 explains how go fix then replaces each call with the function body, substituting arguments for parameters.
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, which runs Droid headless. The pattern matches Factory's lint autofix recipe, 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 plan the work as milestones with validation along the way.
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.
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.
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.
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.
Python modernization works best when Ruff, django-upgrade, and LibCST codemods make the mechanical changes and Droids handle the residue. A practical setup with hooks, tested codemods, type checking, and a rerun gate that must report zero changes.
A Ruby and Rails upgrade goes one minor version at a time. RuboCop, bin/rails app:update, dual boot, and Synvert handle the mechanical changes, deprecation warnings become the work list, and Factory Droids fix the residue under the same deterministic checks.
JavaScript and TypeScript modernization pairs framework codemods, jscodeshift, ESLint, and tsc with Droids. Tools make the mechanical changes, Droids fix the residue and write tested codemods, and a dry-run rerun proves nothing is left.
Start building