Go 1.26: Green Tea GC by Default, and How //go:fix inline Closes the Deprecation Gap
Source: go
The two most consequential changes in Go 1.26 both belong to the same category: infrastructure improvements that deliver results without requiring application code changes. Green Tea GC becomes the default garbage collector, graduating from the experimental opt-in it was in Go 1.25. The go fix tool gets rebuilt on the same go/analysis framework that powers go vet, along with a new source directive that lets library authors embed machine-readable migration instructions directly in their code.
Object-Level vs Page-Level: The Architecture Behind Green Tea GC
Go’s garbage collector has been a concurrent, tri-color mark-and-sweep design since its major rewrite around Go 1.5. It traces from roots (globals, goroutine stacks), marks reachable objects, and sweeps unreachable ones back into free lists. The fundamental unit of that design is the individual heap object: the work list driving GC marking is a stack of object addresses, which means the collector is doing random-access traversal of the heap.
Random memory access is expensive on modern hardware. CPU caches are small relative to typical Go heaps. When the GC follows a pointer to an object, that object is frequently in a cache line that hasn’t been recently accessed. These cache misses accumulate. The Go team measured that around 35% of GC marking CPU time in the traditional design was spent waiting on main memory due to cache miss stalls.
Green Tea GC shifts the unit of tracking from individual objects to heap pages. Instead of a work list of object addresses, it maintains a FIFO queue of pages. When the GC processes a page, multiple objects on that page have likely already been marked “seen,” and the scan proceeds sequentially through contiguous memory. Sequential, cache-local access is what CPUs are optimized for, and the difference in throughput is measurable.
The per-object metadata in Green Tea uses two bits per slot: a “seen” bit marking reachability from roots, and a “scanned” bit indicating all pointers in the object have been processed. These per-page bitmaps are small and cache-friendly structures, which is the entire point of the redesign.
On machines with AVX-512 support, Green Tea takes this further by using the VGF2P8AFFINEQB instruction to process 64 bytes of bitmap at a time. Bulk SIMD operations on bitmaps only make sense with a page-oriented data layout; per-object tracking doesn’t lend itself to this kind of processing. The AVX-512 path adds roughly 10% additional performance on capable hardware, on top of the cache locality improvements.
Performance Numbers and Workload Variability
Green Tea’s GC CPU time reduction is workload-dependent, and the Go team has been consistent about this throughout the experimental period. For typical workloads, the reduction is around 10%. For allocation-heavy workloads with many short-lived small objects, it can reach 40%.
The practical impact depends on what fraction of your program’s CPU time is GC. A service spending 5% of cycles on garbage collection sees different absolute gains from a 40% GC reduction than one spending 25%. Services doing heavy small-object allocation, such as Discord bots handling concurrent message events or HTTP servers processing short-lived request contexts, tend toward the higher end of that range.
Multi-core programs benefit additionally from reduced cache thrashing. When fewer goroutines are competing for cache lines during GC marking, the program’s non-GC work also runs faster. Throughput improvements often exceed what the raw GC time reduction would predict in isolation.
The Rollout: Production Validation Before Default
Green Tea GC was available in Go 1.25 behind the GOEXPERIMENT=greenteagc flag. Before Go 1.26 shipped, Google was already running it in production internally. Making it the default in 1.26 was a decision backed by real-world data, not just synthetic benchmark results.
The Go team could have made Green Tea the default in 1.25 when the implementation was stable enough to ship as an experiment. They chose not to, collecting production feedback through the 1.25 lifecycle. Not all workloads benefit equally, and the team wanted to understand the distribution before changing what all Go programs get automatically.
For programs upgrading to Go 1.26, the improvement is automatic. The GOEXPERIMENT flag is gone; Green Tea is what you get. Running your benchmarks before and after the upgrade is still worthwhile, since the actual improvement varies by workload and the numbers matter for capacity planning.
go fix Gets a Real Foundation
The go fix tool predates much of Go’s current toolchain infrastructure. Earlier versions applied hardcoded source transformations for API changes between Go versions. This was adequate for Go’s first decade but didn’t scale well as the stdlib evolved and as third-party packages accumulated their own deprecated APIs.
Go 1.26 rebuilds go fix on the go/analysis framework. The difference is significant: go/analysis provides type-aware program analysis, not textual pattern matching. The tool now understands what a symbol actually refers to, not just what it looks like in source. This is the same infrastructure go vet has used for years; bringing go fix up to the same level opens up more reliable and composable transformations.
Basic usage is unchanged:
# Apply all available fixes
go fix ./...
# Preview changes before applying
go fix -diff ./...
The recommendation from the Go team is to run go fix every time you bump your toolchain version, starting from a clean git state so code review can distinguish automated modernization from manual edits.
//go:fix inline and the Deprecation Contract
The more interesting addition is the //go:fix inline directive. Library authors can now embed machine-readable migration instructions alongside deprecation notices:
// Deprecated: As of Go 1.16, this function simply calls [os.ReadFile].
//go:fix inline
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
Running go fix ./... on a codebase that calls ioutil.ReadFile replaces the call with os.ReadFile, updates the import statement, and removes the now-unused ioutil import. The directive applies to functions, type aliases, and constants that reference another named constant.
Before this, the deprecation contract in Go was entirely informal. You wrote a // Deprecated: comment, updated the documentation, maybe added a comment pointing to the replacement, and waited for users to migrate. Many didn’t. The tooling had no mechanism to help. With //go:fix inline in place, a batch migration becomes one command. The io/ioutil package, deprecated since Go 1.16 and still present in enormous amounts of Go code six years later, is the obvious beneficiary.
For gopls users, the effect is more granular: the language server shows diagnostics at every call site for deprecated functions that carry the directive, and offers a one-click refactoring to apply the inline. You don’t need to run a batch go fix to get the migration; it surfaces as you work.
Why Automated Inlining Is Harder Than It Sounds
The //go:fix inline implementation required roughly 7,000 lines of new code despite the concept being straightforward. Naive textual substitution breaks in several ways, and each requires explicit handling.
Multi-use parameters are one problem. If a deprecated function uses a parameter twice in its body, inlining a call where that argument has side effects would evaluate the side effect twice. The inliner detects this and introduces a binding before substituting:
// Before inlining:
result := deprecated(expensiveExpr)
// After (simplified):
var _arg = expensiveExpr
result := /* body using _arg */
Name shadowing is another. The callee’s local variable names might shadow identifiers in the caller’s scope once the body is inlined. The inliner wraps the body in a block and renames references as needed, adding import statements when required.
The defer statement changes control flow semantics and must be wrapped in an immediately-invoked function literal when present in the inlined body. Batch go fix refuses to emit this form because it’s a visible structural change; the gopls interactive refactoring will apply it after user review.
Constant expression bounds present a subtler issue. If the deprecated function’s body indexes a string parameter and the caller passes a constant empty string, the inlined expression ""[0] becomes a compile-time error where the original code would have been a runtime panic. A constraint solver in the inliner detects this and refuses the substitution.
Each of these categories requires analysis that goes well beyond textual search-and-replace, which is why the feature required the type-aware go/analysis foundation before it could be implemented reliably.
The Compounding Effect
Go 1.26 is a release where infrastructure investment pays off. Green Tea GC is the result of years of work on GC architecture, validated in production at Google’s scale before being made the default. The rebuilt go fix is the result of having a type-aware analysis framework that can underpin reliable automated tooling. The //go:fix inline directive closes the gap between deprecation notices and actual migrations, a problem that has been accumulating in the Go ecosystem for years as packages evolve and old APIs linger.
For existing programs, the upgrade path is straightforward. The GC improvement is automatic. The go fix improvements are available when you run the tool, and will become more valuable as library authors start shipping //go:fix inline alongside their deprecations. The improvement is invisible by design, which is the right design for runtime and toolchain infrastructure.