Das meiste an Go 1.26 merkst du in Code, den du jeden Tag schreibst. Rund um Pointer und Fehlertypen fällt Boilerplate weg, es gibt einen neuen Weg, Goroutine-Leaks zu finden, und die Runtime wird schneller, sobald du einfach neu baust.
Inhaltsverzeichnis
- Pointer einfacher initialisieren mit new(expr)
- Typsichere Fehlerprüfung mit errors.AsType
- Selbstreferenzielle Generic Constraints
- Einfachere Kryptografie-APIs
- Goroutine-Leaks aufspüren
- Verbesserungen in der Standardbibliothek
- Testing und Logging
- Performance: Green Tea GC
- Runtime-Features
- Deinen Code migrieren
Pointer einfacher initialisieren mit new(expr)
Wenn du schon eine Go-API benutzt hast, die optionale Felder als Pointer abbildet, hast du das hier geschrieben:
example.gogoport := 8080 config := &Config{Port: &port} // Can't use &8080 directly!
Go lässt dich nicht die Adresse eines Literals nehmen, also brauchst du zuerst eine temporäre Variable. Manche Codebases legen Helfer wie intPtr(n int) *int an, aber damit wandert das Problem nur an eine andere Stelle.
Go 1.26 behebt das, indem es das Built-in new() erweitert. Bisher nahm es nur Typen an, new(int) gab dir also einen *int, der auf 0 zeigt. Jetzt nimmt es jeden Ausdruck an:
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) }
Wenn du new(8080) schreibst, wertet Go den Ausdruck aus, reserviert Speicher für seinen Typ (hier ein int), legt den Wert dort ab und gibt einen Pointer zurück. Jeder Ausdruck funktioniert: new(x + y) für einen berechneten Wert, new(time.Now()) für das Ergebnis einer Funktion oder new("default") für einen String.
Am häufigsten brauchst du das bei JSON-APIs, Protobuf-Nachrichten und jedem anderen Struct, in dem ein Pointer-Feld „optional, mit Wert“ von „nicht angegeben“ trennt.
Typsichere Fehlerprüfung mit errors.AsType
Um einen bestimmten Fehlertyp aus einem gewrappten Error herauszuholen, hast du bisher errors.As() benutzt:
example.gogovar appErr *AppError if errors.As(err, &appErr) { fmt.Printf("Code: %d\n", appErr.Code) }
Das funktioniert, ist aber umständlich. Die Variable landet im äußeren Scope, obwohl du sie nur im if-Block brauchst. Du übergibst &appErr, obwohl appErr schon ein Pointer ist, und darüber stolpern viele. Außerdem arbeitet errors.As unter der Haube mit Reflection.
Go 1.26 bringt errors.AsType[T](), eine generische Funktion, die dasselbe mit weniger Umständen erledigt:
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) } }
Der Typparameter [*AppError] sagt dem Compiler genau, wonach du suchst, und das Ergebnis gilt nur im if-Block. Einen Pointer auf einen Pointer gibt es nicht mehr. Weil der Typ zur Compilezeit feststeht, fällt auch die Reflection weg, und in heißen Pfaden der Fehlerbehandlung läuft der Code schneller.
Selbstreferenzielle Generic Constraints
Go 1.26 unterstützt rekursive Type Constraints in begrenztem Umfang. Damit lassen sich manche Muster zum ersten Mal ausdrücken, etwa ein Typ, der sich mit anderen Werten desselben Typs vergleicht:
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) }
Der Constraint Comparable[T] verweist auf seinen eigenen Typparameter, und Max[T Comparable[T]] verlangt, dass sich T mit sich selbst vergleichen lässt. Vor Go 1.26 ließ sich so ein selbstreferenzieller Generic Constraint nicht kompilieren. Jetzt kannst du Fluent APIs mit Method Chaining bauen, Bäume, deren Knoten auf ihren eigenen Typ verweisen, und typsichere Builder, die bei jedem Schritt den richtigen Typ zurückgeben.
Einfachere Kryptografie-APIs
Die meisten Go-Entwickler haben diese Zeile schon geschrieben:
example.gogokey, err := rsa.GenerateKey(rand.Reader, 2048)
Das Argument rand.Reader ist ein historisches Überbleibsel. Frühe Go-Versionen erlaubten eigene Zufallsquellen für Tests. In der Praxis solltest du immer crypto/rand.Reader übergeben, und alles andere, etwa math/rand, reißt eine Sicherheitslücke auf.
Go 1.26 macht die sichere Wahl zum Standard. Übergib nil, und die Funktion verwendet intern crypto/rand.Reader:
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)) }
Das gilt für die Schlüsselerzeugung in allen Krypto-Paketen: rsa.GenerateKey(nil, bits), ecdsa.GenerateKey(curve, nil) und ecdh.GenerateKey(curve, nil). Einen eigenen Reader brauchst du nur noch für einen Test, der eine deterministische Ausgabe verlangt.
HPKE für moderne Verschlüsselung
Das neue Paket crypto/hpke implementiert RFC 9180, einen modernen Standard für hybride Public-Key-Verschlüsselung. RSA allein ist langsam und kann nur kleine Nachrichten verschlüsseln. Der übliche Workaround: Du verschlüsselst einen zufälligen symmetrischen Schlüssel mit RSA und nutzt diesen Schlüssel dann für die eigentlichen Daten. Das funktioniert, aber du musst es sorgfältig implementieren.
HPKE standardisiert dieses Muster mit modernen Algorithmen. Stell dir eine Schließkassette vor. Der Absender erzeugt ein Einmal-Schloss samt Schlüssel, schließt die Nachricht in die Kassette ein und schickt sie zusammen mit einer Anleitung los. Nur mit dem privaten Schlüssel des Empfängers lässt sich aus dieser Anleitung der Einmal-Schlüssel wiederherstellen.
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 verwendet 256-Bit-Schlüssel statt der 2048 bis 4096 Bit von RSA, verarbeitet Nachrichten beliebiger Größe und ist viel schneller.
Goroutine-Leaks aufspüren
Goroutine-Leaks übersieht man leicht. Ein Memory Leak bringt dein Programm irgendwann zum Absturz. Ein Goroutine-Leak macht es nur mit der Zeit langsamer, weil sich Tausende hängende Goroutines ansammeln, die Speicher und CPU festhalten, ohne etwas Sinnvolles zu tun.
So entsteht ein Leak ganz typisch:
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) } }
Wird der Context abgebrochen, bevor expensiveOperation() zurückkehrt, blockiert die Goroutine für immer. Sie versucht, auf einen ungepufferten Channel zu senden, den niemand mehr liest.
Go 1.26 bringt ein Profil goroutineleak und ein Flag go test -goroutineleak mit, die solche Fälle automatisch finden. Ein Leak kannst du auch sehen, indem du die Goroutines vorher und nachher zählst:
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!") } }
Die Lösung besteht aus drei Teilen: einem gepufferten Channel, damit das Senden nicht blockiert, einem select in der Goroutine, damit sie beim Abbruch des Contexts aussteigen kann, und einer Context-Prüfung beim Aufrufer:
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) }
Durch den gepufferten Channel kann das Senden nicht blockieren, auch wenn niemand empfängt. Das select gibt der Goroutine einen Ausweg, wenn der Context abgebrochen wird. Ob die Arbeit gelingt oder abgebrochen wird, die Goroutine endet in beiden Fällen.
Außerdem ergänzt Go 1.26 runtime/metrics um Goroutine-Metriken. Deine Produktions-Dashboards können damit die Gesamtzahl sowie die blockierten und lauffähigen Goroutines verfolgen.
Verbesserungen in der Standardbibliothek
Buffer.Peek
Wenn du aus einem bytes.Buffer liest, werden die Daten verbraucht. Wolltest du vorher hineinschauen, etwa um den Nachrichtentyp zu prüfen, bevor du einen Parser wählst, oder um ein Format zu validieren, bevor du liest, musstest du den Buffer kopieren.
Go 1.26 ergänzt 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-Präfixe vergleichen
Zum Sortieren von IP-Präfixen musstest du bisher Bytes von Hand vergleichen. Jetzt hat netip.Prefix eine Methode Compare(), die du direkt an slices.SortFunc übergeben kannst:
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) } }
Das auslösende Signal als Context-Cause
Wenn du signal.NotifyContext() für einen Graceful Shutdown verwendest, siehst du jetzt, welches Signal den Abbruch ausgelöst hat:
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 und Logging
slog.MultiHandler
Produktionsdienste loggen oft an mehr als einen Ort: JSON in eine Datei für die Log-Aggregation, Text auf stdout zum Debuggen, Fehler an einen Monitoring-Service. slog.MultiHandler übernimmt dieses Fan-out für dich, du musst es nicht selbst schreiben:
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) }
Jeder Handler filtert nach seinem eigenen Level. Die Datei kann alles ab Debug-Level aufnehmen, während auf der Konsole nur Warnungen und Fehler landen.
Verzeichnisse für Test-Artefakte
Integrationstests schreiben oft Ausgabedateien wie Screenshots, SQL-Dumps und generierte Reports. Die landeten bisher verstreut in /tmp oder irgendeinem anderen Verzeichnis, und niemand räumte sie auf.
t.ArtifactDir() gibt ihnen einen festen Ort. Er wird aufgeräumt, wenn der Test besteht, und bleibt erhalten, wenn er fehlschlägt:
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) } }
Nützlich wird das dadurch, dass die Dateien bei einem Fehlschlag bleiben. Du kannst dir die Ausgabe eines fehlgeschlagenen Tests ansehen, ohne danach zu suchen.
Verbesserungen in httptest
Um HTTPS-Code zu testen, der mit externen Hosts spricht, musstest du bisher die Zertifikatsprüfung abschalten (selbst in Tests keine gute Idee) oder ein fummeliges Zertifikats-Setup bauen. In Go 1.26 leitet httptest.Server.Client() Anfragen an example.com auf deinen Testserver um und kümmert sich um TLS:
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
Keine dieser Verbesserungen verlangt Codeänderungen. Bau mit Go 1.26 neu, und dein Programm läuft schneller.
Der neue Garbage Collector Green Tea senkt die P99-Latenz um 30 bis 40 % und den Speicher-Overhead um 10 bis 15 % und erhöht den Durchsatz um 5 bis 10 %. Am meisten gewinnen Programme, die viele kurzlebige Objekte allokieren. Die Gewinne kommen von einer besseren Planung der GC-Arbeit und von weniger Overhead beim Tracking von Allokationen.
System Calls und cgo-Aufrufe sind etwa 30 % schneller, weil die Runtime den Prozessorzustand _Psyscall gestrichen hat, der jeden Syscall mit Overhead belastete. Auch das Allokieren kleiner Objekte (1 bis 512 Bytes) ist etwa 30 % schneller, dank spezialisierter Allokationsroutinen, die Jump Tables statt generischer Pfade nutzen.
Auch einige Funktionen der Standardbibliothek sind schneller geworden. fmt.Errorf ist etwa 30 % schneller und io.ReadAll 28 %. In Produktionstests sank bei einem HTTP-API-Server mit 10.000 Anfragen pro Sekunde die P99-Latenz von 45ms auf 32ms.
Runtime-Features
Context-fähiger Verbindungsaufbau
Das Paket net wendet Context-Deadlines jetzt auch auf die DNS-Auflösung an, nicht nur auf den Verbindungsaufbau selbst:
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
Sensible Daten schützen
Das experimentelle Paket runtime/secret hilft dabei, sensible Daten aus Memory Dumps herauszuhalten:
example.gogosecretKey := secret.New([]byte("sk_live_secret")) defer secretKey.Destroy() apiKey := secretKey.Bytes() makeAuthenticatedRequest(apiKey)
Destroy() löscht den Speicher. Dadurch wird die Zeit kürzer, in der ein Secret in einem Core Dump oder einer Swap-Datei auftauchen kann.
SIMD-Operationen
Das neue Paket simd/archsimd gibt dir auf amd64 die Vektorbefehle der Hardware:
example.gogofunc vectorAdd(a, b [4]float32) [4]float32 { va := archsimd.Float32x4FromArray(a) vb := archsimd.Float32x4FromArray(b) return va.Add(vb).ToArray() }
SIMD verarbeitet mehrere Werte pro Befehl und erreicht so bei numerischer Arbeit in Bildverarbeitung, Audio- und Video-Codecs und wissenschaftlichem Rechnen den 4- bis 16-fachen Durchsatz. Das Paket ist plattformspezifisch, also schreib einen skalaren Fallback, wenn dein Code auch auf anderen Architekturen laufen muss.
Deinen Code migrieren
Go 1.26 verbessert go fix, sodass es die gängigen Migrationen automatisieren kann:
example.bashbashgo fix -diff ./... # Preview changes go fix ./... # Apply all fixes
Es schreibt rsa.GenerateKey(rand.Reader, 2048) in rsa.GenerateKey(nil, 2048) um und wandelt errors.As-Muster, wo es geht, in errors.AsType um. Sieh dir den Diff an, lass deine Tests laufen und committe.
Die vollständige Liste der Rewrites findest du unter alle Modernizer in go fix 1.26.
Was das für deinen Code bedeutet
Als Erstes wirst du mit new(expr) und errors.AsType zu tun haben, und einen Teil dieser Migration nimmt dir go fix ab. Lass go test -goroutineleak über deinen nebenläufigen Code laufen, um Leaks zu finden, bevor sie in Produktion landen. Die Schlüsselgeneratoren der Krypto-Pakete wählen jetzt die sichere Zufallsquelle, wenn du nil übergibst. Für die Beschleunigungen in GC und Runtime musst du nur neu bauen.
