Zurück zum Blog

Was ist neu in Go 1.27? Der komplette Überblick

Go 1.27 bringt generische Methoden, ein uuid-Paket in der Standardbibliothek und json/v2 als Standard-Engine. Hier erfährst du, was du jetzt einsetzen solltest und was beim Upgrade unbemerkt kaputtgeht.

Was ist neu in Go 1.27? Der komplette Überblick

Go 1.27 bringt fünf neue Pakete in die Standardbibliothek (encoding/json/v2, encoding/json/jsontext, crypto/mldsa, uuid und simd) und dazu drei Sprachänderungen. Manches willst du ab dem ersten Tag nutzen, etwa generische Methoden und ein uuid-Paket, das in den meisten go.mod-Dateien eine Abhängigkeit überflüssig macht. Anderes wird deine Test-Suite scheitern lassen, bevor du merkst, dass es das Feature gibt: compress/flate erzeugt andere Bytes, Funktionsliterale bekommen andere Symbolnamen, und eine Handvoll GODEBUG-Notausgänge bricht jetzt den Build ab, statt das alte Verhalten wiederherzustellen.

Inhaltsverzeichnis

Generische Methoden und die eine Sache, die sie immer noch nicht können

Seit Generics mit Go 1.18 kamen, funktionierten Typparameter an genau zwei Stellen: in Top-Level-Funktionen und in Typdeklarationen. Methoden blieben außen vor. Wolltest du also eine Transformation, die den Elementtyp deiner Collection ändert, musstest du sie als Funktion auf Paketebene schreiben, und Funktionen auf Paketebene lassen sich nicht verketten.

Wahrscheinlich hast du so etwas schon geschrieben. Eine Metrik-Pipeline, die Sensorwerte in Anzeige-Labels umwandelt, braucht zwei Transformationen, und jede muss den vorherigen Aufruf umschließen:

example.gogo
type Metric struct {
    Sensor  string
    Celsius int
}

type List[T any] []T

// Package-level, because a method couldn't declare U.
func Map[T, U any](l List[T], f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    temps := Map(metrics, func(m Metric) int { return m.Celsius })
    labels := Map(temps, func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

In Go 1.27 wird Map zu einer echten Methode, die unabhängig vom Receiver ihren eigenen Typparameter deklariert:

example.gogo
// U is the method's own type parameter, new in Go 1.27.
func (l List[T]) Map[U any](f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    labels := metrics.
        Map(func(m Metric) int { return m.Celsius }).
        Map(func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

Das Verketten ist die offensichtliche Verbesserung. Die weniger offensichtliche betrifft die Benennung: MapSlice, MapSet und MapBox schrumpfen zu einem einzigen Map pro Typ zusammen, weil der Receiver bereits sagt, welchen Typ du meinst. Der Receiver hilft auch dem Compiler: List[Metric] legt T fest, sodass nur noch der Ausgabetyp inferiert werden muss. Und Methoden tauchen auf, wenn du in deinem Editor einen Punkt tippst. Eine Hilfsfunktion auf Paketebene hilft nur, wenn du schon weißt, dass es sie gibt.

Die Standardbibliothek nutzt das Feature selbst. math/rand/v2 deklariert jetzt (*Rand) N[Int intType](Int) Int als Methode neben der Funktion N auf Paketebene, was die alten Regeln nicht erlaubt haben.

Es gibt eine Grenze, und die übersieht man leicht. In den Release Notes heißt es:

Das hier kompiliert also nicht:

example.gogo
type Transformer interface {
    // Compile error: interface methods can't declare type parameters.
    Map[U any](f func(Metric) U) any
}

Interface-Dispatch passiert zur Laufzeit, daher kann der Compiler nicht wissen, welche Instanziierungen einer generischen Methode er für einen dynamisch dispatchten Aufruf erzeugen soll. Baut deine API auf einem Interface auf, brauchen Transformationen weiterhin die alte Funktion auf Paketebene. Ob generische Methoden deiner Codebasis helfen, hängt vor allem davon ab, also prüf deine Interfaces, bevor du ein Refactoring planst. Wenn du das üben willst, behandelt die Go Generics Masterclass Constraints und Inferenz in Übungen direkt im Browser.

Fazit: jetzt einsetzen, für konkrete Typen. APIs, die auf Interfaces aufbauen, lässt du in Ruhe.

Ein uuid-Paket in der Standardbibliothek

Fast jeder Go-Service, der mit einer Datenbank spricht, importiert github.com/google/uuid. Go 1.27 bringt ein Paket in der Standardbibliothek mit dem Importpfad "uuid", basierend auf Proposal #62026.

UUID ist als [16]byte definiert. Werte lassen sich also mit == vergleichen und direkt als Map-Keys verwenden. Drei Generatoren sind dabei:

example.gogo
package main

import (
    "fmt"
    "uuid"
)

func main() {
    fmt.Println(uuid.New())   // reach for this when you don't care how it's made
    fmt.Println(uuid.NewV4()) // 128 bits, 122 of them random
    fmt.Println(uuid.NewV7()) // 48-bit timestamp first, so values sort by creation time

    requestID, err := uuid.Parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
    if err != nil {
        return
    }
    fmt.Println(requestID, requestID == uuid.Nil()) // false, it parsed fine
}

New ist der Standard und liefert derzeit eine V4. NewV4 ist vollständig zufällig, niemand kann also die nächste erraten. Das willst du bei einer öffentlichen Request-ID. NewV7 stellt einen 48-Bit-Zeitstempel an den Anfang, sodass neuere Werte nach älteren sortiert werden. Das macht sie zu guten Primärschlüsseln, weil Inserts nahe am Ende des Index landen statt verstreut darin.

Beachte, dass Nil() und Max() Funktionen sind, der Vergleich lautet also id == uuid.Nil() mit Klammern. Nil() dient als Sentinel-Wert für „noch nicht gesetzt“. Max() ist der Wert mit lauter Einsen und eignet sich als Obergrenze, wenn du einen Bereich zeitlich sortierter V7-Keys scannst.

Fazit: jetzt einsetzen für neuen Code. Einen bestehenden Service zu migrieren ist ein simples Suchen und Ersetzen, aber lies die FAQ, bevor du die Abhängigkeit entfernst.

Musst du für encoding/json/v2 etwas ändern?

Nein. Genau darum ging es beim Design. encoding/json basiert jetzt auf der v2-Implementierung, und die Release Notes sagen ausdrücklich: „Das Verhalten beim Marshaling und Unmarshaling bleibt erhalten, der genaue Text von Fehlermeldungen kann sich aber unterscheiden.“ Du bekommst die Geschwindigkeit der neuen Engine, ohne Code zu ändern. Laut Release Notes gilt: „Die Marshal-Performance liegt im Großen und Ganzen auf dem Niveau der bisherigen Implementierung, Unmarshal ist dagegen deutlich schneller.“

Neues Verhalten bekommst du nur, wenn du encoding/json/v2 explizit importierst. Dieses Paket hat strengere Standardwerte, gewählt nach dem, was andere JSON-Implementierungen erwarten: Es lehnt ungültiges UTF-8 in JSON-Strings ab, lehnt doppelte Namen innerhalb eines JSON-Objekts ab, gleicht Feldnamen unter Beachtung der Groß- und Kleinschreibung ab, serialisiert einen nil-Slice als [] statt null und gibt Maps in nicht deterministischer Reihenfolge aus, wo v1 eine deterministische garantiert hat. Die Änderung beim nil-Slice ist die Unterscheidung zwischen nil und leer aus var vs make in Go, und unter v2 siehst du sie in der Ausgabe.

example.gogo
import "encoding/json/v2"

type Event struct {
    ID     string `json:"id"`
    Action string `json:"action"`
}

// v1 would match the key "ID" against the tag `json:"id"`. v2 won't,
// and it skips the mismatched key silently rather than erroring.
var e Event
json.Unmarshal([]byte(`{"ID":"evt_01H","action":"checkout"}`), &e)
fmt.Println(e.ID, e.Action) // "" checkout

// v2 marshals maps in a non-deterministic order. Ask for a stable one
// explicitly when you hash or snapshot the output.
inventory := map[string]int{"widget": 12, "gadget": 3, "gizmo": 7}
b, _ := json.Marshal(inventory, json.Deterministic(true))

Um unter v2 ein v1-Verhalten zurückzubekommen, wechselst du nicht den Import. Du übergibst eine Option für genau dieses Verhalten: Deterministic(true) für eine stabile Map-Reihenfolge, MatchCaseInsensitiveNames(true) für einen toleranten Abgleich der Feldnamen, FormatNilSliceAsNull(true) für null statt []. Das v1-Paket hat dieselben Optionen bekommen, du kannst die v2-Semantik also Verhalten für Verhalten übernehmen, ohne komplett zu migrieren. Die vollständige Liste findest du unter Migrating to v2 in der Dokumentation des v1-Pakets.

Das dritte Paket, encoding/json/jsontext, kümmert sich um die Syntax auf niedrigerer Ebene. Es stellt JSON als Folge von Tokens und Werten dar, mit einer State Machine, die die Ausgabe gültig hält. Streaming-Codecs bauen darauf auf. Der meiste Anwendungscode importiert es nie direkt.

So entscheidest du, welches du importierst:

flowchart TD
    A["Which JSON import?"] --> B{"Upgrading existing code?"}
    B -- Yes --> C["encoding/json<br/>v2 engine, v1 behavior"]
    B -- No --> D{"Want strict defaults<br/>and faster decoding?"}
    D -- No --> C
    D -- Yes --> E["encoding/json/v2"]
    E --> F{"Need raw tokens<br/>or streaming syntax?"}
    F -- Yes --> G["encoding/json/jsontext"]
    F -- No --> H["You're done"]

Entscheidungsbaum für die Wahl zwischen encoding/json, encoding/json/v2 und encoding/json/jsontext in Go 1.27.

Fazit: den Standard jetzt übernehmen (das hast du schon), v2 später explizit importieren, sobald du die Migrationshinweise für deine Struct-Tags gelesen hast. Der Kurs Data Formats behandelt die Tag-Regeln, die bestimmen, wie stark dich das betrifft.

Post-Quanten-Signaturen mit crypto/mldsa

RSA und ECDSA sind heute sicher, weil kein Computer schnell genug große Zahlen faktorisieren oder diskrete Logarithmen berechnen kann. Ein ausreichend großer Quantencomputer könnte das. crypto/mldsa implementiert ML-DSA aus FIPS 204 in drei Parametersätzen, 44, 65 und 87, basierend auf Proposal #77626.

Andere Pakete unterstützen es bereits. crypto/x509 parst und verifiziert ML-DSA-Schlüssel und -Signaturen, und crypto/tls akzeptiert alle drei Parametersätze in einem TLS-1.3-Handshake. MLKEM1024 kommt zu den unterstützten Schlüsselaustauschverfahren für quantensichere Schlüsselvereinbarung hinzu. Du aktivierst es, indem du es zu Config.CurvePreferences hinzufügst.

Ein vollständiges Signieren und Verifizieren für ein Firmware-Release:

example.gogo
sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
    return err
}

firmware, err := os.ReadFile("firmware-v2.1.0.bin")
if err != nil {
    return err
}

// Context is a domain-separation label. Sign and verify must pass the same one.
opts := &mldsa.Options{Context: "acme/firmware-release"}
sig, err := sk.Sign(nil, firmware, opts)
if err != nil {
    return err
}

fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // true
fmt.Println(mldsa.MLDSA65().SignatureSize())                         // 3309

firmware[0] ^= 1 // flip one bit
fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // false

Verify gibt nil zurück, wenn die Signatur gültig ist, der Vergleich mit nil liefert dir also einen Boolean. Der Nachteil ist die Größe, und der Unterschied ist groß:

Horizontales Balkendiagramm, das die Größe digitaler Signaturen in Bytes vergleicht. Ed25519 hat 64 Bytes, ECDSA P-256 etwa 71 Bytes, RSA-2048 256 Bytes, ML-DSA-44 2.420 Bytes, ML-DSA-65 3.309 Bytes und ML-DSA-87 4.627 Bytes.

Quelle: Signaturgrößen laut FIPS 204 (ML-DSA) und den Spezifikationen der jeweiligen Algorithmen.

Eine ML-DSA-65-Signatur ist etwa 46-mal so groß wie eine Ed25519-Signatur, sie lohnt sich also nur dort, wo die Signatur lange halten muss. Firmware und Release-Artefakte, denen ein Gerät auch in zehn Jahren noch vertrauen muss, sind die 3 KB wert. Das gilt auch für Root-Zertifikate, die die heutige Kryptografie überdauern sollen, und für TLS-Server, deren Schlüssel du nicht neu ausstellen willst, sobald Quantencomputer auftauchen. Bei kurzlebigen Session-Tokens, von denen du Tausende pro Sekunde erzeugst, ist die zusätzliche Größe Overhead, den du noch nicht brauchst.

Eine davon unabhängige Änderung an crypto/x509 betrifft denselben Bereich. SystemCertPool berücksichtigt SSL_CERT_FILE und SSL_CERT_DIR jetzt auch unter Windows und Darwin, nicht nur unter Linux. Sind diese Variablen gesetzt, lädt Go die Root-Zertifikate von der Festplatte und nutzt seinen eigenen Verifier statt der Plattform-APIs. Mit GODEBUG=x509sslcertoverrideplatform=0 behältst du das alte Verhalten.

Fazit: für langlebige Signaturen einsetzen, für alles andere ignorieren.

Das experimentelle simd-Paket

Eine normale CPU-Instruktion arbeitet mit einem Wert. Eine SIMD-Instruktion arbeitet in einem Schritt mit einem ganzen Vektor von Werten, dasselbe Ergebnis braucht also deutlich weniger Instruktionen. Das hilft, wenn du dieselbe Berechnung über einen großen Slice aus Audio-Samples oder Bildpixeln laufen lässt oder ein Skalarprodukt berechnest. Code, der hauptsächlich Structs und Strings hin und her schiebt, profitiert nicht davon.

Jede Architektur stellt SIMD anders bereit. Das neue Paket simd ist portabel und setzt keine bestimmte Vektorgröße voraus. Es ist auf allen Architekturen verfügbar und nutzt Hardware-Instruktionen, wo es sie gibt. Du aktivierst es beim Build mit GOEXPERIMENT=simd. Es ist experimentell, die API kann sich also ändern.

Zwei Audiospuren mischen, mehrere Samples pro Schritt:

example.gogo
//go:build goexperiment.simd

// out[i] = trackA[i] + trackB[i], several samples at a time.
out := make([]float32, len(trackA))

lanes := simd.LoadFloat32s(trackA).Len() // how many float32s fit in one vector
for i := 0; i+lanes <= len(trackA); i += lanes {
    va := simd.LoadFloat32s(trackA[i:])
    vb := simd.LoadFloat32s(trackB[i:])
    va.Add(vb).Store(out[i:])
}
// A plain scalar loop handles the leftover tail.

Len meldet, wie viele Elemente auf der Maschine, auf der dein Code gerade läuft, in einen Vektor passen, deshalb muss die Schleife nie eine Vektorbreite fest einprogrammieren. Jeder Schritt lädt so viele Samples aus beiden Spuren, addiert sie elementweise in einer einzigen Operation und speichert das Ergebnis.

Fazit: vorerst ignorieren, außer du profilst bereits eine numerische Hot Loop. Die API steckt hinter einem GOEXPERIMENT und kann sich noch ändern.

Mehr Performance allein durch das Upgrade

Manche Beschleunigungen brauchen nichts als einen neuen Build. Die größte ist die größenspezialisierte Allokation, die in den Release Notes so beschrieben wird:

Diagramm zum größenspezialisierten Allocator in Go 1.27. Allokationen unter 80 Bytes werden bis zu 30 Prozent günstiger, reale allokationslastige Programme verbessern sich insgesamt um etwa 1 Prozent, und die Binärgröße wächst um feste 60 Kilobyte.

Quelle: Release Notes zu Go 1.27, schnellere Speicherallokation.

Die beiden Zahlen messen verschiedene Dinge. Die 30 % beziehen sich auf die Kosten eines einzelnen kleinen Allokationsaufrufs. Die ~1 % sind das, was ein ganzer Service merkt. Stößt du auf eine Regression, schaltest du das mit GOEXPERIMENT=nosizespecializedmalloc ab, und dieser Notausgang soll in Go 1.28 verschwinden.

Außerdem sind drei Compiler-Optimierungen standardmäßig aktiv. Ein Known-Bits-Datenflusspass verfolgt, welche Bits eines Werts nachweislich 0 oder 1 sind, und entfernt die daraus entstehende Redundanz. Loop-Invariant Code Motion zieht Berechnungen, deren Ergebnis sich nie ändert, aus dem Schleifenrumpf heraus, sodass sie einmal statt in jeder Iteration laufen. Und switch-Anweisungen werden jetzt zu Lookup-Tabellen kompiliert, wo die Cases es zulassen, auch mit fallthrough. Sie springen dann direkt zum passenden Case, statt jeden einzeln zu prüfen.

compress/flate wird ebenfalls schneller, mit einer Einschränkung: Die exakte kodierte Ausgabe kann sich von Go 1.26 unterscheiden. DEFLATE liegt unter archive/zip, compress/gzip, compress/zlib und image/png, byte-genaue Golden Tests über jedes davon können also scheitern. Die Ausgabe ist trotzdem korrekt, aktualisiere also die Fixtures.

Fazit: jetzt einsetzen. Du bekommst es mit dem Upgrade ohnehin.

Goroutine-Leaks finden und Tracebacks mit Labels lesen

Das Profil goroutineleak ist vom Experiment zur allgemeinen Verfügbarkeit aufgestiegen, und das GOEXPERIMENT goroutineleakprofile ist gelöscht. Das Profil steht über runtime/pprof und als net/http/pprof-Endpoint /debug/pprof/goroutineleak bereit.

Eine geleakte Goroutine blockiert an einem Nebenläufigkeitsprimitiv, das sich nie wieder lösen kann. Die Runtime findet solche Goroutinen über den Garbage Collector: Wenn Goroutine G an Primitiv P blockiert und P weder von einer lauffähigen Goroutine noch von etwas erreichbar ist, das diese entsperren könnten, dann kann G nie wieder aufwachen.

example.gogo
func startJob() {
    result := make(chan int) // unbuffered: the send waits for a receiver
    go func() {
        result <- expensiveWork() // blocks forever, nobody receives
    }()
    // returns without receiving, so result becomes unreachable
}

func main() {
    startJob()
    runtime.GC() // the detector scans during a GC cycle
    pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
    // goroutineleak profile: total 1
    //   ... main.startJob.func1 ... main.go:14
}

Dieser Ansatz hat einen blinden Fleck. Die Runtime kann Leaks übersehen, bei denen das blockierende Primitiv über eine globale Variable oder über die lokalen Variablen einer lauffähigen Goroutine erreichbar ist. Ein Channel, der in einer Registry auf Paketebene liegt, gilt als erreichbar, das Profil meldet ihn also nicht. Concurrency Fundamentals geht die Leak-Muster durch, die solche Fälle erzeugen, und zeigt, wie du sie mit context schließt.

Die zweite Verbesserung beim Debugging braucht überhaupt keinen Code. Bei Modulen, deren go-Direktive 1.27 oder neuer ist, enthalten Tracebacks jetzt die Goroutine-Labels aus runtime/pprof in der Kopfzeile. Die Labels, die du fürs Profiling ohnehin setzt, tauchen in Crash-Dumps, SIGQUIT-Traces und in der Ausgabe von runtime.Stack auf. Haben zwei Goroutinen identische Stacks, ist das Label oft die einzige Möglichkeit, sie auseinanderzuhalten:

example.gogo
pprof.Do(ctx, pprof.Labels("request", id), func(ctx context.Context) {
    work()
})

// goroutine 34 [running]:
// labels: {"request":"req-42"}
// main.handle.func1 ...

Mit GODEBUG=tracebacklabels=0 schaltest du das ab, falls deine Labels etwas enthalten, das nicht in einem Crash-Dump landen soll.

Fazit: jetzt einsetzen. Keins von beiden kostet dich etwas.

Kleinere Verbesserungen an Sprache und Bibliotheken

Feldselektoren in Struct-Literalen

Ein Key in einem Struct-Literal darf jetzt jeder gültige Feldselektor für den Typ sein, nicht nur ein Feldname der obersten Ebene (Proposal #9859). Wenn du ein gemeinsames Model einbettest, musst du das verschachtelte Literal nicht mehr ausschreiben:

example.gogo
type Model struct {
    ID        int64
    CreatedAt time.Time
}

type Post struct {
    Model
    Author string
    Likes  int
}

p := Post{Model: Model{ID: 42}, Author: "Patrik"} // before
p := Post{ID: 42, Author: "Patrik"}               // Go 1.27

Verallgemeinerte Typinferenz für Funktionen

Die Typinferenz für Funktionen greift jetzt in jedem Kontext, in dem eine generische Funktion einem passenden Funktionstyp zugewiesen oder in ihn konvertiert wird (Proposal #77245). Bisher funktionierte sie nur in einer typisierten Variablendeklaration:

example.gogo
func ascending[T cmp.Ordered](a, b T) int  { return cmp.Compare(a, b) }
func descending[T cmp.Ordered](a, b T) int { return cmp.Compare(b, a) }

orders := []func(int, int) int{ascending[int], descending[int]} // before
orders := []func(int, int) int{ascending, descending}           // Go 1.27

Der Elementtyp des Slices ist func(int, int) int, also inferiert Go T als int. Dasselbe funktioniert jetzt auch bei Konvertierungen und wenn du eine generische Funktion ohne Instanziierung als Argument übergibst.

strings.CutLast und bytes.CutLast

Cut trennt am ersten Separator. CutLast trennt am letzten und gibt den Teil davor, den Teil danach und die Information zurück, ob der Separator gefunden wurde (Proposal #71151):

example.gogo
dir, file, found := strings.CutLast("internal/store/user.go", "/")
fmt.Println(dir, file, found) // internal/store user.go true

Bisher hast du LastIndex aufgerufen, auf -1 geprüft und zweimal von Hand gesliced.

Rand.N, maphash.Hasher und big.Int.Divide

math/rand/v2 bekommt Rand.N als generische Methode (Proposal #77853). Damit ziehst du einen begrenzten Zufallswert aus einer Quelle, die du selbst geseedet hast, statt aus der globalen, die sich nicht seeden lässt. Ein fehlschlagender Test läuft dann mit denselben Eingaben erneut.

hash/maphash bekommt ein generisches Interface Hasher[T] für hashbasierte Strukturen wie eigene Maps und Bloom-Filter (Proposal #70471). Es kombiniert eine Hashfunktion mit einer Gleichheitsprüfung, und gleiche Werte müssen denselben Hash ergeben. ComparableHasher[T] ist die fertige Implementierung für vergleichbare Typen und vergleicht mit ==. go/types nutzt das Interface bereits und liefert einen Hasher mit, der die Identical-Relation respektiert.

math/big.Int bekommt eine Methode Divide, die Quotient und Rest zusammen berechnet, mit einem Rundungsmodus deiner Wahl: Trunc, Floor, Round oder Ceil. Quo und Mod runden immer in Richtung null, Finanz- und Numerikcode, der eine andere Rundung braucht, hat damit jetzt eine eingebaute Option.

Unicode 17, database/sql und der Rest

Das Paket unicode und alles, was darauf aufbaut, springt direkt von Unicode 15 auf Unicode 17, sodass Zeichen aus den beiden Versionen dazwischen jetzt korrekt klassifiziert werden. database/sql hat ConvertAssign bekommen. Damit können Treiber die Typkonvertierungen wiederverwenden, die Rows.Scan durchführt, statt eigene zu schreiben.

Der Rest ist klein:

  • crypto ergänzt den Hashwert MLDSAMu, einen Signalisierungsmechanismus für ML-DSA-Signaturen mit externem mu.
  • crypto/ecdsa prüft jetzt in PrivateKey.Sign, ob die Hashlänge stimmt, wenn du SignerOpts ungleich nil übergibst.
  • crypto/x509 stellt RawSignatureAlgorithm auf Certificate, CertificateRequest und RevocationList bereit und liefert dir damit den DER-kodierten AlgorithmIdentifier, selbst wenn SignatureAlgorithm den Wert UnknownSignatureAlgorithm hat. Beim Parsen in pkix.Name werden außerdem mehr Werttypen akzeptiert, unbekannte landen in asn1.RawValue.
  • crypto/tls ergänzt ConnectionState.LocalCertificate, also die Zertifikatskette, die du dem Peer präsentiert hast, sowie QUICConfig.ClientHelloInfoConn. Config.Rand ist zugunsten von testing/cryptotest.SetGlobalRandom für deterministische Tests als deprecated markiert.
  • net lässt die Lesemethoden von UnixConn direkt io.EOF zurückgeben, statt es in einen net.OpError zu verpacken.
  • runtime/secret überträgt den Secret-Modus auf Goroutinen, die darin gestartet werden.
  • go/constant ergänzt StringLen, go/scanner ergänzt Scanner.End, und go/token gibt File eine String-Methode.
  • Bei den Ports wechselt der Big-Endian-Port für 64-Bit-PowerPC unter Linux auf das ELFv2-System-ABI. Das ermöglicht dort cgo, positionsunabhängige Executables und externes Linken und setzt einen Linux-Kernel ab Version 3.13 voraus. Unter Plan 9 ist syscall.Errno jetzt definiert und implementiert error, sodass portabler Code, der darauf verweist, ohne Build Constraints baut. Der Linker akzeptiert außerdem -macos und -macsdk, um die Versionen festzulegen, die in den macOS-Load-Command LC_BUILD_VERSION geschrieben werden.

Was hat sich an der Go-Toolchain geändert?

go fix hat vier Modernizer dazubekommen. embedlit vereinfacht Verweise auf eingebettete Felder in zusammengesetzten Literalen, was die neue Regel für Struct-Literale möglich macht. atomictypes ersetzt Basistypen in sync/atomic-Aufrufen durch atomare Typen, slicesbackward schreibt rückwärts laufende Schleifen auf slices.Backward um, und unsafefuncs ersetzt unsichere Pointer-Arithmetik durch Funktionsaufrufe. Dazu kamen zwei Aufräumarbeiten: fmtappendf wurde aus stilistischen Gründen entfernt, und waitgroup heißt jetzt waitgroupgo. Führ es einmal aus, nachdem du die go-Direktive angehoben hast:

example.bashbash
go fix -diff ./...   # preview
go fix ./...         # apply

Den kompletten Katalog dessen, was go fix umschreibt, findest du unter alle Modernizer in go fix 1.26.

go test führt jetzt standardmäßig den vet-Check stdversion aus. Er meldet Symbole der Standardbibliothek, die neuer sind als die Go-Version, die für die Datei gilt, festgelegt durch die Direktive in deiner go.mod und durch Build-Tags. Deklariert dein Modul go 1.25 und jemand verwendet strings.CutLast, fängt der Testlauf das ab, bevor ein Nutzer auf 1.25 darauf stößt. go test -json versieht "Action":"output"-Zeilen außerdem mit einem optionalen Feld "OutputType", derzeit error, error-continue oder frame. Das ist nützlich, wenn du Testausgaben in der CI parst.

go doc akzeptiert jetzt die Syntax package@version, go doc rsc.io/[email protected] zeigt also die Doku eines exakten Releases, ohne dass du es auschecken musst. Ein neues Flag -ex listet die ausführbaren Examples eines Pakets auf, und wenn du eines davon angibst, wird sein Quelltext ausgegeben.

go mod tidy erzwingt für Module ab go 1.27 ein Layout mit zwei Blöcken und führt verstreute require-Blöcke zu einem Block für direkte und einem für indirekte Abhängigkeiten zusammen. Kommentarblöcke, die zu Abhängigkeiten gehören, bleiben erhalten, und ein Kommentar über eine gemischte Gruppe wandert in den direkten Block.

go tool trace -http=:6060 bindet jetzt nur noch an localhost, genau wie go tool pprof. Übergib also eine vollständige Adresse wie -http=0.0.0.0:6060, wenn der Server von einem anderen Rechner aus erreichbar sein soll. Die Tools compile, link, asm, cgo, cover und pack akzeptieren außerdem Response Files (@file) in einem GCC-kompatiblen Format. Das hilft Build-Systemen, die die Längengrenzen der Kommandozeile überschreiten.

Testen und net/http

httptest.NewTestServer erstellt einen Server auf einem simulierten In-Memory-Netzwerk, gedacht für den Einsatz mit testing/synctest, es ist also kein echter TCP-Port im Spiel. Das Gegenstück ist synctest.Sleep, das time.Sleep und synctest.Wait in einem Aufruf erledigt: Es stellt die simulierte Uhr vor und lässt die Goroutinen dann zur Ruhe kommen. Mit beiden hängen HTTP-Tests nicht mehr vom Timing ab. Professional Go Testing behandelt das synctest-Modell, auf dem beide aufbauen.

example.gogo
srv := httptest.NewTestServer(t, handler) // in-memory, cleanup auto-registered

Auf der Seite von net/http akzeptiert der HTTP/2-Server jetzt Prioritätssignale von Clients nach RFC 9218 und bedient Streams mit höherer Priorität zuerst. Mit Server.DisableClientPriority = true bekommst du das alte Round-Robin-Verhalten.

Die Änderung, die dir am ehesten auffällt, liegt bei HTTP/1: Wenn du einen teilweise gelesenen Response.Body schließt, wird der restliche Inhalt jetzt bis zu einem konservativen Limit ausgelesen, damit die Verbindung wiederverwendet werden kann. Für die meisten Programme ändert das nichts oder bringt einen kleinen Geschwindigkeitsgewinn. Wenn du früh schließt, um einen großen Download abzubrechen, schaltest du das mit Transport.DisableKeepAlives = true ab.

Server.MaxHeaderValueCount begrenzt, wie viele Werte ein einzelner Header haben darf, was vor Requests schützt, die einen Header mit Werten fluten. Transport und Server können TLS-ALPN auf einer selbst bereitgestellten net.Conn aushandeln, solange diese ConnectionState() tls.ConnectionState implementiert. So wird HTTP/2 auch über getunnelte oder per Proxy weitergeleitete Verbindungen genutzt. Und net/url hat URL.Clone und Values.Clone für tiefe Kopien bekommen. HTTP and Networking behandelt die Einstellungen von Server und Transport, auf denen diese Neuerungen aufbauen.

Fazit: jetzt einsetzen. Wenn du eine flaky HTTP-Test-Suite hast, sind httptest und synctest Grund genug für das Upgrade.

Was geht beim Upgrade auf Go 1.27 kaputt?

Die Release Notes verteilen die Breaking Changes auf acht Abschnitte. Diese Tabelle fasst sie zusammen, sortiert danach, wie wahrscheinlich sie dich treffen.

Was sich ändertWen es trifftWas zu tun ist
compress/flate erzeugt andere BytesByte-genaue Golden Tests über gzip-, zlib-, zip- oder png-AusgabenAktualisiere die Fixtures. Die Kompression ist korrekt, nur anders.
Einfachere Namen für Funktionsliterale (Closures)Tests, die auf Symbolnamen prüfen, und Code, der Code-Pointer von Funktionen auf Gleichheit vergleichtVerlass dich nicht mehr auf die Namen von Literalen. Der Pointer-Vergleich von Funktionen war schon vorher als unzuverlässig dokumentiert.
GODEBUGs asynctimerchan und gotypesalias endgültig entferntAlles, was sie in go.mod oder einer //go:debug-Zeile auf den alten Wert festlegtVor dem Upgrade grep -r asynctimerchan . ausführen. Mit dem finalen Standardwert baut es weiterhin, mit dem alten Wert schlägt der Build fehl.
Fünf TLS/x509-GODEBUGs entfernt: tlsunsafeekm, tlsrsakex, tls3des, tls10server, x509keypairleafServices, die noch auf altes TLS-Verhalten festgelegt sindDieselbe Regel wie oben: Der go-Befehl akzeptiert den finalen Standardwert und lehnt den alten ab, du findest diese Stellen also beim Build.
macOS 13 Ventura ist die MindestversionCI-Runner und Entwicklungsrechner mit älterem macOSAktualisiere das Runner-Image. Angekündigt in den Release Notes zu Go 1.26.
Unterstützung für bzr aus dem go-Befehl entferntModule, die auf Bazaar-Servern gehostet werdenSpiegle die Abhängigkeit oder vendore sie.
Neue Direktive //go:linknamestd markiert Linknames, die der Standardbibliothek vorbehalten sind, der Linker prüft jetzt den Linkname-Zugriff auf Assembly-Symbole, und Typdeskriptoren liegen jetzt in einer Sektion .go.typePakete, die über ein nicht vorgesehenes //go:linkname in die Runtime greifen, egal ob deine eigenen oder die einer AbhängigkeitAktualisiere die Abhängigkeit. Das schlägt beim Build fehl, und zwar unübersehbar. Nichts davon steht in den Release Notes, du merkst es also erst beim Build.
HTTP/1 Response.Body.Close liest ungelesenen Inhalt ausCode, der früh schließt, um einen großen Download abzubrechenSetz für diese Clients Transport.DisableKeepAlives = true.
Text von json/v2-Fehlermeldungen unterscheidet sichTests, die auf exakte JSON-Fehlerstrings prüfenPrüf auf den Fehlertyp oder einen Teilstring statt auf die komplette Meldung.

Die letzte Zeile ist am schwersten zu erkennen. Das Verhalten bleibt erhalten, der Text aber nicht, deshalb schlägt eine Test-Suite, die Unmarshal-Fehler per String-Vergleich prüft, ohne erkennbare Ursache fehl. Grenz das mit zwei Durchläufen auf einem Branch ein, der go.mod bereits auf go 1.27 angehoben hat:

example.bashbash
go test ./... > default.txt 2>&1
GOEXPERIMENT=nojsonv2 go test ./... > nojsonv2.txt 2>&1
diff default.txt nojsonv2.txt

Alles, was im Diff auftaucht, stammt von der JSON-Änderung. Alles, was in beiden Durchläufen fehlschlägt, ist etwas anderes aus der Tabelle. GOEXPERIMENT=nojsonv2 soll in einem künftigen Release entfernt werden, nutz es also zur Diagnose und korrigiere dann die Tests.

Häufig gestellte Fragen

Muss ich meinen JSON-Code für Go 1.27 neu schreiben?

Nein. Das Paket encoding/json ist jetzt auf Basis von v2 implementiert, aber das Verhalten beim Marshaling und Unmarshaling bleibt erhalten, und die v1-API wird weiterhin unterstützt. Der einzige beobachtbare Unterschied: Der genaue Text von Fehlermeldungen kann sich ändern. Die strengeren Standardwerte von v2 bekommst du nur, wenn du encoding/json/v2 explizit importierst.

Kann eine generische Methode ein Interface erfüllen?

Nein. Laut Release Notes dürfen Interface-Methoden keine Typparameter deklarieren, und Interface-Methoden können nicht durch generische Methoden implementiert werden. Interface-Dispatch wird zur Laufzeit aufgelöst, daher kann der Compiler nicht wissen, welche Instanziierungen er für einen dynamisch dispatchten Aufruf erzeugen soll. Baut deine API auf Interfaces auf, bleib bei generischen Funktionen auf Paketebene.

Sollte ich github.com/google/uuid durch das Paket aus der Standardbibliothek ersetzen?

Bei neuem Code ja. Bei bestehendem Code prüfst du vorher zwei Dinge. Das UUID der Standardbibliothek ist ein [16]byte mit eigenem Methodensatz, daher muss jeder Code geprüft werden, der die zusätzlichen Methoden des Drittanbieter-Typs nutzt. Und jede Abhängigkeit, die das Drittanbieter-uuid.UUID in ihrer API exportiert, hält dieses Modul ohnehin in deinem Abhängigkeitsgraphen.

Ist crypto/mldsa produktionsreif?

Es ist ein stabiles Paket der Standardbibliothek, das FIPS 204 implementiert und in crypto/x509 und crypto/tls integriert ist. Die praktische Einschränkung ist die Signaturgröße, nicht die Reife: ML-DSA-65-Signaturen haben 3.309 Bytes, Ed25519-Signaturen nur 64. Setz es dort ein, wo die Signatur die heutige Kryptografie überdauern muss, und verzichte bei großen Mengen kurzlebiger Tokens darauf.

Wie finde ich am schnellsten heraus, was Go 1.27 in meiner Codebasis kaputt macht?

Heb go.mod auf einem Branch auf go 1.27 an und such dann per grep nach asynctimerchan, gotypesalias und den fünf entfernten TLS-Einstellungen, denn die lassen den Build direkt fehlschlagen. Lass deine Suite danach zweimal laufen, einmal normal und einmal mit GOEXPERIMENT=nojsonv2, und vergleich die Fehlschläge per Diff. So trennst du Abweichungen im JSON-Fehlertext von Fehlern bei Golden Files und Symbolnamen.

Quellen

In diesem Beitrag zitierte Primärquellen (zuletzt geprüft am 23. August 2026):

Weiterlernen

Generische Methoden lassen sich leichter gut einsetzen, wenn sich Constraints und Inferenz natürlich anfühlen, und genau diese Teile überfliegt man oft, wenn man Generics zum ersten Mal lernt. Die Go Generics Masterclass behandelt Constraints, Type Sets und Inferenz in Übungen im Browser, die auf der aktuellen Toolchain laufen. So probierst du die Methodensyntax aus 1.27 in echtem Code aus.

Neu bei Go? Starte mit dem Lernpfad Go Fundamentals und komm danach für die Release Notes zurück. Und falls du das Release vom letzten Jahr ausgelassen hast: Was ist neu in Go 1.26 behandelt errors.AsType, den Garbage Collector Green Tea und new(expr).

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen