Go 1.26 reduces boilerplate in several everyday APIs. Pointer initialization that used to take three lines now takes one, error matching no longer needs a pre-declared variable, and the runtime ships a new garbage collector that cuts GC overhead for most workloads. The highlights below are organized by theme and include editable snippets. Hit Run to try them yourself.
Every snippet runs on Go 1.26 via Codapi sandboxes directly in your browser, no local toolchain required.
Less boilerplate
Pointer initialization with new(expr)
Until now, new only accepted a type. Getting a pointer to a concrete value required an intermediate variable:
// Go 1.25: two steps to get a pointer to a value
val := 100
ptr := &val
fmt.Println(*ptr)Starting in Go 1.26 you can pass any expression to new and get back a pointer to a freshly allocated copy:
// Go 1.26: one step
ptr := new(100)
fmt.Println(*ptr)This is especially useful for optional struct fields. APIs serialized as JSON or protobuf often use *T to distinguish "not set" from the zero value. Before you had to declare a helper variable; now it's a one-liner:
f := Feature{Name: "dark-mode", Enabled: new(true)}
data, _ := json.Marshal(f)
fmt.Println(string(data))It works with composite literals and function return values too:
// slice literal
tags := new([]string{"alpha", "beta", "gamma"})
fmt.Println(*tags)
// struct literal
type Dimensions struct{ W, H int }
screen := new(Dimensions{W: 1920, H: 1080})
fmt.Println(*screen)
// function return value
greet := func() string { return "hello, world" }
msg := new(greet())
fmt.Println(*msg)Type-safe error matching with errors.AsType
errors.As requires a pre-declared target variable and uses reflection under the hood. The new generic errors.AsType[E] removes both pain points.
The old way:
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
func main() {
_, err := decode(`{"broken`)
err = fmt.Errorf("config load: %w", err)
// Go 1.25: target variable lives outside the if block
var synErr *json.SyntaxError
if errors.As(err, &synErr) {
fmt.Printf("syntax error at byte %d\n", synErr.Offset)
}
}
func decode(raw string) (map[string]any, error) {
var out map[string]any
err := json.NewDecoder(strings.NewReader(raw)).Decode(&out)
return out, err
}And the new way, everything scoped inside the if:
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
func main() {
_, err := decode(`{"broken`)
err = fmt.Errorf("config load: %w", err)
// Go 1.26: no pre-declared variable, no reflection
if se, ok := errors.AsType[*json.SyntaxError](err); ok {
fmt.Printf("syntax error at byte %d\n", se.Offset)
}
}
func decode(raw string) (map[string]any, error) {
var out map[string]any
err := json.NewDecoder(strings.NewReader(raw)).Decode(&out)
return out, err
}Because the type is checked at compile time, you also avoid the runtime panics that errors.As can produce when called with the wrong kind of target. Here is a dispatcher that classifies multiple error types:
package main
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
)
func diagnose(err error) string {
if se, ok := errors.AsType[*json.SyntaxError](err); ok {
return fmt.Sprintf("bad JSON at byte %d", se.Offset)
}
if ue, ok := errors.AsType[*url.Error](err); ok {
return fmt.Sprintf("URL error (%s %s)", ue.Op, ue.URL)
}
return "unrecognized: " + err.Error()
}
func main() {
// JSON error
var m map[string]any
jerr := json.NewDecoder(strings.NewReader(`{`)).Decode(&m)
fmt.Println(diagnose(jerr))
// URL error
_, uerr := url.Parse("://missing-scheme")
if uerr != nil {
fmt.Println(diagnose(uerr))
}
}Peeking into a bytes.Buffer
bytes.Buffer gains a Peek(n) method that returns the next n bytes without consuming them. This is handy for protocol parsers that need to inspect a header before deciding how to read the rest:
buf := bytes.NewBufferString("GET /index.html HTTP/1.1")
// Look at the first 3 bytes; the read cursor stays put
method, err := buf.Peek(3)
fmt.Printf("method=%q err=%v\n", method, err)
// Skip past "GET "
buf.Next(4)
// Peek at the path
path, err := buf.Peek(11)
fmt.Printf("path=%q err=%v\n", path, err)
// The rest of the buffer is still intact
fmt.Printf("remaining=%q\n", buf.String())If you ask for more bytes than remain, Peek returns whatever is left along with io.EOF:
buf := bytes.NewBufferString("OK")
data, err := buf.Peek(64)
fmt.Printf("data=%q err=%v\n", data, err)Faster by default
Green Tea garbage collector
The Green Tea GC was introduced as an opt-in experiment in Go 1.25. In 1.26 it becomes the default collector. Instead of chasing individual object pointers scattered across the heap, it walks memory in contiguous regions, which plays much better with modern CPU caches and allows more parallel scanning.
The Go team's benchmarks show GC overhead dropping between 10 % and 40 % for allocation-heavy programs, with additional gains on recent Intel and AMD microarchitectures.
Here is a synthetic workload that creates many short-lived allocations, exactly the scenario where Green Tea helps most:
const N = 250_000
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
// Allocate N small structs and keep references alive
type Coord struct{ Lat, Lng float64 }
coords := make([]Coord, N)
for i := range coords {
coords[i] = Coord{Lat: float64(i) * 0.01, Lng: float64(i) * -0.01}
}
runtime.KeepAlive(coords)
runtime.GC()
runtime.ReadMemStats(&after)
fmt.Printf("GC cycles : %d\n", after.NumGC-before.NumGC)
fmt.Printf("Pause total: %.2f ms\n",
float64(after.PauseTotalNs-before.PauseTotalNs)/1e6)
fmt.Printf("Heap in use: %.1f MiB\n",
float64(after.HeapInuse)/1024/1024)If you need the old collector for any reason, build with GOEXPERIMENT=nogreenteagc. That escape hatch is expected to disappear in Go 1.27.
io.ReadAll performance overhaul
io.ReadAll was rewritten internally. It now grows its scratch buffer exponentially and produces a final slice trimmed to the exact size needed. Benchmarks show roughly double the throughput with half the peak memory, and the function signature hasn't changed at all.
// Simulate reading a ~400 KiB HTTP body
line := "The quick brown gopher jumps over the lazy mutex.\n"
body := strings.Repeat(line, 8_000)
data, err := io.ReadAll(bytes.NewBufferString(body))
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Read %d bytes\n", len(data))
fmt.Printf("len == cap: %v (final slice is tightly sized)\n", len(data) == cap(data))testing.B.Loop: inlining fixed
B.Loop() was added in Go 1.25 as the modern replacement for the manual for i := 0; i < b.N; i++ pattern. A regression in 1.25 prevented the loop body from being inlined, which could artificially inflate allocs/op. Go 1.26 fixes that.
The classic b.N pattern:
input := "the quick brown fox jumps over the lazy dog"
r := testing.Benchmark(func(b *testing.B) {
var sink int
for i := 0; i < b.N; i++ {
sink = countVowels(input)
}
_ = sink
})
fmt.Printf("b.N style: %d ns/op %d allocs/op\n", r.NsPerOp(), r.AllocsPerOp())And the cleaner b.Loop() form, now with correct inlining:
input := "the quick brown fox jumps over the lazy dog"
r := testing.Benchmark(func(b *testing.B) {
var sink int
for b.Loop() {
sink = countVowels(input)
}
_ = sink
})
fmt.Printf("b.Loop style: %d ns/op %d allocs/op\n", r.NsPerOp(), r.AllocsPerOp())Stronger generics & reflection
Recursive type constraints
Before Go 1.26 you couldn't write a generic constraint that references the constrained type parameter; the compiler rejected it. Now it works:
fmt.Println(Clamp(Score(150), Score(0), Score(100)))
fmt.Println(Clamp(Score(-5), Score(0), Score(100)))
fmt.Println(Clamp(Score(42), Score(0), Score(100)))This supports self-referential builder interfaces and strongly typed collection contracts that previously required sacrificing type safety.
reflect: iterator methods
reflect.Type and reflect.Value now expose .Fields() and .Methods() iterators that work directly with for range. No more manual indexing.
Type.Fields: walk struct metadata:
for f := range reflect.TypeFor[Server]().Fields() {
fmt.Printf("%-8s tag=%s\n", f.Name, f.Tag.Get("yaml"))
}Value.Fields: iterate over field metadata and runtime values together:
srv := Server{Addr: "0.0.0.0", Port: 443, TLS: true, Workers: 8}
for sf, v := range reflect.ValueOf(srv).Fields() {
fmt.Printf("%-8s = %v\n", sf.Name, v)
}.Methods() works the same way for method sets. The old for i := range t.NumField() pattern still compiles, but the new iterators are shorter and compose nicely with other iterator-based APIs.
Better observability
Fan-out logging with slog.NewMultiHandler
slog.NewMultiHandler sends each log record to every handler you give it. Its Enabled method returns true if any handler accepts the level, so no messages are silently swallowed.
A single handler to start:
h := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
log := slog.New(h)
log.Debug("starting up", "version", "1.26.0")
log.Info("listening", "addr", ":8080")Now wire a text handler (all levels) and a JSON handler (warnings and above) into a single logger:
console := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
var jsonBuf bytes.Buffer
structured := slog.NewJSONHandler(&jsonBuf, &slog.HandlerOptions{
Level: slog.LevelWarn,
})
log := slog.New(slog.NewMultiHandler(console, structured))
log.Debug("loading config", "file", "app.toml")
log.Info("server ready", "workers", 4)
log.Warn("high latency", "p99_ms", 320)
log.Error("disk full", "mount", "/data")
fmt.Println("\n── JSON (warn+ only) ──")
fmt.Print(jsonBuf.String())Signal-aware context cancellation
When signal.NotifyContext catches a signal, context.Cause now returns the actual signal instead of the generic context.Canceled. Combined with errors.AsType, you can branch on exactly which signal arrived:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Fire SIGTERM at ourselves after a short delay
go func() {
time.Sleep(20 * time.Millisecond)
proc, _ := os.FindProcess(os.Getpid())
proc.Signal(syscall.SIGTERM)
}()
<-ctx.Done()
cause := context.Cause(ctx)
fmt.Println("ctx.Err() =", ctx.Err())
fmt.Println("context.Cause() =", cause)
// Use AsType to identify the exact signal
if sig, ok := errors.AsType[syscall.Signal](cause); ok {
fmt.Printf("signal %d (%s): starting graceful shutdown\n", int(sig), sig)
}Goroutine leak detector (experimental)
A new goroutineleak pprof profile identifies goroutines that are permanently stuck on a channel or sync primitive whose counterpart is unreachable. The collector looks at the reachability graph: if no runnable goroutine can ever unblock a waiting one, it's flagged as a leak.
A minimal leaking example, where the sender blocks forever because nobody reads:
startWorker(1) // return value discarded; goroutine leaked
startWorker(2) // same
fmt.Println("Two goroutines leaked.")
fmt.Println("Detect with: GOEXPERIMENT=goroutineleakprofile go build .")
fmt.Println("Then: curl http://localhost:6060/debug/pprof/goroutineleak")The fix: use a buffered channel sized to the number of senders, so they can complete even if the receiver walks away:
fmt.Println(fanOut([]int{10, 20, 30}))Tooling
Modernized go fix
go fix was rebuilt from the ground up on the same analysis engine that powers go vet. It ships over 20 fixers that rewrite idiomatic patterns automatically and safely.
# Modernize your entire module
go fix ./...
# Preview the diff without writing
go fix -diff ./...
# Run a single fixer
go fix -stringsCut ./...The stringsCut fixer, for example, replaces a common two-step strings.Index + slice pattern with strings.Cut:
// Before go fix
func parseHeader(line string) (string, string) {
i := strings.Index(line, ": ")
if i < 0 {
return line, ""
}
return line[:i], line[i+2:]
}// After go fix
func parseHeader(line string) (string, string) {
key, val, _ := strings.Cut(line, ": ")
return key, val
}Library authors can also mark deprecated wrappers with //go:fix inline so that downstream callers are automatically migrated when they run go fix:
// Deprecated: use ProcessV2.
//
//go:fix inline
func Process(data []byte) error { return ProcessV2(data) }Quick reference
| Area | Change |
|---|---|
| Language | new(expr) lets you initialize pointers in a single expression |
| Language | Generic type constraints can now be self-referential |
| Runtime | Green Tea GC is the default; expect 10–40 % less GC overhead |
| Runtime | CGo and syscall paths are ~30 % faster |
| Runtime | Heap base address is randomized on 64-bit platforms |
| stdlib | errors.AsType[T]: generic, compile-time-checked error unwrapping |
| stdlib | io.ReadAll rewritten for ~2× throughput and ~½ peak memory |
| stdlib | slog.NewMultiHandler sends records to multiple handlers at once |
| stdlib | signal.NotifyContext now exposes the signal via context.Cause |
| stdlib | reflect gains .Fields() / .Methods() iterator methods |
| stdlib | bytes.Buffer.Peek reads ahead without consuming |
| stdlib | crypto/hpke implements RFC 9180 with post-quantum KEM support |
| Tools | go fix rebuilt as an analysis-based modernizer with 20+ fixers |
| Testing | B.Loop() inlining regression fixed; accurate allocs/op again |
| Experimental | goroutineleakprofile: GC-based stuck-goroutine detection |
For the full changelog, see the official Go 1.26 release notes.
Comments