GoSuda

Go 1.27, eller Hvordan jeg lærte at holde op med at bekymre mig og elske generiske metoder

By iwanhae views ...

Oversigt over Go 1.27

Go 1.27 udkommer i august 2026, og for en gangs skyld har selve sproget ændret sig. Ikke "vi tilføjede en ny funktion til slices"-ændring, men en faktisk ændring. Generiske metoder er nu til stede. Et elleve år gammelt issue blev lukket. encoding/json blev stille og roligt erstattet af en ny motor, mens flyet var i luften.

Alle eksempler nedenfor blev kørt på go1.27rc3 (darwin/arm64). Hver outputblok er reel output, kopieret direkte, inklusive fejlmeddelelserne. Hvis noget ser mærkeligt ud, skyldes det, at det er præcis det, compileren faktisk sagde.

1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download

1. Sproget ændrede sig (ja, virkelig)

1.1. Generiske metoder

Siden Go 1.18 har vi haft generics, og siden Go 1.18 har vi haft Samtalen. Den forløber sådan her:

"Lad mig bare skrive en Map-metode på min slice-type—"

method must have no type parameters

"...okay. Så bliver det en funktion på pakkeniveau."

Metoder kunne kun bruge typeparametre, der var deklareret af receiveren. Din egen metode kunne ikke introducere nye. Derfor blev hver generisk transformation forvist til pakkeniveau, hvor den stod ved siden af sytten andre frit svævende hjælpefunktioner, alle med en variant af navnet MapSlice.

Go 1.27 retter op på dette:

 1type List[E any] []E
 2
 3// F er en typeparameter, som selve metoden deklarerer
 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}

Receiveren behøver ikke engang at være generisk. En almindelig struct kan have en generisk metode:

1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }

Før du refaktorerer hele din kodebase i eftermiddag, er der nu to begrænsninger, og de betyder mere, end de ser ud til:

  1. Interface-metoder kan ikke deklarere typeparametre.
  2. Generiske metoder kan ikke implementere interface-metoder.

Den anden er den, der vil bide dig:

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

Så generiske metoder og dynamisk dispatch hænger ikke sammen. Det er ikke Go-teamet, der er nærige. Det handler om, at en generisk metode har uendeligt mange instantieringer, mens en interface-metodetabel skal være endelig og kendt på compileringstidspunktet. Man kan ikke putte noget uendeligt ind i en endelig tabel. Universet sagde nej.

Praktisk læsning: generiske metoder er til konkrete typer med hjælpefunktionspræget API'er. Alt, der skal skjules bag et interface, har stadig brug for den gamle fremgangsmåde.

Standardbiblioteket har allerede udnyttet det. math/rand/v2s Rand fik en generisk metode:

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

Tidligere var kun den pakkeniveau-baserede rand.N generisk, hvilket betød, at man brugte den globale kilde. Nu får din egen seedede *Rand den samme bekvemmelighed. En lille ting. En god ting.

1.2. Feltvælgere i struct-literals, eller: Issue #9859 når endelig i mål

Embedding giver dig u.ID. Embedding giver ikke dig User{ID: 1}. I stedet giver det dig User{Base: Base{ID: 1}}, hvilket er Go's måde at spørge, om du virkelig mente det.

Issue #9859 blev oprettet i 2015. Det er nu lukket. Et sted er en gopher, som siden har skiftet karriere to gange, ved at modtage en GitHub-notifikation.

 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: brug det forfremmede felt direkte
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name er ikke et felt i Line. Det er et felt i den indlejrede Object. Tidligere skrev man Line{Object: Object{name: "diagonal"}, ...}.

Nu til det med småt, som jeg tjekkede ved faktisk at forsøge at kompilere det hele.

(a) Nøglen er stadig en simpel identifikator. Du kan ikke skrive en vilkårlig selektorsti.

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

Så funktionen er ikke "nøgler fungerer nu som feltadgang". Den er specifikt "implicit forfremmede feltnavne er tilladt". Lidt snævrere, end overskriften antyder.

(b) Du kan ikke angive et indlejret felt og noget forfremmet fra det samtidig.

1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object

Rimeligt. Du fortalte den to modstridende ting om den samme hukommelse, og den nægtede at gætte.

(c) Pointer-embedding er ikke inviteret.

1type PtrEmbed struct {
2	*Object
3	z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name

For at følge en pointer har du brug for en pointer at følge, og på tidspunktet for konstruktion af literalen findes der endnu ingen. Også rimeligt.

Gode nyheder: go fix vil omskrive dine gamle literals for dig. Mere om det senere.

1.3. Typeinferens for funktionstyper er blevet mindre vilkårlig

Typeinferens for generiske funktioner virkede tidligere på nogle steder og ikke andre, uden nogen synlig logik bag, hvilket var hvilket. Tildeling til en variabel? Fint. At putte præcis den samme funktion i et struct-felt? Compilerfejl, skriv venligst double[int], som i 2022.

Go 1.27 får inferens til at virke i enhver kontekst, hvor måltypen er entydigt kendt:

 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: krævede double[int]
 8	a := A{double}             // 1.26: krævede double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: krævede double[int]
12
13	var fn func(float64) float64 = double  // dette virkede altid
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

Én double, instantieret som func(int) int de tre steder og func(float64) float64 det fjerde sted, udelukkende ud fra konteksten. Hvis du skriver optionstrukturer eller handler-tabeller fulde af funktionsværdier, er en masse [T]-støj ved at forsvinde fra din kodebase.

2. Runtime: Gratis ydeevne og en lækagedetektor

2.1. Størrelsesspecialiseret allokering

Compileren udsender nu kald til størrelsesspecialiserede allokeringsrutiner for små objekter. Release notes hævder op til 30% reduktion i allokeringer under 80 bytes og cirka 1% samlet for allokeringstunge programmer.

Jeg tror ikke på release notes uden at måle, så:

 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# standard (størrelsesspecialiseret malloc til)
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% hurtigere i en mikrobenchmark, der intet andet gør end at allokere på Apple Silicon. Altså: dette er det bedste tilfælde, opnået under laboratorieforhold af en benchmark designet til at få tallet til at se godt ud. I et rigtigt program vil GC og det egentlige arbejde begrave det meste af det. Påstanden om ~1% er den ærlige.

Prisen er binærstørrelsen. Hello World:

12413202 bytes  (standard)
22361826 bytes  (nosizespecializedmalloc)

Cirka 50KB, fast, uanset dit program. Du kan fravælge det med GOEXPERIMENT=nosizespecializedmalloc, men den nødudgang er planlagt fjernet i Go 1.28, så behandl den som en workaround til fejlrapporter og ikke som en livsstil.

2.2. Goroutine-lækageprofilen er nu virkelig

Eksperimentel i Go 1.26, generelt tilgængelig i 1.27. GOEXPERIMENT'et goroutineleakprofile er væk; det virker bare.

 1func leak() {
 2	ch := make(chan int) // ingen vil nogensinde sende. ingen.
 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

Tre goroutines, permanent fastlåste, med en fil og et linjenummer, der peger præcis på, hvor du gjorde det.

Tricket bag dette er virkelig snedigt: det genbruger garbage collectorens analyse af tilgængelighed. Hvis goroutine G er blokeret på primitiven P, og P ikke kan nås fra nogen kørbar goroutine (eller noget, disse goroutines kunne vække), så kan intet nogensinde røre P igen, og G vil derfor aldrig vågne. Det er ikke en heuristik, det er et bevis.

Samme design giver dig begrænsningen gratis: hvis kanalen eller mutexen kan nås via en global variabel eller via en lokal variabel i en goroutine, der stadig kører, kan GC'en stadig se den, og runtime kan derfor ikke konkludere noget. Bemærk, at selv det lille eksempel ovenfor kræver to runtime.GC()-kald, før det rapporterer. Det fanger ikke alt.

Den vil dog fange det klassiske "glemte at annullere konteksten, arbejdergoroutinen lever for evigt" — hvilket er de fleste lækager det meste af tiden.

Hvis du importerer net/http/pprof, findes den også på /debug/pprof/goroutineleak. Sæt det op i staging, tjek det ugentligt, og vær stille forfærdet.

Dette arbejde blev bidraget af Vlad Saioc hos Uber, som fortjener en drik efter eget valg.

2.3. Tracebacks fortæller nu, hvilken forespørgsel der døde

For moduler, der deklarerer Go 1.27 eller senere, inkluderer traceback-headerlinjer nu runtime/pprof-goroutine-labels.

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

Det vigtige detalje: labels nedarves af child-goroutines. Goroutine 3 satte aldrig en label. Den fik en fra sin forælder.

Så næste gang produktionen deadlocker, og nogen sender SIGQUIT til processen, får du i stedet for 4.000 goroutines, der alle ser ens ud, 4.000 goroutines mærket med, hvilken forespørgsel og hvilken tenant de tilhører. Tre linjer pprof.Do ved dit indgangspunkt for forespørgsler giver dig det.

Da labels kan indeholde ting, du helst ikke vil dumpe til stderr, slår GODEBUG=tracebacklabels=0 det fra, og den fravalgsmulighed er udtrykkeligt tænkt til at blive for evigt.

2.4. asynctimerchan er væk for altid

Go 1.23 gjorde timer-kanaler ubufferede (synkrone) og tilbød asynctimerchan=1 som en vej tilbage til den gamle adfærd. I 1.27 er den indstilling permanent fjernet. Kanaler i pakken time er synkrone, punktum, ingen forhandling.

Det interessante er den politik, der blev introduceret sammen med det. En fjernet GODEBUG, som er efterladt i din go.mod, bryder ikke automatisk builden — den bryder kun, hvis den er sat til den gamle værdi:

1# go.mod indeholder: 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 indeholder: godebug asynctimerchan=0
6(builds fine)

Det betyder, at de eneste, der får en næse, er dem, der faktisk var afhængige af den fjernede adfærd. Alle, der satte den til den endelige standard og glemte det, kan fortsætte med ikke at tænke over det. Det er et eftertænksomt stykke API-arkæologi.

3. Standardbiblioteket

3.1. encoding/json/v2: Motoren blev udskiftet midt under flyvningen

Dette er det store. Over to års forslagsdiskussion er endelig landet.

Der er nu tre pakker, og forståelsen af opdelingen er størstedelen af kampen:

PakkeOpgave
encoding/jsonv1-API'et, du kender. Adfærd 100% uændret. Nu implementeret oven på v2
encoding/json/v2Semantisk behandling. Go-værdier ↔ JSON
encoding/json/jsontextSyntaktisk behandling. JSON som en tokenstrøm

Overskriften er, at encoding/json blev genopbygget på en helt anden implementering og opfører sig identisk. Sådan her:

1// Go 1.27s encoding/json.Unmarshal, forkortet
2func Unmarshal(data []byte, v any) error {
3	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}

Alle legacy v1-særheder blev kodet som en option, samlet i DefaultOptionsV1(), og v1-API'et anvender altid samlingen. Hvilket betyder, at dette opfører sig ens i 1.26 og 1.27:

1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)  // <nil>, map[a:2]

Ja, v1 accepterer stadig stille dublerede nøgler og tager den sidste. Det har altid gjort det. Det gør det stadig. Kompatibilitet betyder kompatibilitet med de dårlige dele også.

v2 har derimod meninger:

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

Dublerede nøgler og ugyldig UTF-8 afvises. Begge er reelt farlige, ikke blot uordentlige — når to parsere er uenige om, hvilken duplikeret nøgle der vinder, får du sikkerhedsfejl. CouchDB's CVE-2017-12635 var præcis dette: en JSON-body med to roles-nøgler, hvor valideringen læste den ene, og lagringslaget læste den anden. At afvise er det rigtige valg.

Options er variadiske:

 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// sorterede map-nøgler + indrykning
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// afvis ukendte felter — krævede tidligere en Decoder og 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 fleste ting, du tidligere løste med et tredjepartsbibliotek eller en håndskrevet MarshalJSON, er nu flag.

Og migreringshistorien er den bedste del. Senere options vinder, så du kan adoptere v2's strenghed én adfærd ad gangen:

1// bevar v1-semantik, men afvis dublerede nøgler ligesom v2 gør
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Du behøver ikke at portere en kodebase på 200.000 linjer til v2 for at holde op med at acceptere dublerede nøgler. Du vender én option. For alt stort er dette den realistiske vej.

Ydeevne: marshaling er nogenlunde på niveau, unmarshaling er betydeligt hurtigere. Hvis det går galt, genskaber GOEXPERIMENT=nojsonv2 den gamle implementering — og den fravalgsmulighed står også til at blive fjernet på et tidspunkt, så opret et issue i stedet for at slå dig til ro.

jsontext er det lavniveau-lag: Encoder/Decoder, der bevæger sig gennem JSON som Tokens og Values med en tilstandsmaskine, der holder dig ærlig. Grib fat i den, når du skriver en streaming-transformer eller et JSON-filter og ikke ønsker at materialisere Go-værdier overhovedet.

3.2. En standard uuid-pakke

Endelig. RFC 9562, i standardbiblioteket, intet go get påkrævet.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: de øverste 48 bit er et tidsstempel, så disse sorterer altid i oprettelsesrækkefølge
 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}

Hele API'et er New, NewV4, NewV7, Nil, Max, Parse, MustParse og én type: type UUID [16]byte. Det er alt. Du kan læse hele pakkedokumentationen, mens din kaffe køler af.

Detaljer, der er værd at kende:

  • UUID er [16]byte, så == virker, og den kan bruges direkte som map-nøgle. Samme design som google/uuid.
  • Nil og Max er funktioner, ikke variable. Fordi en pakkeniveau-var Nil UUID er en ladt pistol rettet mod din fod, og nogen, et sted, ville til sidst tildele den en værdi.
  • Tilfældige bits kommer fra en kryptografisk sikker generator.
  • Den implementerer encoding.TextMarshaler/TextUnmarshaler/TextAppender, så den passer direkte ind i JSON-structs.
  • NewV7 er den, du vil have til databasens primærnøgler. Tidsordnet betyder, at dit B-træ-indeks holder op med at fragmentere, hvilket v4-UUID'er er berygtede for.

Du kan nu slette en afhængighed. Hvis du har brug for v1/v3/v5 eller mere avancerede parse-options, har tredjepartsbiblioteker stadig en opgave.

3.3. crypto/mldsa: Post-kvante-signaturer, og de er enorme

Go 1.24 gav os crypto/mlkem til post-kvante nøgleudveksling. Go 1.27 bringer den anden halvdel: ML-DSA-signaturer, standardiseret som 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

Se på tallene. En enkelt signatur er 3.309 bytes. Ed25519 er 64. Det er en stigning på 50 gange, og den offentlige nøgle er næsten 2KB oven i det. Proppes nogle af disse ind i en certifikatkæde, begynder dit TLS-håndtryk at kræve sin egen MTU-strategi.

Dette er den reelle pris for kvanteresistens i dag, og det er grunden til, at ingen skifter alt over i morgen. Men det er nu i standardbiblioteket, hvilket er, hvor du vil have det, før du har brug for det.

Options.Context er domæneseparation: signér med den samme nøgle til forskellige formål, brug en anden kontekst for hvert, og en signatur fra én kontekst vil ikke verificeres i en anden. Eksemplet ovenfor viser netop det — samme nøgle, samme besked, anden kontekst, afvist.

PrivateKey implementerer crypto.Signer, så den passer ind i eksisterende interfaces. crypto/x509 håndterer ML-DSA-nøgler og -signaturer, og crypto/tls understøtter signaturskemaerne MLDSA44/MLDSA65/MLDSA87 i TLS 1.3.

Der er også SignDeterministic, som springer over tilfældigheden — nyttig til tests og reproducerbare builds.

3.4. simd: Vektorinstruktioner uden assembly

Go 1.26 introducerede den arkitekturspecifikke simd/archsimd som et eksperiment. Go 1.27 tilføjer simd — portabel og vektorbreddeagnostisk. Aktiveres med 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	// håndter halen med en delvis load/store — ingen skalar oprydningsløkke
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]

Designvalget, der betyder noget: vektorbredden er aldrig hårdkodet. Du spørger va.Len() ved kørsel. VectorBitSize() fortæller dig den faktiske bredde, Emulated() fortæller dig, om du fik rigtig hardware eller en høflig softwareefterligning. Min Mac rapporterede 128-bit NEON; en AVX-512-maskine rapporterer mere; en maskine uden noget rapporterer emulering, og koden kører stadig.

LoadFloat32sPart/StorePart fortjener særlig omtale. Den mest irriterende del af håndskrevet SIMD er altid den ujævne hale i slutningen af arrayet, og dette håndterer den uden en separat skalar løkke.

Stadig eksperimentel, API'et er stadig ustabilt, og det skal ikke i produktion. Men det faktum, at dette er Go-kode og ikke assembly eller cgo, er et virkelig stort anliggende.

3.5. hash/maphash.Hasher

Et nyt interface, der beskriver kontrakten mellem en værditype og hash-baserede containere:

1type Hasher[T any] interface {
2	Hash(*Hash, T)
3	Equal(x, y T) bool
4}

Hvorfor? Gos indbyggede map accepterer kun comparable-nøgler. Du kan ikke bruge en slice som nøgle. Du kan ikke definere "lige, ignorerer store/små bogstaver". Hasher løser begge dele på én gang:

 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)   // samme seed, ellers betyder intet af dette noget
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

For almindelig ==-semantik findes ComparableHasher[T]:

1hashOf(maphash.ComparableHasher[int]{}, 42)

Haken: Hasher er et interface, ikke en datastruktur. Der er endnu ingen standard hash-tabel eller Bloom-filter, der bruger det. Dette er grundlag for en fremtidig containerpakke. I dag ville du bruge det, når du bygger din egen struktur, eller forbruge en eksisterende implementering som go/types.Hasher (som lader dig bruge types.Type som map-nøgle og respekterer Identical).

Seed-håndtering er op til dig. Hvis hashOf ovenfor lavede en ny seed ved hvert kald, ville identiske værdier give forskellige hashes, og eksemplet ville udskrive false — hvilket netop er den fejl, jeg skrev i mit første forsøg. Én seed pr. container. (Seeden er randomiseret for at forhindre hash-flooding DoS-angreb, hvilket er grunden til, at den ikke bare er en konstant.)

3.6. httptest.NewTestServer + synctest.Sleep: Sleeper-hittet

Hvis du kun adopterer én ting fra denne udgivelse, så gør det til denne.

testing/synctest blev til stabil i Go 1.25 og gav os et falsk ur til test af samtidig kode. Men så snart din test rørte et rigtigt netværk, faldt illusionen sammen. Go 1.27's httptest.NewTestServer bruger et in-memory falsk netværk, så boblen forbliver intakt. Og synctest.Sleep (= time.Sleep + synctest.Wait) runder det af.

Her er en retry-test med eksponentiel 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 skal være præcis 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)

Læs det to gange. Den simulerede tre sekunders backoff mod en rigtig HTTP-server og var færdig på 0,00 sekunder. Og påstanden er elapsed == 3*time.Second — præcis tre sekunder, ikke "mindst tre sekunder, plus minus scheduler-jitter." Falske ure jitter ikke.

synctest.Sleep findes af en bestemt grund: hvis din test sover i samme varighed som koden under test, er det en gætteleg, hvilken der vågner først. synctest.Sleep sover og venter derefter, indtil alle andre goroutines i boblen er varigt blokerede, så du observerer systemet, efter det har stabiliseret sig.

Timeouts, retries, circuit breakers, rate limiters — hver tidsafhængig HTTP-klienttest, du ejer, kan blive hurtig og deterministisk. Hvis din testsuite i øjeblikket holdes sammen af time.Sleep(100 * time.Millisecond) og håb, er dette din udgang.

3.7. Ændringer i net/http, der faktisk påvirker produktion

Stille, men de vil dukke op i dine metrikker.

HTTP/1-responskroppe dræner automatisk ved Close. Ulæst indhold dræneres nu (op til en konservativ grænse), når du lukker kroppen, så forbindelsen kan genbruges. Hvilket betyder, at du endelig kan slette denne besværgelse, som alle har kopieret fra det samme Stack Overflow-svar siden 2016:

1// er ikke længere nødvendig
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

For de fleste programmer er dette en no-op eller en lille gevinst. Hvis det gør tingene værre, er du sandsynligvis i den kategori, release notes høfligt beskriver: Transport.MaxIdleConns sat til 0, eller en ny Client pr. forespørgsel, som helt omgår grænsen for inaktive forbindelser. Transport.DisableKeepAlives = true vil skjule det, men release notes' egentlige råd er, at "a deeper look would likely be beneficial", hvilket er Go-teamet for du har større problemer.

HTTP/2-klientprioritet (RFC 9218). Serveren respekterer nu klientens prioritetssignaler. Hvis du foretrak den gamle round-robin-planlægning, sæt Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Begrænser, hvor mange headerværdier serveren accepterer, med standard DefaultMaxHeaderValueCount. Endnu en dør lukket for "send ti tusind headers og se, hvad der sker."

ALPN på brugerleverede forbindelser. Hvis din net.Conn implementerer ConnectionState() tls.ConnectionState, vil Transport og Server udføre TLS ALPN-forhandling på den — så brugerdefinerede dialere, der går gennem en proxy, stadig kan forhandle HTTP/2.

3.8. Det små, der glæder

strings.CutLast / bytes.CutLast. Cut deler ved den første separator. Der fandtes ingen "sidste"-variant, så alle håndskrev LastIndex plus slicing, og cirka 30% af os fik en off-by-one første gang.

1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true

Fint til filendelser og til parsing af host:port — hvor du skal finde det sidste kolon, fordi IPv6-adresser er fulde af dem.

math/big.Int.Divide. Division med en eksplicit afrundingstilstand, der erstatter spørgsmålet "er det Quo eller Div, jeg vil have?":

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

Hvis du arbejder i et domæne, hvor afrundingsreglen er skrevet ned i en forskrift, er dette noget for dig.

url.URL.Clone og url.Values.Clone. Dybe kopier. Den gamle *u-shallow-kopi delte Userinfo-pointeren, hvilket producerede præcis den slags fejl, der tager en dag at finde og fem sekunder at rette.

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

Læsemetoderne i net.UnixConn returnerer nu io.EOF direkte i stedet for at pakke det ind i net.OpError. err == io.EOF virker nu, som det altid burde have gjort.

database/sql.ConvertAssign og driver.RowsColumnScanner. Funktioner til driverforfattere. Den første eksponerer de typekonverteringer, Rows.Scan udfører; den anden lader drivere scanne direkte ind i brugerdestinationer og springer en mellemliggende allokering over.

unicode 15 → 17. To versioner i ét hop. Adfærden for strengklassifikation og normalisering kan ændre sig subtilt. Hvis du har tests, der afhænger af det, finder du det ud af.

compress/flate er blevet hurtigere — og dens outputbytes kan afvige fra Go 1.26. Dette kaskaderer til archive/zip, compress/gzip, compress/zlib og image/png. Hvis du har golden-file-tests, der hasher komprimeret output, vil de gå i stykker, og det vil ikke være en regression. Tjek dette før du opgraderer, ikke under gennemgangen af hændelsen.

4. Værktøjskæden

4.1. go fix har fået flere modernizers

go fix er stille og roligt ved at blive et værktøj til kodemodernisering. Go 1.27 tilføjer atomictypes, embedlit, slicesbackward og unsafefuncs. Brug -diff til at forhåndsvise:

 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 }

Fire modernizers blev udløst på én lille fil:

  • embedlit — omskriver indlejrede literals til den nye syntaks fra §1.2
  • atomictypes — atomic.AddInt64(&x, 1) bliver til atomic.Int64.Add(1), hvilket gør ikke-atomisk adgang umulig og stille retter 32-bit-justeringsfejl, du ikke vidste, du havde
  • slicesbackward — baglæns løkker bliver til slices.Backward
  • waitgroupgo — ritualet Add/go/Done bliver til wg.Go (omdøbt fra 1.26's waitgroup for at undgå tvetydighed)

go tool fix help lister alle 26; go tool fix help <navn> forklarer én. Desuden blev fmtappendf fjernet "på grund af stilistiske bekymringer", hvilket er en smukt diplomatisk måde at beskrive, hvad der end skete i den issue-tråd.

At køre go fix ./... på en gammel kodebase er en virkelig tilfredsstillende eftermiddag. Læs diffen først, selvfølgelig.

4.2. go test kører stdversion som standard

Dette er den ændring, der mest sandsynligt vil afbryde din dag.

go test kører nu vet-checket stdversion som standard og markerer standardbibliotekssymboler, der er nyere end din go.mods go-direktiv tillader:

1// go.mod siger: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLast er et 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]

Dette dræber den klassiske fejlsituation, hvor alt virker på din maskine (seneste toolchain) og eksploderer i CI eller i en brugers ældre miljø. Hvis du udgiver biblioteker med et konservativt go-direktiv, er dette en gave.

4.3. go test -json fik OutputType

Linjer med "Action":"output" bærer nu et valgfrit "OutputType"-felt: "error", "error-continue" eller "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'

Output fra t.Log (intet felt), output fra t.Error (error) og frameworkgenerede linjer (frame) kan nu skelnes. Enhver, der har skrevet et regex for at finde ud af, hvilke linjer i testoutput der er "ægte", kan nu slette det.

4.4. Forbedringer i go doc

package@version-syntaks. Læs dokumentationen for en bestemt version uden at tilføje den til dit modul:

1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...

Praktisk til at tjekke, hvad der har ændret sig, før en opgradering.

-ex og udskrivning af eksempelkilde:

 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

Eksempelkilde og forventet output, i terminalen, uden at åbne en browserfane, som stadig vil være åben næste torsdag.

4.5. go mod tidy rydder op i dine require-blokke

For moduler på go 1.27 eller senere sammenfletter go mod tidy spredte require-blokke til højst to (direkte og indirekte), og bevarer vedhæftede kommentarer.

 1// Før
 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// Efter: 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)

Bemærk, at kommentaren // networking overlevede. Dette rydder primært op efter løsning af Git-merge-konflikter, hvor løse require-blokke opstår. Hvis dit team har mere end tre personer, der tilføjer afhængigheder, vil dine go.mod-konflikter blive mærkbart mere stille.

4.6. Småting

  • Understøttelse af bzr er fjernet. Hvis dette påvirker dig, vil jeg virkelig gerne høre historien.
  • go tool trace -http binder til localhost, når der kun gives en port. -http=:6060 lytter ikke længere på alle interfaces. Brug -http=0.0.0.0:6060, hvis du mente det. Dette matcher nu go tool pprof og forhindrer den lejlighedsvise utilsigtede offentlige profiler.
  • Response-filer (@file) understøttes af compile, link, asm, cgo, cover og pack, i et GCC-kompatibelt format. Til build-systemer, der overskrider længdegrænser for kommandolinjen — hej, Bazel.

5. Compiler, linker, platforme

Compiler. Relative filnavne i //line-direktiver løses nu mod mappen for den indeholdende fil, i overensstemmelse med go/scanner. Relevant, hvis du skriver kodegeneratorer.

Navne på funktionsliterals (closures) er også enklere nu — det samme navn uanset inlining, og flere forekomster af den samme literal kan dele kode i binæren. Ingen funktionel ændring, undtagen: kode, der sammenligner funktionsidentitet via reflect.Value.Pointer, vil se "lige" oftere end før. Denne sammenligning har aldrig været gyldig, men hvis du har den, er det nu, den begynder at lyve for dig højere.

Linker. Nye -macos- og -macsdk-options sætter OS- og SDK-versionerne i macOS' LC_BUILD_VERSION-load-kommando.

Platforme.

  • Darwin kræver nu macOS 13 Ventura eller nyere, som annonceret i 1.26. Tjek dine CI-runners.
  • PowerPC (GOOS=linux GOARCH=ppc64) er skiftet til ELFv2-ABI'et. Kræver Linux-kerne 3.13+ (RHEL7 bagportet til 3.10). Cgo, PIE og ekstern linking understøttes nu. Hvis du bruger cgo, men har brug for en statisk ren Go-binær, sæt CGO_ENABLED=0.

6. Opgraderingstjekliste

Go 1.27 tager kompatibilitet alvorligt, men tjek disse, før du hæver versionen:

  • Golden-file-tests på komprimeret output. compress/flate har ændret encoders; gzip/zip/png-bytes kan afvige.
  • Tests, der matcher JSON-fejlmeddelelsesstrenge. Adfærden er identisk; fejlteksten er det ikke.
  • Fjernede GODEBUG'er i go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — disse fejler builden kun, hvis de er sat til deres gamle værdier.
  • Overtrædelser af stdversion. go test fanger disse nu. Kør det tidligt, hvis du vedligeholder et bibliotek med et konservativt go-direktiv.
  • CI-runners med macOS 12 eller ældre. Understøttelsen er væk.
  • Tests, der hviler på navne på funktionsliterals, og reflect.Value.Pointer-sammenligninger af funktioner.
  • Transport.MaxIdleConns = 0 eller en Client pr. forespørgsel. Kombineret med automatisk dræning af responskroppe kan dette blive langsommere.

Afsluttende overvejelser

Go 1.27 er den udgivelse, hvor en masse udskudt vedligeholdelse forfaldt på én gang.

Generiske metoder var den manglende brik i 1.18-generics-arbejdet. Feltvælgere i struct-literals lukkede et elleve år gammelt issue. encoding/json/v2 var to års forslagsdiskussion. Og uuid fyldte et hul, som stort set alle Go-projekter havde lappet med den samme tredjepartsafhængighed i et årti.

Hvis du vil have en prioriteret rækkefølge for at få reel værdi ud af dette:

  1. httptest.NewTestServer + synctest.Sleep — tidsafhængige HTTP-tests bliver hurtige og deterministiske. Det bedste forhold mellem indsats og udbytte i hele udgivelsen.
  2. Goroutine-lækageprofilen — eksponér ét endpoint i staging og forbered dig på at blive ydmyget.
  3. Goroutine-labels i tracebacks — tre linjer pprof.Do ved dit indgangspunkt for forespørgsler gør din næste stack-dump kl. 3 om natten læsbar.
  4. go fix ./... — lad værktøjet klare modernisering.
  5. encoding/json/v2 — ingen hast. Adopter det én option ad gangen.

Hent go1.27rc3 og kør din testsuite mod den nu. Alt på den tjekliste er betydeligt mere behageligt at opdage en tirsdag eftermiddag end under en hændelse.

Referencer