Zurück zum Blog

Was ist neu in Go 1.26? Der komplette Leitfaden

Ein umfassender Leitfaden zu den neuen Features von Go 1.26: verbessertes Error Handling mit errors.AsType, einfachere Krypto-APIs, Erkennung von Goroutine-Leaks, Performance-Gewinne durch den Green Tea GC und mehr. Mit lauffähigen Codebeispielen.

Was ist neu in Go 1.26? Der komplette Leitfaden

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)

Wenn du schon eine Go-API benutzt hast, die optionale Felder als Pointer abbildet, hast du das hier geschrieben:

example.gogo
port := 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.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)
}

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.gogo
var 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.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)
    }
}

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.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)
}

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.gogo
key, 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.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))
}

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.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 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.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)
    }
}

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.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!")
    }
}

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.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)
}

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.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-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.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)
    }
}

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.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 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.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)
}

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.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)
    }
}

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.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

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.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

Sensible Daten schützen

Das experimentelle Paket runtime/secret hilft dabei, sensible Daten aus Memory Dumps herauszuhalten:

example.gogo
secretKey := 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.gogo
func 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.bashbash
go 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.

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen