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
- Ein uuid-Paket in der Standardbibliothek
- Musst du für encoding/json/v2 etwas ändern?
- Post-Quanten-Signaturen mit crypto/mldsa
- Das experimentelle simd-Paket
- Mehr Performance allein durch das Upgrade
- Goroutine-Leaks finden und Tracebacks mit Labels lesen
- Kleinere Verbesserungen an Sprache und Bibliotheken
- Was hat sich an der Go-Toolchain geändert?
- Testen und net/http
- Was geht beim Upgrade auf Go 1.27 kaputt?
- Häufig gestellte Fragen
- Quellen
- Weiterlernen
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.gogotype 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.gogotype 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.gogopackage 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.gogoimport "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.gogosk, 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ß:

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:

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.gogofunc 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.gogopprof.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.gogotype 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.gogofunc 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.gogodir, 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:
cryptoergänzt den HashwertMLDSAMu, einen Signalisierungsmechanismus für ML-DSA-Signaturen mit externem mu.crypto/ecdsaprüft jetzt inPrivateKey.Sign, ob die Hashlänge stimmt, wenn duSignerOptsungleich nil übergibst.crypto/x509stelltRawSignatureAlgorithmaufCertificate,CertificateRequestundRevocationListbereit und liefert dir damit den DER-kodierten AlgorithmIdentifier, selbst wennSignatureAlgorithmden WertUnknownSignatureAlgorithmhat. Beim Parsen inpkix.Namewerden außerdem mehr Werttypen akzeptiert, unbekannte landen inasn1.RawValue.crypto/tlsergänztConnectionState.LocalCertificate, also die Zertifikatskette, die du dem Peer präsentiert hast, sowieQUICConfig.ClientHelloInfoConn.Config.Randist zugunsten vontesting/cryptotest.SetGlobalRandomfür deterministische Tests als deprecated markiert.netlässt die Lesemethoden vonUnixConndirektio.EOFzurückgeben, statt es in einennet.OpErrorzu verpacken.runtime/secretüberträgt den Secret-Modus auf Goroutinen, die darin gestartet werden.go/constantergänztStringLen,go/scannerergänztScanner.End, undgo/tokengibtFileeineString-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.Errnojetzt definiert und implementierterror, sodass portabler Code, der darauf verweist, ohne Build Constraints baut. Der Linker akzeptiert außerdem-macosund-macsdk, um die Versionen festzulegen, die in den macOS-Load-CommandLC_BUILD_VERSIONgeschrieben 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.bashbashgo 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.gogosrv := 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 ändert | Wen es trifft | Was zu tun ist |
|---|---|---|
compress/flate erzeugt andere Bytes | Byte-genaue Golden Tests über gzip-, zlib-, zip- oder png-Ausgaben | Aktualisiere 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 vergleicht | Verlass 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 entfernt | Alles, was sie in go.mod oder einer //go:debug-Zeile auf den alten Wert festlegt | Vor 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, x509keypairleaf | Services, die noch auf altes TLS-Verhalten festgelegt sind | Dieselbe 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 Mindestversion | CI-Runner und Entwicklungsrechner mit älterem macOS | Aktualisiere das Runner-Image. Angekündigt in den Release Notes zu Go 1.26. |
Unterstützung für bzr aus dem go-Befehl entfernt | Module, die auf Bazaar-Servern gehostet werden | Spiegle 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.type | Pakete, die über ein nicht vorgesehenes //go:linkname in die Runtime greifen, egal ob deine eigenen oder die einer Abhängigkeit | Aktualisiere 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 aus | Code, der früh schließt, um einen großen Download abzubrechen | Setz für diese Clients Transport.DisableKeepAlives = true. |
| Text von json/v2-Fehlermeldungen unterscheidet sich | Tests, die auf exakte JSON-Fehlerstrings prüfen | Prü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.bashbashgo 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):
- Go 1.27 Release Notes, abgerufen am 23.08.2026
- Interaktive Tour durch Go 1.27, VictoriaMetrics, abgerufen am 23.08.2026
- Dokumentation des Pakets
uuid, abgerufen am 23.08.2026 - Dokumentation des Pakets
encoding/json/v2, abgerufen am 23.08.2026 encoding/json: Migrating to v2, die vollständige Liste der Verhaltensunterschiede, abgerufen am 23.08.2026- Dokumentation des Pakets
crypto/mldsa, abgerufen am 23.08.2026 - Dokumentation des Pakets
simd, abgerufen am 23.08.2026 - Dokumentation des Pakets
net/http/httptest, abgerufen am 23.08.2026 - NIST FIPS 204: Module-Lattice-Based Digital Signature Standard, abgerufen am 23.08.2026
- Dokumentation des modernize-Analyzers in
x/tools, abgerufen am 23.08.2026 - Release Notes zu Unicode 17.0.0, abgerufen am 23.08.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).
