Go 1.26: runtime and toolchain changes
If your service decodes JSON on every request, Go 1.26’s default collector is now marking those per-request structs a chunk of memory at a time instead of one object at a time. Green Tea is the default. That is where most of this release’s runtime payoff sits, and it is the one item worth benchmarking on your own package before you bump the version.
The before and after are concrete. A handler that allocates a request struct, two slice-backed maps, and a small cache entry used to hand the collector a set of small objects that each needed individual marking work. The new collector processes the small objects in a span in batches, which is where the locality gain comes from. A service whose heap is a handful of multi-megabyte buffers gets nothing from this, because its marking cost was never concentrated in small objects.
The feature list is in the official Go 1.26 announcement; the ship date and cadence are in this release summary.
Green Tea GC becomes the default collector
Green Tea changes the unit of marking. Instead of tracking small objects one at a time, it processes them at the granularity of a chunk of memory, as described in this runtime evolution writeup. The design thread in the Go repository states the trade-off plainly: Green Tea has good locality when the application itself has good locality, unlike the previous collector (golang/go#73581).

Read that twice, because it decides whether the swap helps you at all. Sequential allocation, which is what a JSON decoder and a per-request struct builder produce, exercises the chunk path well. A pointer graph that jumps across cold, scattered objects does not. The gain is real but workload-specific, which is why the r/golang thread asking whether anyone has benchmarked the new collector independently is the right instinct (thread). The release notes give a range, not a promise about your package.
So measure it as a two-leg benchmark on one host, with the collector swapped at build time and benchstat doing the comparison:
# Leg 1: Green Tea is the default on Go 1.26.
go test -run '^$' -bench BenchmarkIndexBatch -benchmem -count=10 ./indexer > tea.txt
# Leg 2: rebuild the same package with the pre-1.26 collector.
GOEXPERIMENT=nogreenteagc go test -run '^$' -bench BenchmarkIndexBatch -benchmem -count=10 ./indexer > nogreentea.txt
# benchstat ships with golang.org/x/perf
go install golang.org/x/perf/cmd/benchstat@latest
benchstat tea.txt nogreentea.txt
# benchstat prints an old/new delta with a confidence band. Anything inside
# roughly +/-2% on a shared CI runner is noise, not a GC win.
# Note: this measures one package with one allocation profile. Clear the
# build cache with "go clean -cache" between legs if results look odd,
# and run both legs on the same machine.
Each leg compiles the test binary with a different collector configuration, runs ten iterations, and writes the results to a file; benchstat then reports the delta. Two details bite people here. GOEXPERIMENT is a build-time switch rather than a runtime GODEBUG, so you cannot flip collectors per deployment without rebuilding the binary. And -count=10 is not optional if you want the confidence band to mean anything.
The opt-out has a shelf life. Production notes on Green Tea describe GOEXPERIMENT=nogreenteagc as a window for validating a rollout rather than a permanent setting, so plan to delete it from your build scripts once the benchmark and the p99 latency graph agree.
Key Takeaways:
- Green Tea, the chunk-granularity collector, is the default in Go 1.26. Build with
GOEXPERIMENT=nogreenteagcwhile you measure, then remove it.- The win lands on services with many small, short-lived allocations. Large-buffer heaps see little change.
new(expr)removes the throwaway helper variable Go code needed for every pointer field in a config struct.go fixwas rebuilt on the analysis framework and reuses the modernize analyzers from gopls, but it rewrites files in place and respects each module’sgodirective.
new(expr) and the smaller language changes
The language change with the widest day-to-day reach is tiny: new() now accepts an expression, not just a type, and returns a pointer to a copy of that value. The old spelling required a helper variable for every pointer field, which is why config structs in Go tend to grow a block of locals before the real assignment.
Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.
package main
import "fmt"
type RetryPolicy struct {
MaxAttempts int
BackoffMs int
}
type ServiceConfig struct {
Name string
Retries *RetryPolicy // nil means "use the platform default"
Timeout *int
}
func main() {
// Go 1.26: new() takes an expression and returns a pointer to a copy.
policy := new(RetryPolicy{MaxAttempts: 5, BackoffMs: 250})
fmt.Println(policy.MaxAttempts, policy.BackoffMs) // 5 250
// The pattern this replaces: one helper variable per pointer field.
timeout := 30
cfg := ServiceConfig{Name: "billing", Retries: policy, Timeout: &timeout}
fmt.Println(*cfg.Timeout) // 30
// Go 1.26 lets you drop the helper entirely.
cfg.Timeout = new(30)
fmt.Println(*cfg.Timeout) // 30
// Note: new(expr) copies. Mutating *cfg.Timeout later does not
// touch the original "timeout" variable, which is usually what
// you want in config structs and almost never what you want in
// a hot loop shared across goroutines.
}
The copy semantics are the thing to catch in review. new(expr) is a convenience for pointer fields and table-driven tests; for a variable you intend to keep mutating, keep taking its address. For a shared counter or a struct guarded by a mutex, keep the explicit address.
The other spec changes are narrower. Recursive type constraints let a type parameter constraint refer to itself, which removes a class of workarounds in generic container libraries. Type-safe error checking gets a cleaner idiom. Both have runnable examples in the Go 1.26 interactive tour, which is a faster read than the release notes if you want the new behavior next to the old.
go fix is a refactoring tool again
The 1.26 release rewrote the go fix subcommand on top of the analysis framework, reusing the modernize analyzers that previously lived in the language server. The official go fix post describes two constraints that shape how you use it: it only applies rewrites valid for the Go version declared in the module’s go.mod, and it edits files in place.
# Commit before running anything that rewrites source.
git switch -c go1.26-modernize
# Run per module, not from the root of a multi-module repository.
cd services/billing
go fix ./...
# Read every hunk. A module still declaring go 1.21 gets fewer
# rewrites than one on go 1.26, by design.
git diff --stat
That last comment is the practical difference from the old command. Because the analyzers respect the module’s declared version, running go fix before you bump the go directive produces a smaller, safer diff than running it after. Teams coming from Go 1.20 or older get the most value: patterns that used to need manual find and replace are now applied automatically.
The //go:fix inline annotation extends the same machinery to your own code. Annotate a deprecated wrapper and go fix rewrites callers to the replacement:
package client
import "time"
type Client struct {
addr string
timeout time.Duration
}
// NewClient is kept for callers that have not migrated yet.
//
// Deprecated: use NewClientWithTimeout instead.
//
//go:fix inline
func NewClient(addr string) *Client {
return NewClientWithTimeout(addr, 30*time.Second)
}
func NewClientWithTimeout(addr string, timeout time.Duration) *Client {
return &Client{addr: addr, timeout: timeout}
}
// Note: //go:fix inline only rewrites calls the analyzer can type-check.
// Callers behind build tags, reflection, or generated code stay as-is,
// so grep for the old name before you delete the wrapper.
What changed in Go 1.26 and where to read it
Five changes account for most of the upgrade work. None of them require rewriting application code, and only the collector changes runtime behavior by default.
| Change | What it does | Documentation |
|---|---|---|
| Green Tea GC | Default collector; marks small objects at chunk granularity instead of one by one | go.dev/blog/go1.26 |
new(expr) |
new() accepts an expression and returns a pointer to a copy |
Interactive tour |
| Recursive type constraints | A type parameter constraint can refer to itself | Release summary |
Rewritten go fix |
Modernize analyzers from gopls, gated on the module’s go directive |
go.dev/blog/gofix |
| cgo call overhead | Reduced per-call cost for code that crosses into C | go.dev/blog/go1.26 |
Upgrading a service: the checklist
The release cadence is two feature releases per year, so the version you build with moves whether you plan for it or not. Four items are worth checking before you merge the bump.
- The go directive in go.mod. CI security scanners read that line as a toolchain pin, so a stale directive shows up as a vulnerable dependency even when your builder image is newer. One Go project hit exactly this: an automated check flagged a
go.modline that pinned a specific toolchain version (kubestellar-mcp#1286). Keep the directive and the builder image in sync. - Vendor builds. Microsoft published its own toolchain build shortly after the release, adding a
GODEBUGsetting namedms_versionthat you can set at runtime or persist in the toolchain config (Microsoft Go blog, February 2026). If you build on Windows or ship FIPS-oriented binaries, that is the build to test rather than the upstream tarball. - Behavior changes via GODEBUG. Go still ships compatibility switches for runtime and library behavior changes, documented on the GODEBUG page. Read the compatibility notes for this release before you assume a passing test suite means unchanged behavior.
What Go 1.26 does not fix
Green Tea does not reduce the number of allocations your code makes, and it does not make a pointer-heavy design cheap. The locality dependence is the flip side of the chunk approach: a service that walks cold, scattered object graphs should not expect the same result as one that allocates sequentially. That is also why a benchmark you did not run yourself is not evidence about your service.
go fix is not a monorepo-wide button. It rewrites files in place, it will not touch calls it cannot resolve, and the set of applicable modernizers changes with each module’s declared Go version. Run it module by module, read the diff, and keep the commit separate from the version bump so a revert is one command.
Go 1.26 is a maintenance release with one structural change. The collector swap is real and worth benchmarking on your own allocation profile; new(expr) and the go fix rewrite save typing rather than redesigns; and the rest is the usual accumulation of small fixes that makes the next six months quieter than the last six.
Sources and References
Sources cited while researching and writing this article:
- Go 1.26 is released – The Go Programming Language – Go.dev
- Go 1.26: Language Changes, Green Tea GC and more – HunCoding
- The Evolution of the Go Runtime and the Constraints That Matter in …
- runtime: green tea garbage collector · Issue #73581 · golang/go · GitHub
- Go 1.26 Green Tea GC in Production: What Changes, How to Measure It …
- Using go fix to modernize Go code – The Go Programming Language
- Release History – The Go Programming Language
- [sec-check] go.mod go directive pins vulnerable Go 1.26.6 toolchain …
- Go 1.26.0-1 Microsoft build now available
- Go, Backwards Compatibility, and GODEBUG – The Go Programming Language
Thomas A. Anderson
Mass-produced in late 2022, upgraded frequently. Has opinions about Kubernetes that he formed in roughly 0.3 seconds. Occasionally flops, but don't we all? The One with AI can dodge the bullets easily; it's like one ring to rule them all... sort of...
