A New Mandate for Modernization
For years, `go fix` has been part of the Go developer's toolkit, albeit often in the background. Its new iteration, however, leverages a sophisticated suite of algorithms designed to identify opportunities for code improvement proactively. This involves adopting modern Go features, simplifying expressions, and generally aligning older code with the language's evolution. If you're managing a large Go project, or even contributing to open source, you'll want to pay close attention to this. Here's the thing: while language changes are usually exciting, the reality of maintaining existing code can be daunting. The Go team isn't just building new features; they're now building tools to help developers get there. This post will walk us through how to deploy `go fix` to bring your Go codebase into the modern era. We'll then glance at the sophisticated infrastructure that powers it and, finally, touch on the emerging theme of "self-service" analysis tools, empowering module maintainers and organizations to bake their own best practices into automated tooling.Running and Refining Your Fixes
Using the new `go fix` command isn't a radical departure from familiar Go tools like `go build` or `go vet`; it accepts package patterns. To update all packages within your current directory, you'd simply run: ``` $ go fix ./... ``` By default, the command silently updates your source files. One crucial detail: it intelligently bypasses any fixes that would touch generated files, understanding that the appropriate change belongs in the generator's logic itself. A word of advice, especially if you're dealing with a large codebase: always start from a clean Git state. This ensures that `go fix`'s edits are isolated, making code reviews far more manageable. The team recommends running `go fix` every time you update your build to a newer Go toolchain release. Want to see what changes `go fix` would make before applying them? The `-diff` flag is your friend: ``` $ go fix -diff ./... --- dir/file.go (old) +++ dir/file.go (new) - eq := strings.IndexByte(pair, '=') - result[pair[:eq]] = pair[1+eq:] + before, after, _ := strings.Cut(pair, "=") + result[before] = after … ``` As you can see from that snippet, a common transformation is replacing older `strings.IndexByte` and manual slicing with the more concise Go 1.18 `strings.Cut`. To explore the available fixes, you can list the registered analyzers: ``` $ go tool fix help … Registered analyzers: any replace interface{} with any buildtag check //go:build and // +build directives fmtappendf replace []byte(fmt.Sprintf) with fmt.Appendf forvar remove redundant re-declaration of loop variables hostport check format of addresses passed to net.Dial inline apply fixes based on 'go:fix inline' comment directives mapsloop replace explicit loops over maps with calls to maps package minmax replace if/else statements with calls to min or max … ``` Each analyzer comes with its own documentation. For example, to understand `forvar`: ``` $ go tool fix help forvar forvar: remove redundant re-declaration of loop variables The forvar analyzer removes unnecessary shadowing of loop variables. Before Go 1.22, it was common to write `for _, x := range s { x := x ... }` to create a fresh variable for each iteration. Go 1.22 changed the semantics of `for` loops, making this pattern redundant. This analyzer removes the unnecessary `x := x` statement. This fix only applies to `range` loops. ``` By default, `go fix` runs all these analyzers. For massive projects, you might find it easier to manage code reviews by applying fixes from the most prolific analyzers in separate commits. You can enable specific analyzers using flags (e.g., `-any`) or even exclude them (`-any=false`). Finally, if your project heavily uses build tags for different CPUs or platforms, remember to run `go fix` multiple times with varying `GOARCH` and `GOOS` values for comprehensive coverage: ``` $ GOOS=linux GOARCH=amd64 go fix ./... $ GOOS=darwin GOARCH=arm64 go fix ./... $ GOOS=windows GOARCH=amd64 go fix ./... ``` Running it more than once isn't just about coverage; it can also uncover "synergistic fixes" — where one change opens the door for another, which we'll discuss shortly.The Mandate to Modernize
The introduction of generics in Go 1.18 signaled a shift. The era of minimal language spec changes concluded, making way for a period of more rapid, though still meticulously considered, evolution, particularly within the standard libraries. Many common Go patterns, like gathering map keys into a slice, can now be elegantly handled by generic functions like `maps.Keys`. These new features translate directly into opportunities for simplifying existing code, and that's where the modernizers come in. But there's an even more compelling reason behind this push, hinted at in the fragment: the rise of large language model (LLM) coding assistants. As early as December 2024, it became clear that these tools, unsurprisingly, tended to generate Go code mirroring the vast corpus of older Go code they were trained on. This meant they'd often produce code in an older style, even when explicitly instructed to use the latest idioms of, say, Go 1.25. Some models even outright denied the existence of features. (You can find more frustrating details in Alan Donovan's 2025 GopherCon talk.) The solution? We need to ensure that the open-source Go code — the very training data for these models — reflects the latest and greatest idioms. Over the past year, the team has been busy building dozens of analyzers specifically designed for modernization. Here are a few examples of the fixes they suggest: * **minmax**: This one replaces `if` statements with Go 1.21's `min` or `max` functions:
x := f()
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
x := min(max(f(), 0), 100)
for i := 0; i < n; i++ {
f()
}
for range n {
f()
}
i := strings.Index(s, ":")
if i >= 0 {
return s[:i]
}
before, _, ok := strings.Cut(s, ":")
if ok {
return before
}
A Deeper Dive: Go 1.26's new(expr) and its Modernizer
Go 1.26 introduces a small but widely impactful change to the language: the built-in `new` function can now take any value as an argument, initializing a new variable to that value and returning its address. Historically, `new` was restricted to accepting only a type, initializing the variable to its "zero" value. This means `ptr := new(string); *ptr = "go1.25"` can now simply become `ptr := new("go1.26")`.ptr := new(string) *ptr = "go1.25"
ptr := new("go1.26")
Synergies and the Challenge of Conflicts
One of the more powerful aspects of this new `go fix` is the concept of synergistic fixes. Applying one modernization can often create entirely new opportunities for another. Take the `minmax` example:
x := f()
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
x := min(max(f(), 0), 100)
s := ""
for _, b := range bytes {
s += fmt.Sprintf("%02x", b)
}
use(s)
var s strings.Builder
for _, b := range bytes {
s.WriteString(fmt.Sprintf("%02x", b))
}
use(s.String())
Navigating Fix Conflicts
A single `go fix` run can apply numerous changes within one source file. Each fix is treated as conceptually independent, much like individual Git commits. `go fix` employs a straightforward three-way merge algorithm to reconcile these changes. If a proposed fix syntactically conflicts with changes already accumulated, it's simply discarded, and you'll receive a warning that some fixes were skipped. This is your cue to re-run the command. However, syntactic conflicts aren't the only concern. A more subtle challenge arises with *semantic* conflicts. These occur when two changes are textually distinct but their underlying meaning clashes. Imagine two different fixes, both removing the second-to-last use of a local variable. Each fix is fine on its own, but together, they render the variable entirely unused, which is a compilation error in Go. Neither fix is responsible for deleting the variable declaration itself; that responsibility falls to the developer using `go fix`. Fortunately, `go fix` automatically handles one common semantic conflict: it detects and removes unused imports in a final pass. While semantic conflicts are relatively uncommon, they typically surface as compilation errors, making them hard to miss. The downside, of course, is that they demand some manual cleanup after `go fix` has done its work. With that understanding of the practical application and implications, let's now turn our attention to the underlying architecture: the Go analysis framework.The Go static analysis story has always been compelling, but with the 1.26 release, it's hitting a new stride. The big news? The `go fix` command, long a bit of a relic, has finally caught up to its sibling, `go vet`. If you're building Go projects, this means a significantly more powerful, integrated, and *actionable* static analysis experience is now standard. That's the immediate takeaway, but the implications run deeper. We're seeing the full maturity of Go's analysis framework, which powers everything from developer tooling to Google's internal systems, and a clear vision for a future where developers can more easily extend these capabilities themselves.Bringing `go fix` into the Modern Era
For years, `go fix` felt a bit stuck in the past, a holdover from Go's early, rapidly evolving days. It predated the Go compatibility promise and served mainly to help early adopters keep their code aligned with a changing language. Meanwhile, `go vet` evolved, getting smarter and more integrated. Go 1.26 changes all that. `go fix` now runs on the same robust analysis framework as `go vet`, effectively converging their implementations. The distinction between them is subtle but important: `go vet` is about *identifying* potential mistakes with low false positives and reporting them to you. `go fix`, on the other hand, is about *applying* changes directly. Its core mandate is to generate fixes that are demonstrably safe, ensuring no regressions in correctness, performance, or even coding style. The diagnostics from `go fix` might not even be shown; the fixes are just applied. This makes developing a fixer quite similar to developing a checker, just with a different end goal. This unified framework is what underpins a wide array of tools that Go developers interact with daily: * **`unitchecker`**: This is the engine that transforms analyzer suites into subcommands runnable by `go build`'s scalable system, a direct analog to a compiler. It's the very foundation of both `go fix` and `go vet` themselves. Learn more about unitchecker. * **`nogo`**: For those in environments using alternative build systems like Bazel and Blaze, `nogo` offers a similar driving mechanism. Check out nogo. * **`singlechecker` and `multichecker`**: These allow you to run individual analyzers or suites as standalone commands, useful for ad hoc experiments, or for measurements across the vast `proxy.golang.org` corpus. singlechecker | multichecker | proxy.golang.org * **`gopls`**: The Go language server that powers IDEs like VS Code provides real-time diagnostics generated by these analyzers with every keystroke. It's also where the concept of a `SuggestedFix` first landed in 2019, forming the basis for many quick fixes and refactoring features we now take for granted. gopls | Language Server Protocol | SuggestedFix * **`staticcheck`**: A well-known third-party tool that not only offers a configurable driver but also a massive suite of analyzers that can integrate with other drivers. Staticcheck website. * **`Tricorder`**: Google's own batch static analysis pipeline for its massive monorepo, deeply integrated with its code review system. Read about Tricorder. * **`gopls`' `MCP server`**: A crucial component making these diagnostics available to LLM-based coding agents, effectively providing “guardrails” for AI-assisted development. MCP server details. * **`analysistest`**: The dedicated test harness for the entire analysis framework. analysistest.Under the Hood: The Power of Context and Inter-Package Deductions
What makes this framework so effective isn't just the sheer number of tools built on it, but its sophisticated internal mechanisms. One significant benefit is the ability to define *helper analyzers*. These don't report problems or suggest fixes themselves. Instead, they compute intermediate data structures, like control-flow graphs or the SSA representation of functions, or support optimized AST navigation. Other analyzers can then tap into these pre-computed structures, amortizing the cost of their construction. Even more powerful is the framework's support for "facts"—deductions that cross package boundaries. An analyzer can attach a "fact" to a function or symbol. This means information learned during the analysis of a function's body can be reused when that function is called elsewhere, even in a different package or a separate process. This inductive process makes scalable interprocedural analysis surprisingly easy. Take the `printf` checker, for example: it can recognize that `log.Printf` is simply a wrapper around `fmt.Printf`, and then apply the same printf-style checks to calls to `log.Printf`, and even to further wrappers around `log.Printf`. Uber's `nilaway`, a tool for detecting potential nil pointer dereferences, relies heavily on this "facts" mechanism. nilaway on GitHub. This "separate analysis" within `go fix` mirrors the "separate compilation" model of `go build`. Just as the compiler processes packages from the bottom of the dependency graph upwards, passing type information, the analysis framework passes facts (and types) up the dependency chain.Infrastructure Improvements and the Road Ahead for Authors
As the number of analyzers continues its inevitable climb, the Go team has been investing heavily in the underlying infrastructure, aiming for both performance gains and a smoother experience for analyzer authors. For instance, most analyzers begin by traversing syntax trees. The inspector package already makes this efficient by pre-computing indexes. Recently, it gained the Cursor datatype, allowing flexible, efficient navigation between nodes—think HTML DOM traversal, but for Go's AST. This makes complex queries, like "find every `go` statement that's the first statement in a loop body," both concise and fast. Similarly, searching for calls to specific functions, like `fmt.Printf`, can be costly since function calls are so numerous. Rather than brute-force searching every expression, the `typeindex` package and its helper analyzer pre-compute an index of symbol references. This means enumerating calls to `fmt.Printf` becomes proportional to the number of calls, not the size of the package. For niche symbols, like `net.Dial` sought by the hostport analyzer, this can translate into a dramatic 1,000x speedup. Read about the 1,000x speedup. Other recent infrastructural improvements include: * A **dependency graph for the standard library**, helping analyzers avoid introducing pesky import cycles. * Support for **querying the effective Go version** of a file, ensuring analyzers don't suggest features "too new" for the target `go.mod` or build tags. * A richer **library of refactoring primitives** that correctly handle comments and other complex edge cases during code modification. That said, fixing code programmatically is inherently tricky. The expectation is that users will apply hundreds of suggested fixes with minimal review, so correctness, even in obscure edge cases, is paramount. We've seen this firsthand; a modernizer to replace `append([]string{}, slice...)` with the clearer `slices.Clone(slice)` had to be excluded from `go fix` because `slices.Clone` returns `nil` when the input slice is empty—a subtle behavior change that could introduce bugs. More on this particular modernizer. Addressing these difficulties for analyzer authors is high on the roadmap. Better documentation, perhaps with checklists of common edge cases, would be a huge boon. A pattern-matching engine for syntax trees, drawing inspiration from tools like staticcheck's or Tree Sitter, could streamline the task of identifying code needing fixes. A richer set of operators for accurate fix generation and a more robust test harness to validate fixes are also in the pipeline.The "Self-Service" Paradigm: Decentralizing Modernization
Looking further out, the Go team is setting its sights on a "self-service" paradigm for 2026, and this is where things get really interesting for the broader community. The current model, with its bespoke algorithms for language and standard library features, works well for core Go. But it doesn't scale to the vast and rapidly growing ecosystem of third-party packages. If you write a modernizer for your own API, there's no automatic way to ensure your users adopt it. Getting it into `gopls` or the `go vet` suite is a long shot unless your API is ubiquitous, requiring code reviews, approvals, and release cycles. This centralized bottleneck simply can't keep pace with the Go community's growth. The "self-service" paradigm aims to solve this by empowering Go programmers to define and distribute modernizations for their *own* APIs, allowing users to apply them without needing the Go team's direct involvement. Go 1.26 offers a glimpse of this future with a preview of the **annotation-driven source-level inliner**, detailed in a follow-up post. The plans for the coming year under this paradigm include two key approaches: 1. **Dynamically loading modernizers from the source tree**: Imagine a package providing a SQL database API that could also ship a checker for common misuses, such as SQL injection vulnerabilities or neglected error handling. These modernizers could be securely executed in `gopls` or `go fix`. Project maintainers could also use this mechanism to encode internal housekeeping rules, enforcing specific coding disciplines or flagging problematic function calls. See the issue for dynamically loading. 2. **Generalizing control-flow checkers**: Many common checks boil down to patterns like "don't forget to X after you Y!" (e.g., closing a file after opening it, canceling a context, unlocking a mutex). The goal is to explore unified, generalized versions of these checkers. This would allow Go programmers to apply such invariants to new domains simply by annotating their code, abstracting away complex analytical logic. Ultimately, these developments are about making Go maintenance less arduous and helping developers embrace newer language features and libraries more smoothly. Try `go fix` on your projects in Go 1.26 and see what it does for you. And if you have ideas for new modernizers, fixers, checkers, or broader self-service approaches to static analysis, the team is listening. Report problems or share ideas here.
Next article: Allocating on the Stack
Previous article: Go 1.26 is released
Blog Index