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)
- Type-Safe Error Checking with errors.AsType
- Self-Referential Generic Constraints
- Simpler Cryptography APIs
- Finding Goroutine Leaks
- Standard Library Improvements
- Testing and Logging
- Performance: Green Tea GC
- Runtime Features
- Migrating Your Code
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.gogoport := 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.gogopackage 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.gogovar 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.gogopackage 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.gogopackage 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.gogokey, 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.gogopackage 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.gogosuite := 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.gogofunc 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogoctx, 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.gogopackage 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.gogofunc 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.gogofunc 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.gogoctx, 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.gogosecretKey := 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.gogofunc 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.bashbashgo 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.
