Back to Blog

What's New in Go 1.26: A Complete Guide

A comprehensive guide to Go 1.26's new features: enhanced error handling with errors.AsType, simplified crypto APIs, goroutine leak detection, Green Tea GC performance improvements, and more. Includes runnable code examples.

What's New in Go 1.26: A Complete Guide

Go 1.26 brings meaningful improvements that you'll actually use in your daily work. This release focuses on reducing boilerplate, catching bugs earlier, and making your code faster without any changes on your part.

Let's explore what's new and why it matters.

Table of Contents


Cleaner Pointer Initialization with new(expr)

If you've worked with Go APIs that use optional fields, you've written this awkward pattern before:

example.gogo
port := 8080
config := &Config{Port: &port}  // Can't use &8080 directly!

Go doesn't allow taking the address of a literal value, so you need a temporary variable. Some codebases create helper functions like intPtr(n int) *int to work around this, but that just moves the problem.

Go 1.26 solves this by extending the new() built-in. Previously, new() only accepted types: new(int) returned a *int pointing to zero. Now it accepts any expression:

example.gogo
package main

import "fmt"

type ServerConfig struct {
    Host    string
    Port    *int
    Enabled *bool
}

func main() {
    config := ServerConfig{
        Host:    "localhost",
        Port:    new(8080),
        Enabled: new(true),
    }

    fmt.Printf("Port: %d, Enabled: %t\n", *config.Port, *config.Enabled)
}

How this works: When you write new(8080), Go evaluates the expression, allocates memory for that type (an int in this case), stores the value, and returns a pointer. The same pattern works with any expression—new(x + y) for computed values, new(time.Now()) for function results, or new("default") for strings.

This is particularly useful when working with JSON APIs, Protobuf messages, or any struct where pointer fields indicate "optional with a value" versus "not provided."

Type-Safe Error Checking with errors.AsType

Error handling in Go often requires extracting specific error types from wrapped errors. The standard approach uses errors.As():

example.gogo
var appErr *AppError
if errors.As(err, &appErr) {
    fmt.Printf("Code: %d\n", appErr.Code)
}

This works, but has some rough edges. You declare a variable that leaks into the outer scope even though you only use it inside the if block. The double-pointer syntax (&appErr where appErr is already a pointer) causes confusion. And there's reflection happening under the hood.

Go 1.26 introduces errors.AsType[T](), a generic function that handles this more elegantly:

example.gogo
package main

import (
    "errors"
    "fmt"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("app error %d: %s", e.Code, e.Message)
}

func main() {
    err := &AppError{Code: 404, Message: "not found"}

    if appErr, ok := errors.AsType[*AppError](err); ok {
        fmt.Printf("Code: %d, Message: %s\n", appErr.Code, appErr.Message)
    }

    // Works with wrapped errors too
    wrappedErr := fmt.Errorf("operation failed: %w", err)
    if appErr, ok := errors.AsType[*AppError](wrappedErr); ok {
        fmt.Printf("Unwrapped code: %d\n", appErr.Code)
    }
}

Why this matters: The type parameter [*AppError] tells the compiler exactly what you're looking for. The returned value is properly scoped to the if block. There's no pointer-to-pointer confusion. And because the type is known at compile time, there's no reflection overhead—this runs faster in hot error-handling paths.

Self-Referential Generic Constraints

Go 1.26 adds limited support for recursive type constraints, which enables patterns that were previously impossible to express. Consider a type that needs to compare itself to other values of the same type:

example.gogo
package main

import "fmt"

type Comparable[T any] interface {
    CompareTo(other T) int
}

type Integer int

func (i Integer) CompareTo(other Integer) int {
    if i < other {
        return -1
    } else if i > other {
        return 1
    }
    return 0
}

func Max[T Comparable[T]](a, b T) T {
    if a.CompareTo(b) > 0 {
        return a
    }
    return b
}

func main() {
    result := Max(Integer(5), Integer(3))
    fmt.Printf("Max: %d\n", result)
}

How this works: The constraint Comparable[T] references its own type parameter, and Max[T Comparable[T]] requires that T can compare to itself. Before Go 1.26, this kind of self-referential generic constraint would fail to compile. Now you can build fluent APIs with method chaining, tree structures where nodes reference their own type, and type-safe builders that return the correct type at each step.

Simpler Cryptography APIs

Every Go developer has written this line:

example.gogo
key, err := rsa.GenerateKey(rand.Reader, 2048)

The rand.Reader parameter exists for historical reasons—early Go allowed custom random sources for testing. In practice, you should always use crypto/rand.Reader. Passing anything else, like math/rand, creates a security vulnerability.

Go 1.26 makes the secure choice the default. Pass nil and the function uses crypto/rand.Reader internally:

example.gogo
package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
)

func main() {
    key := make([]byte, 32)
    rand.Read(key)

    fmt.Printf("Generated key: %s\n", hex.EncodeToString(key))
}

This applies to key generation across the crypto packages: rsa.GenerateKey(nil, bits), ecdsa.GenerateKey(curve, nil), and ecdh.GenerateKey(curve, nil). The only time you'd pass a custom reader is in tests where you need deterministic output.

HPKE for Modern Encryption

The new crypto/hpke package implements RFC 9180, a modern standard for hybrid public-key encryption. Traditional public-key encryption like RSA is slow and limited to small messages. The standard workaround—encrypt a random symmetric key with RSA, then use that key for the actual data—works but requires careful implementation.

HPKE standardizes this pattern with modern algorithms. Think of it like a lockbox: the sender generates a one-time lock and key, puts the message in the lockbox, and sends both the locked box and instructions that only the recipient's private key can use to recreate the one-time key.

example.gogo
suite := hpke.NewSuite(
    hpke.DHKEM_X25519,
    hpke.KDF_HKDF_SHA256,
    hpke.AEAD_ChaCha20Poly1305,
)

publicKey, privateKey, _ := suite.GenerateKeyPair(nil)

sender, _ := suite.NewSender(publicKey, nil)
ciphertext, _ := sender.Seal(plaintext, nil)
encapsulated := sender.EncapsulatedKey()

recipient, _ := suite.NewRecipient(privateKey, encapsulated)
decrypted, _ := recipient.Open(ciphertext, nil)

HPKE uses 256-bit keys instead of RSA's 2048-4096 bits, handles messages of any size, and runs significantly faster.

Finding Goroutine Leaks

Goroutine leaks are insidious. Unlike memory leaks that eventually crash your program, goroutine leaks cause gradual performance degradation. Thousands of stuck goroutines accumulate over time, consuming memory and CPU while producing no useful work.

Here's a classic leak pattern:

example.gogo
func handleRequest(ctx context.Context) error {
    results := make(chan Result)

    go func() {
        result := expensiveOperation()
        results <- result  // Blocks forever if context cancels
    }()

    select {
    case <-ctx.Done():
        return ctx.Err()  // Goroutine still blocked on send
    case r := <-results:
        return processResult(r)
    }
}

If the context cancels before expensiveOperation() completes, the goroutine blocks forever trying to send on an unbuffered channel that nobody will ever receive from.

Go 1.26 adds the goroutineleak profile and go test -goroutineleak flag to catch these issues automatically:

example.gogo
package main

import (
    "fmt"
    "runtime"
    "time"
)

func createLeak() {
    ch := make(chan int)
    go func() {
        <-ch  // Blocks forever - no sender
    }()
}

func main() {
    before := runtime.NumGoroutine()
    createLeak()
    time.Sleep(50 * time.Millisecond)
    after := runtime.NumGoroutine()

    fmt.Printf("Before: %d, After: %d\n", before, after)
    if after > before {
        fmt.Println("Goroutine leak detected!")
    }
}

The fix combines three techniques: use a buffered channel so sends don't block, add a select in the goroutine to exit on context cancellation, and check the context in the caller:

example.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func expensiveWork() string {
    time.Sleep(50 * time.Millisecond)
    return "completed"
}

func safeHandler(ctx context.Context) (string, error) {
    ch := make(chan string, 1)

    go func() {
        result := expensiveWork()
        select {
        case ch <- result:
        case <-ctx.Done():
            return
        }
    }()

    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case result := <-ch:
        return result, nil
    }
}

func main() {
    ctx1 := context.Background()
    result1, err1 := safeHandler(ctx1)
    fmt.Printf("Normal: %s, err: %v\n", result1, err1)

    ctx2, cancel := context.WithCancel(context.Background())
    cancel()
    result2, err2 := safeHandler(ctx2)
    fmt.Printf("Canceled: %s, err: %v\n", result2, err2)
}

How this avoids the leak: The buffered channel ensures the send won't block even if nobody receives. The select in the goroutine provides an exit path when the context cancels. The goroutine cleans up properly in both success and cancellation cases.

Go 1.26 also exposes detailed goroutine metrics through runtime/metrics, letting you monitor goroutine counts, blocked goroutines, and runnable goroutines in production dashboards.

Standard Library Improvements

Buffer.Peek

Reading from bytes.Buffer consumes the data. When you need to look ahead without consuming—checking a message type before deciding how to parse it, or validating a format before committing to read—you previously had to copy the buffer.

Go 1.26 adds Peek():

example.gogo
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("Hello, World!")

    peeked := buf.Bytes()[:5]
    fmt.Printf("Peeked: %s\n", peeked)

    fmt.Printf("Full content: %s\n", buf.String())
    fmt.Printf("Length: %d\n", buf.Len())
}

IP Prefix Comparison

Sorting IP prefixes used to require manual byte-by-byte comparison. Now netip.Prefix has a Compare() method that works directly with slices.SortFunc:

example.gogo
package main

import (
    "fmt"
    "net/netip"
    "slices"
)

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("192.168.0.0/16"),
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
    }

    slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })

    for _, p := range prefixes {
        fmt.Println(p)
    }
}

Signal Context Cause

When using signal.NotifyContext() for graceful shutdown, you can now determine which signal triggered the cancellation:

example.gogo
ctx, stop := signal.NotifyContext(
    context.Background(),
    os.Interrupt,
    syscall.SIGTERM,
)
defer stop()

<-ctx.Done()

if cause := context.Cause(ctx); cause != nil {
    switch cause {
    case os.Interrupt:
        log.Println("User interrupted (Ctrl+C)")
    case syscall.SIGTERM:
        log.Println("Graceful shutdown requested")
    }
}

Testing and Logging

slog.MultiHandler

Production applications often need to send logs to multiple destinations—JSON to a file for log aggregation, text to stdout for human debugging, errors to a monitoring service. Go 1.26's slog.MultiHandler handles this without custom fan-out logic:

example.gogo
package main

import (
    "log/slog"
    "os"
)

func main() {
    jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(jsonHandler)
    logger.Info("Server started", "port", 8080)
}

You can configure different log levels per destination. Send everything to the file at debug level, but only warnings and errors to the console. Each handler filters independently.

Test Artifact Directories

Integration tests often produce output files—screenshots, SQL dumps, generated reports. These used to scatter across /tmp or random directories with no cleanup.

t.ArtifactDir() provides a standard location that's cleaned up when tests pass but preserved when tests fail:

example.gogo
func TestGenerateReport(t *testing.T) {
    artifactDir := t.ArtifactDir()
    reportPath := filepath.Join(artifactDir, "report.txt")
    
    err := os.WriteFile(reportPath, []byte("Test Report\n"), 0644)
    if err != nil {
        t.Fatalf("Failed to write: %v", err)
    }
}

The preservation behavior on failure is key—you can inspect artifacts from failing tests without manually hunting for them.

httptest Improvements

Testing HTTPS code that connects to external hosts required either disabling certificate verification (dangerous even in tests) or complex certificate setup. Go 1.26's httptest.Server.Client() now redirects example.com requests to your test server with TLS handled automatically:

example.gogo
func TestExternalAPIClient(t *testing.T) {
    server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()

    client := server.Client()
    resp, err := client.Get("https://example.com/api/status")
    // Request goes to test server, not real example.com
}

Performance: Green Tea GC

Go 1.26 includes performance improvements that require no code changes. Rebuild your application and it runs faster.

The new "Green Tea" garbage collector delivers 30-40% lower P99 latency, 10-15% reduced memory overhead, and 5-10% higher throughput. Applications that allocate many short-lived objects see the biggest gains. The improvements come from smarter scheduling of GC work and reduced overhead for tracking allocations.

System calls and cgo are approximately 30% faster due to removing the _Psyscall processor state that added overhead to every syscall. Small object allocation (1-512 bytes) is also ~30% faster through specialized allocation routines using jump tables instead of generic paths.

Standard library functions benefit too: fmt.Errorf is about 30% faster, and io.ReadAll is 28% faster. In production testing, an HTTP API server handling 10,000 requests per second saw P99 latency drop from 45ms to 32ms.

Runtime Features

Context-Aware Network Dialing

The net package now propagates context deadlines through DNS resolution and connection, not just the connection phase:

example.gogo
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", "example.com:443")
// Timeout now covers DNS + connect time combined

Protecting Sensitive Data

The experimental runtime/secret package helps protect sensitive data from memory dumps:

example.gogo
secretKey := secret.New([]byte("sk_live_secret"))
defer secretKey.Destroy()

apiKey := secretKey.Bytes()
makeAuthenticatedRequest(apiKey)

The Destroy() call clears the memory, reducing the window where secrets are exposed in core dumps or swap files.

SIMD Operations

The new simd/archsimd package provides access to hardware vector instructions on amd64:

example.gogo
func vectorAdd(a, b [4]float32) [4]float32 {
    va := archsimd.Float32x4FromArray(a)
    vb := archsimd.Float32x4FromArray(b)
    return va.Add(vb).ToArray()
}

SIMD processes multiple values per instruction—4-16x throughput for numeric operations in image processing, audio/video codecs, and scientific computing. Keep in mind this is platform-specific; provide scalar fallbacks for portability.

Migrating Your Code

Go 1.26 improves go fix to automate common migrations:

example.bashbash
go fix -diff ./...   # Preview changes
go fix ./...         # Apply all fixes

The tool updates rsa.GenerateKey(rand.Reader, 2048) to rsa.GenerateKey(nil, 2048) and converts errors.As patterns to errors.AsType where applicable. Review the diff, run your tests, and commit.

For the full catalog of rewrites the new go fix ships with, see every modernizer in go fix 1.26.

What This Means for Your Code

Go 1.26 reduces boilerplate with new(expr) and errors.AsType, catches goroutine leaks before they reach production, runs faster without code changes, and simplifies crypto APIs by making secure defaults automatic.

Ready to master Go?

Join LevelUpGo and start building real projects with interactive, hands-on lessons.

Start Learning Free