Go 1.26 landed on February 10th, and it's a big one. The Go team are calling it the largest release they've ever shipped, and having spent the morning poking through the release notes and upgrading a few of my own projects, I'm inclined to agree. This isn't a release full of flashy syntax changes — it's a release that makes Go programs faster, Go tooling sharper, and Go developers' lives a bit easier.
Here's what caught my eye.
The new(expr) Built-in Enhancement
This is the headline language change, and it's deceptively simple. Previously, new only accepted a type: new(int) gave you a *int pointing to a zero value. Now new accepts an expression, so new(42) gives you an *int pointing to 42.
If you've worked with JSON serialisation or protocol buffers in Go, you'll immediately see why this matters. Optional fields represented as pointers have always been a pain:
// Before Go 1.26 — the helper function tax
func ptr[T any](v T) *T { return &v }
type Config struct {
MaxRetries *int `json:"max_retries,omitempty"`
Timeout *int `json:"timeout,omitempty"`
}
c := Config{
MaxRetries: ptr(3),
Timeout: ptr(30),
}
Every Go codebase I've ever worked on has some variant of that ptr helper. Now:
// Go 1.26
c := Config{
MaxRetries: new(3),
Timeout: new(30),
}
No helper needed. Clean, obvious, built-in. This is the kind of change that removes a paper cut you'd stopped noticing.
Self-Referential Generic Types
The restriction that a generic type couldn't refer to itself in its own type parameter list has been lifted. The release notes give a compact example:
// Go 1.26 — this was a compile error before
type Adder[A Adder[A]] interface {
Add(A) A
}
func algo[A Adder[A]](x, y A) A {
return x.Add(y)
}
The self-reference Adder[A] on the first line says: "any type A that satisfies this constraint must implement Add taking and returning its own type. Previously, the compiler rejected the self-reference outright.
Where this gets practical is when you need to express "a type that can operate on itself" — something that comes up constantly in domain modelling. Here's a more realistic example with a comparable/mergeable node for a CRDT or tree structure:
// Node represents any type that can merge with another of its own kind
// and report its children — useful for CRDTs, ASTs, or any tree structure
// where nodes must be homogeneously typed.
type Node[N Node[N]] interface {
Merge(other N) N
Children() []N
}
// Walk traverses any self-referential node tree.
// The constraint ensures every node in the tree is the same concrete type.
func Walk[N Node[N]](n N, visit func(N)) {
visit(n)
for _, child := range n.Children() {
Walk(child, visit)
}
}
Before 1.26, you couldn't write Node[N Node[N]] — the compiler would reject it. You'd have to fall back to any constraints and lose type safety, or restructure your code to avoid the self-reference entirely. This lifts that restriction cleanly.
It's a niche change — most Go code won't need it — but it unblocks patterns that were genuinely impossible before, particularly around recursive data structures and the kind of "fluent" interfaces where methods return the receiver's own type.
The Green Tea Garbage Collector
The experimental Green Tea GC from Go 1.25 is now the default. The numbers are compelling: 10–40% reduction in GC overhead for real-world programs that lean heavily on the garbage collector. On newer AMD64 hardware (Intel Ice Lake, AMD Zen 4 and newer), vector instructions provide an additional ~10% improvement on top of that.
For the SafeOps365 compliance engine we work on, which does heavy JSON parsing and struct allocation, this is the kind of change that translates directly into lower tail latencies without touching a single line of application code. That's the best kind of performance improvement.
If you hit issues, you can disable it with GOEXPERIMENT=nogreenteagc at build time, but that opt-out disappears in Go 1.27 — so if something's wrong, file an issue now.
Faster cgo and Memory Allocation
Two more runtime wins: cgo call overhead is down ~30%, and the compiler now generates size-specialised allocation routines for small objects (under 512 bytes), cutting allocation costs by up to 30% in those cases.
The cgo improvement is particularly relevant if you're interfacing Go with C libraries. That 30% reduction in baseline overhead makes Go a more compelling choice for projects where you need to bridge between the two worlds.
Goroutine Leak Profiler (Experimental)
This is the feature I'm most excited to try. Enable it with GOEXPERIMENT=goroutineleakprofile and you get a new goroutineleak profile type in runtime/pprof, plus a /debug/pprof/goroutineleak HTTP endpoint.
The runtime can now identify goroutines blocked on channels, mutexes, or condition variables that are unreachable from any running goroutine — in other words, goroutines that will never unblock. If you've ever had a production service slowly accumulate leaked goroutines from early returns in fan-out patterns, this is for you.
The design is zero-overhead when not in use, and the Go team plan to enable it by default in 1.27. I'd recommend enabling it in your CI test suite today.
Revamped go fix
The go fix command has been completely rewritten to use the Go analysis framework (the same one powering go vet). It now ships with a couple of dozen "moderniser" analysers that suggest safe refactors to take advantage of newer language and library features.
It also supports the //go:fix inline directive for inlining function calls. This is a significant improvement for large codebases where you want to gradually adopt new idioms without a big-bang refactor.
Experimental: runtime/secret — Forward Secrecy in the Runtime
This is the feature that has me most excited from a security perspective. The new runtime/secret package, enabled with GOEXPERIMENT=runtimesecret, gives Go a runtime-level facility for securely erasing sensitive data from memory after use. This has been an open issue since 2017, originally filed by the WireGuard team who were resorting to reflection hacks to zero out AEAD cipher keys. Eight years later, Go has a proper answer.
The API is beautifully simple. secret.Do takes a function, runs it, and then erases everything that function touched:
import "runtime/secret"
secret.Do(func() {
// Generate an ephemeral key and
// use it to negotiate the session.
// After this block returns, registers,
// stack, and heap allocations are zeroed.
})
Here's the guarantee: when secret.Do returns, all registers used by the function are zeroed, all stack frames are zeroed, and all heap allocations made inside the block are zeroed as soon as the GC determines they're unreachable.
Why This Matters
Forward secrecy depends on ephemeral keys being truly ephemeral. In a TLS handshake or a peer-to-peer key exchange, you generate a temporary private key, derive a shared secret, and then that temporary key is supposed to disappear forever. If an attacker gets a memory dump — via a core dump, a Heartbleed-style vulnerability, or physical access — and those ephemeral keys are still sitting in memory, forward secrecy is broken.
In Go until now, you had no reliable way to ensure that. The garbage collector moves memory around, copies stack frames, and makes no promises about when or whether freed memory gets zeroed. Developers of libraries like WireGuard's Go implementation resorted to unsafe reflection to try to zero internal cipher buffers:
// The old way — unsettling reflection hacks from wireguard-go
func (con *safeAEAD) clear() {
con.mutex.Lock()
if con.aead != nil {
val := reflect.ValueOf(con.aead)
elm := val.Elem()
typ := elm.Type()
elm.Set(reflect.Zero(typ))
con.aead = nil
}
con.mutex.Unlock()
}
This is fragile, platform-dependent, and doesn't catch temporaries on the stack or in registers. secret.Do solves this at the runtime level.
A Realistic Example: Ephemeral Key Exchange
Here's how you'd use secret.Do to protect an ephemeral key exchange — the kind of thing you'd do in a peer-to-peer encrypted transport:
import (
"crypto/ecdh"
"crypto/rand"
"runtime/secret"
)
func negotiateSession(peerPub *ecdh.PublicKey) (sessionKey []byte, ourPub *ecdh.PublicKey, err error) {
// Allocate the result buffer outside the secret block.
// Anything allocated inside will be erased.
sessionKey = make([]byte, 32)
secret.Do(func() {
// Generate ephemeral private key
privKey, e := ecdh.P256().GenerateKey(rand.Reader)
if e != nil {
err = e
return
}
// Derive shared secret
sharedSecret, e := privKey.ECDH(peerPub)
if e != nil {
err = e
return
}
// Copy the results we need out of the secret block.
// Everything else — privKey, sharedSecret, intermediates —
// will be erased when secret.Do returns.
ourPub = privKey.PublicKey()
copy(sessionKey, deriveSessionKey(sharedSecret))
})
// At this point:
// - The ephemeral private key is gone from registers and stack
// - The raw shared secret is gone
// - Any intermediate allocations are marked for zeroing by GC
// - Only sessionKey and ourPub survive
if err != nil {
return nil, nil, err
}
return sessionKey, ourPub, nil
}
The critical pattern: allocate your result buffersoutsidethe secret.Do block, and copy results into them. Everything allocated inside the block gets erased. The ephemeral private key and raw shared secret — the "toxic waste" of a key exchange — never survive past the block boundary.
Using secret.Enabled for Library Code
If you're writing a library that should behave differently when running inside a secret block, there's a companion function:
func processKey(key []byte) {
if secret.Enabled() {
// We're inside a secret.Do block.
// The runtime will handle cleanup.
} else {
// Defensive manual zeroing as a fallback.
defer func() {
for i := range key {
key[i] = 0
}
}()
}
// ... use the key
}
Limitations to Know
There are some important constraints:
- Linux only, AMD64 and ARM64. On unsupported platforms,
secret.Dojust calls your function directly with no erasure guarantees. No build error, just a silent no-op. - No protection for globals. If your function writes to a global variable, that data isn't erased.
- No protection across goroutines. If your function spawns a new goroutine, the new goroutine's data isn't covered.
- Heap erasure is deferred. Stack and registers are zeroed immediately when
secret.Doreturns. Heap allocations are zeroed when the GC collects them, which could be some time later. - Allocation costs compound. Growing a slice or inserting into a map inside
secret.Domeans theentirenew backing allocation gets erased, not just the secret parts. Be mindful of allocations inside the block.
The Bigger Picture
For anyone building systems where key material transits through Go code — TLS termination, cryptographic protocol implementations, peer-to-peer key exchange — this is a significant step forward. Go is providing a runtime-level guarantee that was previously impossible without dropping to C or using unsafe hacks.
The package is experimental in 1.26, but the API is intentionally minimal (Do and Enabled), and the Go team clearly intend to stabilise it. I'd recommend adopting it now in any code that handles ephemeral keys or session secrets.
I'm currently integrating runtime/secret into envctl, a peer-to-peer secrets management tool I'm building that uses post-quantum encryption (ML-KEM) with no cloud dependencies. The key exchange in envctl is exactly the kind of operation secret.Do was designed for — ephemeral keys that must not survive past the handshake. I'll be writing a dedicated post on what that integration looks like in practice: where secret.Do fits in the exchange flow, what the limitations mean for a real peer-to-peer protocol, and whether the heap erasure timing is acceptable for a tool where secrets are the entire product.
Build with: GOEXPERIMENT=runtimesecret go build ./...
Tooling and Standard Library Highlights
A few other changes worth calling out:
pprof defaults to flame graphs. When you open the pprof web UI with -http, it now lands on the flame graph view instead of the old graph view. A small but welcome default — flame graphs are almost always what you want.
crypto/hpke package. Hybrid Public Key Encryption per RFC 9180, including post-quantum hybrid KEMs. If you're building systems that need to start thinking about post-quantum cryptography (and you should be), this is Go providing batteries-included support.
Post-quantum TLS by default. The hybrid SecP256r1MLKEM768 and SecP384r1MLKEM1024 key exchanges are now available in crypto/tls. Go is pushing the ecosystem forward here.
Experimental SIMD support. The new simd/archsimd package (behind GOEXPERIMENT=simd) provides access to architecture-specific SIMD operations. Currently AMD64 only, supporting 128, 256, and 512-bit vectors. This is early days but it opens the door to serious number-crunching in pure Go.
bytes.Buffer.Peek — returns the next n bytes without advancing the buffer. Simple, useful, long overdue.
Randomised heap base addresses. On 64-bit platforms, the runtime now randomises the heap base at startup. This is a security hardening measure that makes address prediction harder when using cgo.
log/slog multi-handler support. You can now send log records to multiple handlers — stdout and a file, for example. This was a notable gap in slog since it launched in 1.21.
go mod init defaults lower. Running go mod init with Go 1.26 now creates a go.mod with go 1.25.0 rather than go 1.26.0. The intent is to encourage modules that work with currently supported Go versions. A sensible default.
Race Detector on RISC-V
The linux/riscv64 port now supports the race detector. If you're targeting RISC-V, this is a significant improvement for development and testing.
The Bottom Line
Go 1.26 is a release that rewards upgrading. The Green Tea GC, faster cgo, and size-specialised allocation are free performance. The goroutine leak profiler is a tool I wish I'd had years ago. And new(expr) removes a daily annoyance that the entire Go community had collectively worked around.
There's no compatibility risk here — Go's backwards compatibility promise continues to hold. Upgrade your toolchain, run your tests, and enjoy the wins.
For the Belfast Gophers meetup, I'm planning a deep-dive session on the GC changes and the goroutine leak profiler. If you're in the area, come along.
The full release notes are at go.dev/doc/go1.26.