Go 1.27, of hoe ik leerde mij geen zorgen meer te maken en generieke methoden te omarmen


Go 1.27 verschijnt in augustus 2026, en deze keer is de taal zelf veranderd. Niet "we hebben een nieuwe functie aan slices toegevoegd" veranderd, maar echt veranderd. Generieke methoden zijn er nu. Een elf jaar oud issue is gesloten. encoding/json is ongemerkt vervangen door een nieuwe engine, terwijl het vliegtuig in de lucht was.
Elk voorbeeld hieronder is uitgevoerd op go1.27rc3 (darwin/arm64). Elk uitvoerblok is echte uitvoer, gekopieerd en geplakt, inclusief de foutmeldingen. Als iets vreemd lijkt, komt dat doordat dat is wat de compiler daadwerkelijk heeft gezegd.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. De taal is veranderd (ja, echt)
1.1. Generieke methoden
Sinds Go 1.18 hebben we generics, en sinds Go 1.18 hebben we ook The Conversation. Die verloopt als volgt:
"Laat me gewoon een
Map-methode schrijven op mijn slice-type—"
method must have no type parameters"...prima. Dan wordt het een functie op pakketniveau."
Methoden konden alleen typeparameters gebruiken die door de receiver waren gedeclareerd. Je eigen methode kon er geen nieuwe introduceren. Dus elke generieke transformatie werd verbannen naar het pakketniveau, waar ze naast zeventien andere losse hulpfuncties stonden, allemaal een variant van MapSlice.
Go 1.27 lost dit op:
1type List[E any] []E
2
3// F is a type parameter declared by the method itself
4func (l List[E]) Apply[F any](f func(E) F) List[F] {
5 r := make(List[F], len(l))
6 for i, x := range l {
7 r[i] = f(x)
8 }
9 return r
10}
11
12func main() {
13 l := List[int]{1, 2, 3}
14 fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15 // [10 20 30]
16}
De receiver hoeft zelfs niet generiek te zijn. Een saaie struct kan een generieke methode hebben:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Voordat je vanmiddag je hele codebase gaat herschrijven, zijn er echter twee beperkingen, en die zijn belangrijker dan ze lijken:
- Interfacemethoden kunnen geen typeparameters declareren.
- Generieke methoden kunnen geen interfacemethoden implementeren.
Die tweede is degene die je gaat bijten:
1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2 G does not implement Adder (wrong type for method Add)
3 have Add[T any](T) T
4 want Add(int) int
Generieke methoden en dynamische dispatch passen dus niet goed samen. Dit is geen gierigheid van het Go-team: een generieke methode heeft oneindig veel instantiaties, en een interfacemethodetabel moet eindig zijn en op compileertijd bekend. Je kunt geen oneindig ding in een eindige tabel stoppen. Het universum zei nee.
Praktische conclusie: generieke methoden zijn bedoeld voor concrete types met hulpfunctie-achtige API's. Alles wat achter een interface verborgen moet blijven, heeft nog steeds de oude aanpak nodig.
De standaardbibliotheek heeft er al gebruik van gemaakt. Het Rand-type van math/rand/v2 kreeg een generieke methode:
1func (r *Rand) N[Int intType](n Int) Int
1r := rand.New(rand.NewPCG(1, 2))
2fmt.Println(r.N(int32(100))) // 76
3fmt.Println(r.N(time.Second)) // 616.436223ms
Voorheen was alleen de functie rand.N op pakketniveau generiek, wat betekende dat je de globale bron moest gebruiken. Nu krijgt je eigen, geseede *Rand hetzelfde gemak. Klein ding. Fijn ding.
1.2. Selectors van velden in struct-literals, of: issue #9859 krijgt eindelijk rust
Embedding geeft je u.ID. Embedding geeft je niet User{ID: 1}. In plaats daarvan krijg je User{Base: Base{ID: 1}}, wat Go's manier is om na te gaan of je het echt zo bedoelde.
Issue #9859 werd in 2015 aangemaakt. Het is nu gesloten. Ergens krijgt een gopher die sindsdien twee keer van carrière is veranderd een GitHub-melding.
1type Object struct{ name, color string }
2type Point3D struct {
3 Object
4 x, y, z float64
5}
6type Line struct {
7 Object
8 p, q Point3D
9}
10
11// Go 1.27: use the promoted field directly
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name is geen veld van Line. Het is een veld van het ingebedde Object. Voorheen schreef je Line{Object: Object{name: "diagonal"}, ...}.
Nu de kleine lettertjes, die ik heb gecontroleerd door alles daadwerkelijk te proberen te compileren.
(a) De sleutel is nog steeds een gewone identifier. Je kunt geen willekeurig selectorpad schrijven.
1_ = Line{Object.name: "x"} // invalid field name Object.name in struct literal
2_ = Line{p.x: 1} // invalid field name p.x in struct literal
De functie betekent dus niet "sleutels werken nu als veldtoegang." Het betekent specifiek "impliciet gepromoveerde veldnamen zijn toegestaan." Iets smaller dan de kop suggereert.
(b) Je kunt niet tegelijkertijd een ingebed veld en iets wat uit dat veld is gepromoveerd opgeven.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Redelijk. Je vertelde de compiler twee tegenstrijdige dingen over hetzelfde geheugen, en hij weigerde te gokken.
(c) Pointer-embedding is niet welkom.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Om een pointer te volgen heb je een pointer nodig om te volgen, en op het moment van het construeren van het literal is die er nog niet. Ook dat is terecht.
Goed nieuws: go fix herschrijft je oude literals voor je. Daarover later meer.
1.3. Typeinferentie van functies is minder willekeurig geworden
Typeinferentie voor generieke functies werkte vroeger op sommige plaatsen wel en op andere niet, zonder herkenbaar principe achter welke plaats welke was. Toewijzen aan een variabele? Prima. Dezelfde functie in een struct-veld zetten? Compilerfout, schrijf alsjeblieft double[int] alsof het 2022 is.
Go 1.27 zorgt ervoor dat inferentie werkt in elke context waarin het doeltype ondubbelzinnig bekend is:
1func double[T ~int | ~float64](v T) T { return v * 2 }
2
3type S struct{ f func(int) int }
4type A [1]func(int) int
5
6func main() {
7 s := S{f: double} // 1.26: needed double[int]
8 a := A{double} // 1.26: needed double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: needed double[int]
12
13 var fn func(float64) float64 = double // this always worked
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Eén double, geïnstantieerd als func(int) int op drie plaatsen en als func(float64) float64 op een vierde, volledig uit de context. Als je optiestructs of handlertabellen vol functiewaarden schrijft, gaat een flink deel van de [T]-ruis in je codebase verdwijnen.
2. Runtime: gratis prestaties en een lekdetector
2.1. Grootte-gespecialiseerde allocatie
De compiler genereert nu aanroepen naar grootte-gespecialiseerde allocatieroutines voor kleine objecten. De release notes beloven tot 30% minder tijd voor allocaties onder 80 bytes, en ongeveer 1% over het geheel voor allocatie-intensieve programma's.
Ik geloof release notes niet zonder te meten, dus:
1type small struct{ a, b, c, d int }
2
3var sink any
4
5func BenchmarkAlloc(b *testing.B) {
6 for b.Loop() {
7 sink = &small{}
8 }
9}
10
11func BenchmarkSlice(b *testing.B) {
12 for b.Loop() {
13 sink = make([]byte, 48)
14 }
15}
1# default (size-specialized malloc on)
2BenchmarkAlloc-12 3000000 5.358 ns/op
3BenchmarkSlice-12 3000000 13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12 3000000 8.722 ns/op
7BenchmarkSlice-12 3000000 20.81 ns/op
8
30–35% sneller in een microbenchmark die niets anders doet dan alloceren op Apple Silicon. Dat wil zeggen: dit is het beste geval, onder laboratoriumomstandigheden, behaald door een benchmark die is ontworpen om het getal er goed te laten uitzien. In een echt programma verdringen GC en het daadwerkelijke werk het grootste deel ervan. De claim van ~1% is de eerlijke.
De prijs zit in de binaire grootte. Hello World:
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Ongeveer 50KB, vast, ongeacht je programma. Je kunt je afmelden met GOEXPERIMENT=nosizespecializedmalloc, maar die noodontsnapping staat gepland om in Go 1.28 te worden verwijderd, dus beschouw het als een workaround voor een bugrapport en niet als levensstijl.
2.2. Het goroutine-lekprofiel is nu echt
Experimenteel in Go 1.26, algemeen beschikbaar in 1.27. De GOEXPERIMENT goroutineleakprofile is verdwenen; het werkt gewoon.
1func leak() {
2 ch := make(chan int) // nobody will ever send. nobody.
3 go func() {
4 <-ch
5 }()
6}
7
8func main() {
9 for range 3 {
10 leak()
11 }
12 runtime.GC()
13 runtime.GC()
14 pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
15}
1goroutineleak profile: total 3
23 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
3# 0x104e374f3 main.leak.func1+0x23 /tmp/go127/leak/main.go:12
Drie goroutines, permanent vastgelopen, met een bestandsnaam en regelnummer die precies aangeven waar je het hebt gedaan.
De truc achter dit is werkelijk slim: het hergebruikt de bereikbaarheidsanalyse van de garbage collector. Als goroutine G geblokkeerd is op primitief P, en P is onbereikbaar vanuit elke uitvoerbare goroutine (of iets wat die goroutines zouden kunnen wekken), dan kan niets P ooit nog aanraken, en dus wordt G nooit wakker. Dat is geen heuristiek, het is een bewijs.
Hetzelfde ontwerp geeft je de beperking gratis: als het kanaal of de mutex bereikbaar is via een globale variabele, of via een lokale variabele van een goroutine die nog draait, kan de GC het nog steeds zien, dus de runtime kan niets concluderen. Let op dat zelfs het speelgoedvoorbeeld hierboven twee aanroepen van runtime.GC() nodig heeft om te rapporteren. Het vangt niet alles.
Het zal echter wel het klassieke "vergeten de context te annuleren, werker-goroutine leeft voor altijd" vangen, en dat is de meeste lekken de meeste tijd.
Als je net/http/pprof importeert, is het ook beschikbaar op /debug/pprof/goroutineleak. Koppel dat aan staging, controleer het wekelijks, en wees stilletjes geschokt.
Dit werk is bijgedragen door Vlad Saioc bij Uber, die een drankje naar keuze verdient.
2.3. Tracebacks vertellen nu welk verzoek is gecrasht
Voor modules die Go 1.27 of later declareren, bevatten traceback-kopregels nu de goroutine-labels van runtime/pprof.
1func handle(ctx context.Context) {
2 var wg sync.WaitGroup
3 wg.Go(func() {
4 buf := make([]byte, 4<<10)
5 os.Stdout.Write(buf[:runtime.Stack(buf, true)])
6 })
7 wg.Wait()
8}
9
10func main() {
11 labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12 pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3 /tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5 ...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...
Het belangrijke detail: labels worden overgenomen door kindgoroutines. Goroutine 3 heeft nooit een label ingesteld. Het kreeg er een van zijn ouder.
Dus de volgende keer dat productie deadlockt en iemand het proces met SIGQUIT stopt, krijg je in plaats van 4.000 goroutines die er allemaal hetzelfde uitzien, 4.000 goroutines met labels die aangeven bij welk verzoek en welke tenant ze horen. Drie regels pprof.Do bij je verzoekingang leveren je dat op.
Omdat labels dingen kunnen bevatten die je liever niet naar stderr dumpt, schakelt GODEBUG=tracebacklabels=0 het uit, en die uitschakelmogelijkheid is uitdrukkelijk bedoeld om voor altijd te blijven bestaan.
2.4. asynctimerchan is definitief verdwenen
Go 1.23 maakte timerkanalen unbuffered (synchroon) en bood asynctimerchan=1 als manier om terug te keren naar het oude gedrag. In 1.27 is die instelling permanent verwijderd. Kanalen van het pakket time zijn synchroon, punt uit, zonder onderhandeling.
Het interessante is het beleid dat er samen mee is ingevoerd. Een verwijderde GODEBUG die in je go.mod blijft staan, breekt de build niet automatisch: hij breekt alleen als hij op de oude waarde is ingesteld:
1# go.mod contains: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
4
5# go.mod contains: godebug asynctimerchan=0
6(builds fine)
Dat betekent dat alleen de mensen die er echt op vertrouwden dat het verwijderde gedrag bestond, een standje krijgen. Iedereen die het op de uiteindelijke standaardwaarde had gezet en het vergeten was, mag blijven niet nadenken over het onderwerp. Dat is een doordacht stukje API-archeologie.
3. Standaardbibliotheek
3.1. encoding/json/v2: de engine is onderweg vervangen
Dit is het grote werk. Meer dan twee jaar discussie over het voorstel is eindelijk afgerond.
Er zijn nu drie pakketten, en het begrijpen van de splitsing is het grootste deel van het werk:
| Pakket | Taak |
|---|---|
encoding/json | De v1-API die je kent. Gedrag 100% ongewijzigd. Nu geïmplementeerd bovenop v2 |
encoding/json/v2 | Semantische verwerking. Go-waarden ↔ JSON |
encoding/json/jsontext | Syntactische verwerking. JSON als tokenstroom |
De kern is dat encoding/json opnieuw is opgebouwd op een geheel andere implementatie en zich identiek gedraagt. Zo werkt het:
1// Go 1.27's encoding/json.Unmarshal, abridged
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Elke legacy v1-eigenaardigheid is als optie gecodeerd, gebundeld in DefaultOptionsV1(), en de v1-API past die bundel altijd toe. Dat betekent dat dit zich in 1.26 en 1.27 hetzelfde gedraagt:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Ja, v1 accepteert nog steeds stilzwijgend dubbele sleutels en neemt de laatste. Dat deed het altijd. Dat doet het nog steeds. Compatibiliteit betekent compatibiliteit met de slechte delen ook.
v2 heeft daarentegen meningen:
1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: duplicate object member name "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: invalid UTF-8 after offset 1
Dubbele sleutels en ongeldige UTF-8 worden geweigerd. Beide zijn werkelijk gevaarlijk, niet alleen onopgeruimd: wanneer twee parsers het oneens zijn over welke dubbele sleutel wint, ontstaan beveiligingsfouten. CouchDB's CVE-2017-12635 was precies dit: een JSON-body met twee roles-sleutels, waarbij de validator de ene las en de opslaglaag de andere. Weigeren is de juiste keuze.
Opties zijn variadisch:
1type Config struct {
2 Name string `json:"name"`
3 Tags []string `json:"tags,omitzero"`
4 Timeout int `json:"timeout,omitzero"`
5}
6
7out, _ := jsonv2.Marshal(Config{Name: "api"})
8// {"name":"api"}
9
10// sorted map keys + indentation
11out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
12 jsonv2.Deterministic(true), jsontext.WithIndent(" "))
13// {
14// "a": 1,
15// "b": 2
16// }
17
18// reject unknown fields — previously required a Decoder and DisallowUnknownFields()
19var c Config
20err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
21 jsonv2.RejectUnknownMembers(true))
22// json: cannot unmarshal JSON string into Go main.Config:
23// unknown object member name "nope"
Deterministic, MatchCaseInsensitiveNames, StringifyNumbers, FormatNilSliceAsNull, OmitZeroStructFields: de meeste dingen die je voorheen oploste met een bibliotheek van derden of een handgeschreven MarshalJSON zijn nu vlaggen.
En het migratieverhaal is het beste deel. Latere opties winnen, dus je kunt de strengheid van v2 gedrag voor gedrag overnemen:
1// keep v1 semantics, but reject duplicate keys like v2 does
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Je hoeft geen codebase van 200.000 regels naar v2 te porten om te stoppen met het accepteren van dubbele sleutels. Je zet één optie om. Voor grote projecten is dit het realistische pad.
Prestaties: marshaling is ongeveer gelijk, unmarshaling is aanzienlijk sneller. Als er iets misgaat, herstelt GOEXPERIMENT=nojsonv2 de oude implementatie. Ook die afmeldoptie staat uiteindelijk op de nominatie, dus dien een issue in in plaats van ermee te blijven leven.
jsontext is de low-levellaag: Encoder/Decoder die JSON als Tokens en Values doorlopen, met een toestandsmachine die je eerlijk houdt. Pak het erbij wanneer je een streamingtransformator of JSON-filter schrijft en helemaal geen Go-waarden wilt materialiseren.
3.2. Een standaard uuid-pakket
Eindelijk. RFC 9562, in de standaardbibliotheek, geen go get nodig.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: top 48 bits are a timestamp, so these always sort in creation order
8 a, b := uuid.NewV7(), uuid.NewV7()
9 fmt.Println(a.Compare(b) < 0) // true
10
11 u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
12 fmt.Println(u, err)
13 // 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>
14
15 fmt.Println(uuid.Nil(), uuid.Max())
16 // 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
17}
De hele API bestaat uit New, NewV4, NewV7, Nil, Max, Parse, MustParse, en één type: type UUID [16]byte. Dat is alles. Je kunt de volledige pakketdocumentatie lezen terwijl je koffie afkoelt.
Details die het weten waard zijn:
UUIDis[16]byte, dus==werkt en het is direct bruikbaar als mapsleutel. Hetzelfde ontwerp alsgoogle/uuid.NilenMaxzijn functies, geen variabelen. Want eenvar Nil UUIDop pakketniveau is een geladen pistool gericht op je voet, en iemand, ergens, zou er uiteindelijk een waarde aan toewijzen.- Willekeurige bits komen van een cryptografisch veilige generator.
- Het implementeert
encoding.TextMarshaler/TextUnmarshaler/TextAppender, dus het past direct in JSON-structs. NewV7is degene die je wilt voor databaseprimaire sleutels. Doordat het op tijd gesorteerd is, fragmenteert je B-tree-index niet meer, iets waar v4-UUID's berucht slecht in zijn.
Je kunt nu een afhankelijkheid verwijderen. Als je v1/v3/v5 of geavanceerdere parseropties nodig hebt, hebben bibliotheken van derden nog steeds een taak.
3.3. crypto/mldsa: post-quantum handtekeningen, en ze zijn enorm
Go 1.24 gaf ons crypto/mlkem voor post-quantum sleuteluitwisseling. Go 1.27 brengt de andere helft: ML-DSA-handtekeningen, gestandaardiseerd als FIPS 204.
1sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
2pk := sk.PublicKey()
3
4msg := []byte("release the gophers")
5opts := &mldsa.Options{Context: "gosuda.org/blog"}
6
7sig, _ := sk.Sign(rand.Reader, msg, opts)
8
9fmt.Println("private key seed:", len(sk.Bytes()), "bytes") // 32
10fmt.Println("public key:", len(pk.Bytes()), "bytes") // 1952
11fmt.Println("signature:", len(sig), "bytes") // 3309
12
13fmt.Println(mldsa.Verify(pk, msg, sig, opts))
14// <nil>
15fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
16// mldsa: invalid signature
Kijk naar die getallen. Een enkele handtekening is 3.309 bytes. Ed25519 is 64. Dat is een toename van 50 keer, en de publieke sleutel is er bovenop bijna 2KB. Stop er een paar in een certificaatketen en je TLS-handshake krijgt zijn eigen MTU-strategie nodig.
Dit zijn de werkelijke kosten van kwantumweerstand vandaag, en daarom schakelt niemand morgen alles om. Maar het zit nu in de standaardbibliotheek, en dat is waar je het wilt hebben voordat je het nodig hebt.
Options.Context is domeinscheiding: onderteken met dezelfde sleutel voor verschillende doeleinden, gebruik een andere context voor elk, en een handtekening uit de ene context wordt niet geverifieerd in een andere. Het voorbeeld hierboven laat precies dat zien: dezelfde sleutel, hetzelfde bericht, andere context, afgewezen.
PrivateKey implementeert crypto.Signer, dus het past in bestaande interfaces. crypto/x509 verwerkt ML-DSA-sleutels en -handtekeningen, en crypto/tls ondersteunt de handtekeningschema's MLDSA44/MLDSA65/MLDSA87 in TLS 1.3.
Er is ook SignDeterministic, dat de willekeur overslaat: handig voor tests en reproduceerbare builds.
3.4. simd: vectorinstructies zonder assembly
Go 1.26 introduceerde de architectuurspecifieke simd/archsimd als experiment. Go 1.27 voegt simd toe: portabel en onafhankelijk van de vectorbreedte. Inschakelen met GOEXPERIMENT=simd.
1// dst = a*x + y
2func axpy(dst, x, y []float32, a float32) {
3 va := simd.BroadcastFloat32s(a)
4 w := va.Len()
5
6 i := 0
7 for ; i+w <= len(x); i += w {
8 vx := simd.LoadFloat32s(x[i:])
9 vy := simd.LoadFloat32s(y[i:])
10 vx.MulAdd(va, vy).Store(dst[i:])
11 }
12 // handle the tail with a partial load/store — no scalar cleanup loop
13 if i < len(x) {
14 vx, _ := simd.LoadFloat32sPart(x[i:])
15 vy, _ := simd.LoadFloat32sPart(y[i:])
16 vx.MulAdd(va, vy).StorePart(dst[i:])
17 }
18}
1$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
2vector bits: 128 emulated: false
3[4 7 10 13 16 19 22 25 28 31]
De ontwerpkeuze die ertoe doet: de vectorbreedte wordt nooit hardgecodeerd. Je vraagt va.Len() tijdens runtime. VectorBitSize() vertelt je de werkelijke breedte, Emulated() vertelt je of je echte hardware hebt of een beleefde softwarenabootsing. Mijn Mac meldde 128-bit NEON; een AVX-512-machine meldt meer; een machine zonder ondersteuning meldt emulatie en de code draait nog steeds.
LoadFloat32sPart/StorePart verdienen een speciale vermelding. Het meest vervelende aan handgeschreven SIMD is altijd de onregelmatige staart aan het eind van de array, en dit regelt die zonder aparte scalaire lus.
Nog steeds experimenteel, de API is nog onstabiel, zet het niet in productie. Maar het feit dat dit Go-code is en geen assembly of cgo, is werkelijk een groot probleem.
3.5. hash/maphash.Hasher
Een nieuwe interface die het contract beschrijft tussen een waardetype en hashgebaseerde containers:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Waarom? De ingebouwde map van Go accepteert alleen comparable-sleutels. Je kunt niet op een slice sleutelen. Je kunt niet "gelijk negerend hoofdletters" definiëren. Hasher lost beide tegelijk op:
1type CaseInsensitive struct{}
2
3func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
4 h.WriteString(strings.ToLower(s))
5}
6func (CaseInsensitive) Equal(x, y string) bool {
7 return strings.ToLower(x) == strings.ToLower(y)
8}
9
10var seed = maphash.MakeSeed()
11
12func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
13 var h maphash.Hash
14 h.SetSeed(seed) // same seed, or none of this means anything
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Voor gewone ==-semantiek is er ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
De val: Hasher is een interface, geen datastructuur. Er is nog geen standaard hashtabel of Bloomfilter die het gebruikt. Dit is voorbereidend werk voor een toekomstig containerpakket. Vandaag gebruik je het wanneer je je eigen structuur bouwt, of consumeer je een bestaande implementatie zoals go/types.Hasher (waarmee je types.Type als mapsleutel kunt gebruiken, met respect voor Identical).
Seedbeheer is aan jou. Als hashOf hierboven bij elke aanroep een nieuwe seed maakte, zouden identieke waarden verschillend worden gehasht en zou het voorbeeld false printen, precies de bug die ik bij mijn eerste poging schreef. Eén seed per container. (De seed is willekeurig om hash-flooding-DoS-aanvallen te verijdelen, daarom is het geen eenvoudige constante.)
3.6. httptest.NewTestServer + synctest.Sleep: de slaapmutsje-hit
Als je maar één ding uit deze release overneemt, maak het dan dit.
testing/synctest ging in Go 1.25 uit de experimentele fase en gaf ons een nepklok voor het testen van concurrente code. Maar zodra je test een echt netwerk aanraakte, stortte de illusie in. Go 1.27's httptest.NewTestServer gebruikt een in-memory nep-netwerk, zodat de bubbel intact blijft. En synctest.Sleep (= time.Sleep + synctest.Wait) maakt het compleet.
Hier is een test met retry en exponentiële backoff:
1func TestRetryWithBackoff(t *testing.T) {
2 synctest.Test(t, func(t *testing.T) {
3 var hits int
4 srv := httptest.NewTestServer(t, http.HandlerFunc(
5 func(w http.ResponseWriter, r *http.Request) {
6 hits++
7 if hits < 3 {
8 w.WriteHeader(http.StatusServiceUnavailable)
9 return
10 }
11 io.WriteString(w, "ok")
12 }))
13
14 client := srv.Client()
15 start := time.Now()
16 var body string
17 for attempt := range 5 {
18 resp, err := client.Get(srv.URL)
19 if err != nil {
20 t.Fatal(err)
21 }
22 b, _ := io.ReadAll(resp.Body)
23 resp.Body.Close()
24 if resp.StatusCode == http.StatusOK {
25 body = string(b)
26 break
27 }
28 synctest.Sleep(time.Duration(1<<attempt) * time.Second)
29 }
30
31 if body != "ok" {
32 t.Fatalf("got %q", body)
33 }
34 // backoff should be exactly 1s + 2s = 3s
35 if elapsed := time.Since(start); elapsed != 3*time.Second {
36 t.Fatalf("elapsed = %v, want 3s", elapsed)
37 }
38 t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
39 })
40}
1=== RUN TestRetryWithBackoff
2 x_test.go:48: hits=3 elapsed=3s
3--- PASS: TestRetryWithBackoff (0.00s)
Lees dit twee keer. Het simuleerde drie seconden backoff tegen een echte HTTP-server en was klaar in 0,00 seconden. En de bewering is elapsed == 3*time.Second: precies drie seconden, niet "minstens drie seconden, plus of min schedulerjitter." Nepklokken hebben geen jitter.
synctest.Sleep bestaat om een specifieke reden: als je test even lang slaapt als de code die je test, is het gissen welke als eerste wakker wordt. synctest.Sleep slaapt en wacht daarna tot elke andere goroutine in de bubbel duurzaam geblokkeerd is, zodat je het systeem observeert nadat het tot rust is gekomen.
Timeouts, retries, circuit breakers, rate limiters: elke tijdsafhankelijke HTTP-clienttest die je beheert kan snel en deterministisch worden. Als je testsuite op dit moment bij elkaar wordt gehouden door time.Sleep(100 * time.Millisecond) en hoop, is dit je uitweg.
3.7. net/http-wijzigingen die daadwerkelijk productie raken
Stil, maar ze verschijnen in je metrics.
Responsbodies van HTTP/1 worden automatisch leeggelezen bij Close. Ongelezen inhoud wordt nu leeggelezen (tot een conservatieve limiet) wanneer je de body sluit, zodat de verbinding opnieuw gebruikt kan worden. Dat betekent dat je eindelijk deze bezwering mag verwijderen die iedereen sinds 2016 van dezelfde Stack Overflow-antwoord heeft gekopieerd:
1// no longer necessary
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Voor de meeste programma's is dit een no-op of een kleine winst. Als het dingen erger maakt, zit je waarschijnlijk in het bakje dat de release notes beleefd beschrijven: Transport.MaxIdleConns ingesteld op 0, of een nieuwe Client per verzoek, waardoor de limiet op inactieve verbindingen volledig wordt omzeild. Transport.DisableKeepAlives = true plakt het dicht, maar het werkelijke advies in de release notes is dat "a deeper look would likely be beneficial", wat in Go-team-taal betekent: je hebt grotere problemen.
HTTP/2-clientprioriteit (RFC 9218). De server honoreert nu prioriteitssignalen van de client. Als je de oude round-robin-planning verkoos, dan Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Begrenst hoeveel headerwaarden de server accepteert, standaard DefaultMaxHeaderValueCount. Nog een deur dicht op "stuur tienduizend headers en kijk wat er gebeurt."
ALPN op door de gebruiker geleverde verbindingen. Als je net.Conn ConnectionState() tls.ConnectionState implementeert, doen Transport en Server TLS-ALPN-onderhandeling erop, zodat aangepaste dialers die via een proxy lopen nog steeds HTTP/2 kunnen onderhandelen.
3.8. De kleine dingen die vreugde brengen
strings.CutLast / bytes.CutLast. Cut splitst bij de eerste scheidingsteken. Er was geen "last"-variant, dus iedereen schreef LastIndex plus slicing met de hand, en ongeveer 30% van ons maakte bij de eerste poging een off-by-one-fout.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Geweldig voor bestandsextensies en voor het parsen van host:port, waar je de laatste dubbele punt moet vinden, omdat IPv6-adressen er vol mee zitten.
math/big.Int.Divide. Deling met een expliciete afrondingsmodus, ter vervanging van de munt die opgegooid wordt over "wil ik Quo of Div?":
1x, y := big.NewInt(-7), big.NewInt(2)
2
3new(big.Int).Divide(x, y, new(big.Int), big.Trunc) // q=-3 r=-1
4new(big.Int).Divide(x, y, new(big.Int), big.Floor) // q=-4 r=1
5new(big.Int).Divide(x, y, new(big.Int), big.Round) // q=-4 r=1
6new(big.Int).Divide(x, y, new(big.Int), big.Ceil) // q=-3 r=-1
Als je werkt in een domein waar de afrondingsregel in een regelgeving is vastgelegd, is dit voor jou.
url.URL.Clone en url.Values.Clone. Diepe kopieën. De oude ondiepe kopie *u deelde de Userinfo-pointer, wat precies het soort bug veroorzaakte dat een dag kost om te vinden en vijf seconden om op te lossen.
1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.Println(orig.Host, clone.Host) // gosuda.org example.com
Leesmethoden van net.UnixConn geven nu io.EOF direct terug in plaats van het in een net.OpError te wikkelen. err == io.EOF werkt nu, zoals het altijd had moeten doen.
database/sql.ConvertAssign en driver.RowsColumnScanner. Functies voor driverauteurs. De eerste maakt de typeconversies zichtbaar die Rows.Scan uitvoert; de tweede laat drivers rechtstreeks in gebruikersbestemmingen scannen, waardoor een tussentijdse allocatie wordt overgeslagen.
unicode 15 → 17. Twee versies in één sprong. Het gedrag van stringclassificatie en normalisatie kan subtiel veranderen. Als je tests hebben die daarvan afhangen, zul je het merken.
compress/flate is sneller geworden, en de uitvoerbytes kunnen afwijken van Go 1.26. Dit werkt door naar archive/zip, compress/gzip, compress/zlib en image/png. Als je golden-file-tests hebt die gecomprimeerde uitvoer hashen, zullen ze breken, en dat is geen regressie. Controleer dit voordat je upgradet, niet tijdens de incidentbespreking.
4. Toolchain
4.1. go fix kreeg meer modernizers
go fix verandert geruisloos in een hulpmiddel voor codemodernisering. Go 1.27 voegt atomictypes, embedlit, slicesbackward en unsafefuncs toe. Gebruik -diff om een voorvertoning te bekijken:
1package fixdemo
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Base struct{ ID int }
9type User struct {
10 Base
11 Name string
12}
13
14func New() User {
15 return User{Base: Base{ID: 1}, Name: "gopher"}
16}
17
18var counter int64
19
20func Incr() { atomic.AddInt64(&counter, 1) }
21
22func Reverse(s []string) {
23 for i := len(s) - 1; i >= 0; i-- {
24 _ = s[i]
25 }
26}
27
28func Spawn(wg *sync.WaitGroup) {
29 wg.Add(1)
30 go func() {
31 defer wg.Done()
32 }()
33}
1$ go1.27rc3 fix -diff ./fixdemo
1 import (
2+ "slices"
3 "sync"
4 "sync/atomic"
5 )
6
7 func New() User {
8- return User{Base: Base{ID: 1}, Name: "gopher"}
9+ return User{ID: 1, Name: "gopher"}
10 }
11
12-var counter int64
13+var counter atomic.Int64
14
15-func Incr() { atomic.AddInt64(&counter, 1) }
16+func Incr() { counter.Add(1) }
17
18 func Reverse(s []string) {
19- for i := len(s) - 1; i >= 0; i-- {
20- _ = s[i]
21+ for _, v := range slices.Backward(s) {
22+ _ = v
23 }
24 }
25
26 func Spawn(wg *sync.WaitGroup) {
27- wg.Add(1)
28- go func() {
29- defer wg.Done()
30- }()
31+ wg.Go(func() {
32+ })
33 }
Vier modernizers zijn afgegaan op één klein bestand:
embedlit: herschrijft ingebedde literals naar de nieuwe syntaxis uit §1.2atomictypes:atomic.AddInt64(&x, 1)wordtatomic.Int64.Add(1), wat niet-atomaire toegang onmogelijk maakt en stilletjes 32-bits uitlijningsfouten oplost waarvan je niet wist dat je ze hadslicesbackward: achterwaartse lussen wordenslices.Backwardwaitgroupgo: het ritueelAdd/go/Donewordtwg.Go(hernoemd vanwaitgroupuit 1.26 om verwarring te voorkomen)
go tool fix help toont alle 26; go tool fix help <name> legt er één uit. Verder is fmtappendf verwijderd "vanwege stilistische overwegingen", wat een prachtig diplomatieke manier is om te beschrijven wat er ook in die issuediscussie gebeurd is.
go fix ./... uitvoeren op een oude codebase is een werkelijk bevredigende middag. Lees de diff natuurlijk eerst.
4.2. go test draait standaard stdversion
Dit is de wijziging die het meest waarschijnlijk je dag onderbreekt.
go test draait nu standaard de vetcontrole stdversion, die standaardbibliotheeksymbolen markeert die nieuwer zijn dan wat de go-directive in je go.mod toestaat:
1// go.mod says: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast is a 1.27 symbol
8 return ext
9}
1$ go1.27rc3 test ./...
2# sv
3./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
4FAIL sv [build failed]
Dit maakt een einde aan de klassieke faalmodus waarbij alles op jouw machine werkt (nieuwste toolchain) en in CI of in de oudere omgeving van een gebruiker ontploft. Als je bibliotheken publiceert met een conservatieve go-directive, is dit een cadeau.
4.3. go test -json kreeg OutputType
Regels "Action":"output" bevatten nu een optioneel veld "OutputType": "error", "error-continue" of "frame".
1'frame' '=== RUN TestFail\n'
2None ' x_test.go:6: hello\n'
3'error' ' x_test.go:7: boom\n'
4'frame' '--- FAIL: TestFail (0.00s)\n'
5'frame' 'FAIL\n'
Uitvoer van t.Log (zonder veld), uitvoer van t.Error (error) en door het framework gegenereerde regels (frame) zijn nu te onderscheiden. Iedereen die een reguliere expressie heeft geschreven om uit te zoeken welke regels van testuitvoer "echt" zijn, kan die nu weggooien.
4.4. Verbeteringen aan go doc
Syntaxis package@version. Lees de documentatie voor een specifieke versie zonder het aan je module toe te voegen:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Handig om te controleren wat er veranderd is vóór een upgrade.
-ex en het afdrukken van voorbeeldbroncode:
1$ go1.27rc3 doc -ex strings | grep Example
2 func ExampleClone()
3 func ExampleCompare()
4 func ExampleContains()
5 func ExampleCut()
6 func ExampleCutPrefix()
7 ...
8
9$ go1.27rc3 doc strings.ExampleCut
10package main
11
12import (
13 "fmt"
14 "strings"
15)
16
17func main() {
18 show := func(s, sep string) {
19 before, after, found := strings.Cut(s, sep)
20 fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
21 }
22 show("Gopher", "Go")
23 ...
24}
25
26Output:
27Cut("Gopher", "Go") = "", "pher", true
Voorbeeldbroncode en verwachte uitvoer, in de terminal, zonder een browsertabblad te openen dat volgende donderdag nog steeds open staat.
4.5. go mod tidy ruimt je require-blokken op
Voor modules met go 1.27 of later voegt go mod tidy verspreide require-blokken samen tot maximaal twee (direct en indirect), met behoud van bijbehorende commentaar.
1// Before
2module tidydemo
3
4go 1.27
5
6require golang.org/x/sync v0.10.0
7
8// networking
9require golang.org/x/net v0.33.0
10
11require (
12 golang.org/x/sys v0.28.0 // indirect
13)
14
15require golang.org/x/text v0.21.0 // indirect
1// After: go mod tidy
2module tidydemo
3
4go 1.27
5
6require (
7 // networking
8 golang.org/x/net v0.33.0
9 golang.org/x/sync v0.10.0
10)
Let op dat het commentaar // networking behouden is gebleven. Dit ruimt vooral op na het oplossen van Git-mergeconflicten, waar verdwaalde require-blokken ontstaan. Als je team meer dan drie mensen heeft die afhankelijkheden toevoegen, worden je go.mod-conflicten merkbaar stiller.
4.6. Overige zaken
- Ondersteuning voor
bzrverwijderd. Als dit jou raakt, hoor ik graag het verhaal. go tool trace -httpbindt aan localhost wanneer alleen een poort wordt opgegeven.-http=:6060luistert niet langer op alle interfaces. Gebruik-http=0.0.0.0:6060als je dat echt bedoelde. Dit komt nu overeen metgo tool pprofen voorkomt de af en toe per ongeluk publieke profiler.- Responsbestanden (
@file) worden ondersteund doorcompile,link,asm,cgo,coverenpack, in een GCC-compatibel formaat. Voor buildsystemen die de lengtelimiet van de commandoregel overschrijden: hallo, Bazel.
5. Compiler, linker, platforms
Compiler. Relatieve bestandsnamen in //line-directieven worden nu opgelost ten opzichte van de map van het bevattende bestand, in overeenstemming met go/scanner. Relevant als je codegeneratoren schrijft.
Namen van functieliteralen (closures) zijn ook eenvoudiger geworden: dezelfde naam ongeacht inlining, en meerdere instanties van dezelfde literal kunnen code delen in de binary. Geen functionele wijziging, behalve: code die functie-identiteit vergelijkt via reflect.Value.Pointer zal vaker "gelijk" zien dan voorheen. Die vergelijking was nooit geldig, maar als je die hebt, begint hij nu luider tegen je te liegen.
Linker. Nieuwe opties -macos en -macsdk stellen de OS- en SDK-versies in het LC_BUILD_VERSION-laadcommando van macOS in.
Platforms.
- Darwin vereist nu macOS 13 Ventura of later, zoals aangekondigd in 1.26. Controleer je CI-runners.
- PowerPC (
GOOS=linux GOARCH=ppc64) is overgeschakeld naar de ELFv2-ABI. Vereist Linux-kernel 3.13+ (RHEL7 heeft het teruggeport naar 3.10). Cgo, PIE en externe linking worden nu ondersteund. Als je cgo gebruikt maar een statisch puur-Go-binary nodig hebt, stel danCGO_ENABLED=0in.
6. Upgrade-checklist
Go 1.27 neemt compatibiliteit serieus, maar controleer deze punten voordat je de versie verhoogt:
- Golden-file-tests op gecomprimeerde uitvoer.
compress/flateheeft zijn encoders veranderd; gzip-, zip- en png-bytes kunnen afwijken. - Tests die JSON-foutmeldingen als strings vergelijken. Het gedrag is identiek; de foutmeldingstekst is dat niet.
- Verwijderde GODEBUGs in
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf: deze laten de build alleen falen als ze op hun oude waarden zijn ingesteld. - Overtredingen van
stdversion.go testvangt ze nu op. Draai het vroeg als je een bibliotheek met een conservatievego-directive onderhoudt. - macOS 12 of oudere CI-runners. Ondersteuning is verdwenen.
- Tests die op functieliteral-symboolnamen controleren, en vergelijkingen van functies via
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0of eenClientper verzoek. In combinatie met het automatisch leegmaken van responsbodies kan dit trager worden.
Slotbeschouwingen
Go 1.27 is de release waarin veel uitgestelde onderhoud tegelijk aan de beurt kwam.
Generieke methoden ontbraken nog in het werk aan generics van 1.18. Selectors van velden in struct-literals sloten een elf jaar oud issue af. encoding/json/v2 was twee jaar discussie over een voorstel. En uuid vulde een gat dat vrijwel elk Go-project een decennium lang had gedicht met dezelfde afhankelijkheid van derden.
Als je een prioriteitsvolgorde wilt om daadwerkelijk waarde uit dit alles te halen:
httptest.NewTestServer+synctest.Sleep: tijdsafhankelijke HTTP-tests worden snel en deterministisch. De beste verhouding tussen inspanning en opbrengst in de hele release.- Het goroutine-lekprofiel: stel één eindpunt beschikbaar in staging en bereid je voor om nederig te worden.
- Goroutine-labels in tracebacks: drie regels
pprof.Dobij je verzoekingang maken van je volgende stackdump om 3 uur 's nachts iets leesbaars. go fix ./...: laat het hulpmiddel de modernisering doen.encoding/json/v2: geen haast. Neem het één optie tegelijk over.
Pak go1.27rc3 en draai nu je testsuite ertegen. Alles op die checklist is aanzienlijk prettiger om op een dinsdagmiddag te ontdekken dan tijdens een incident.
Referenties