GoSuda

Go 1.27, eller hvordan jeg lærte at holde op med at bekymre mig og elske Generic Methods

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 på den måde, hvor "vi har tilføjet en ny funktion til slices" – det har ændret sig reelt. Generiske metoder er nu en del af sproget. Et elleve år gammelt problem er blevet lukket. encoding/json blev i al stilhed erstattet med en ny motor, mens flyet var i luften.

Hvert eksempel herunder er kørt på go1.27rc3 (darwin/arm64). Hver output-blok er et ægte output, kopieret direkte, inklusive fejlmeddelelser. Hvis noget ser mærkeligt ud, er det fordi, det er præcis, hvad compileren sagde.

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

1. Sproget har ændret sig (ja, virkelig)

1.1. Generiske metoder

Siden Go 1.18 har vi haft generiske typer, og siden Go 1.18 har vi haft "Samtalen". Den lyder således:

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

method must have no type parameters

"...fint. Så må det blive en funktion på pakkeniveau."

Metoder kunne kun benytte typeparametre deklareret af receiveren. Din egen metode kunne ikke introducere nye. Derfor blev enhver generisk transformation forvist til pakke-scope, hvor den lå ved siden af sytten andre fritflydende hjælpere med navne som en variation af MapSlice.

Go 1.27 løser dette:

 1type List[E any] []E
 2
 3// F er en typeparameter deklareret af selve metoden
 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 helt 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) }

Nu, før du refaktorerer hele din kodebase i eftermiddag, er der 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.

Det er den anden begrænsning, der vil give dig problemer:

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 fungerer ikke sammen. Dette er ikke Go-teamet, der er nærige — det skyldes, at en generisk metode har uendeligt mange instansieringer, og en interface-metodetabel skal være endelig og kendt ved kompileringstidspunktet. Du kan ikke placere en uendelig ting i en endelig tabel. Universet sagde nej.

Praktisk læsning: Generiske metoder er til konkrete typer med utility-prægede API'er. Alt, der skal gemme sig bag et interface, kræver stadig den gamle tilgang.

Standardbiblioteket har allerede taget det i brug. math/rand/v2's Rand har fået 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 rand.N på pakkeniveau generisk, hvilket betød, at man var tvunget til at bruge den globale kilde. Nu får din egen seedede *Rand den samme bekvemmelighed. En lille ting. En god ting.

1.2. Struct Literal Field Selectors, eller: Issue #9859 får endelig fred

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

Issue #9859 blev oprettet i 2015. Den er nu lukket. Et sted sidder en gopher, der har skiftet karriere to gange siden da, og modtager 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 promoverede 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 embeddede Object. Tidligere ville du skulle skrive Line{Object: Object{name: "diagonal"}, ...}.

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

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

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 som feltadgang nu". Det er specifikt "implicit promoverede feltnavne er tilladte". Lidt mere snævert end overskriften antyder.

(b) Du kan ikke angive et embedded felt og noget, der er promoveret ud af det, på samme tid.

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

Rimeligt. Du gav den to modstridende oplysninger 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 skal du have en pointer at følge, og ved literal-konstruktionstidspunktet findes der endnu ingen. Også fair.

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

1.3. Typeinferens for funktioner er blevet mindre vilkårlig

Typeinferens for generiske funktioner fungerede før i tiden nogle steder, men ikke andre, uden nogen synlig logik bag, hvad der var hvad. Tildeling til en variabel? Fint. At placere den samme funktion i et struct-felt? Compiler-fejl, skriv venligst double[int], som om det var 2022.

Go 1.27 får inferens til at fungere 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, instansieret som func(int) int tre steder og func(float64) float64 på et fjerde, udelukkende baseret på kontekst. Hvis du skriver option-structs eller handler-tabeller fulde af funktionsværdier, er en del [T]-støj ved at forsvinde fra din kodebase.

2. Runtime: Gratis ydeevne og en lækage-detektor

2.1. Størrelsesspecifik allokering

Compileren udsender nu kald til størrelsesspecifikke allokeringsrutiner for små objekter. Release-noterne hævder op til 30 % besparelse på allokeringer under 80 bytes og omkring 1 % generelt for allokerings-tunge programmer.

Jeg tror ikke på release-noter 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# default (størrelsesspecifik malloc aktiveret)
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

30–35 % hurtigere i en mikrobechmark, der ikke gør andet end at allokere på Apple Silicon. Det vil sige: 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 faktisk arbejde dække det meste. Påstanden om ~1 % er den ærlige.

Prisen er binær størrelse. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Omkring 50 KB, fast, uanset dit program. Du kan fravælge det med GOEXPERIMENT=nosizespecializedmalloc, men denne flugtvej er planlagt til fjernelse i Go 1.28, så betragt det som en workaround til fejlrapportering snarere end en livsstil.

2.2. Goroutine-lækageprofilen er nu reel

Eksperimentel i Go 1.26, generelt tilgængelig i 1.27. goroutineleakprofile GOEXPERIMENT 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 et fil- og linjenummer, der peger præcis på, hvor du gjorde det.

Tricket bag dette er virkelig smart: det genbruger garbage collector'ens reachability-analyse. Hvis goroutine G er blokeret på primitiv P, og P ikke er tilgængelig fra nogen kørbar goroutine (eller noget, de goroutines kunne vække), så kan intet nogensinde røre P igen, så G vågner aldrig. Det er ikke en heuristik — det er et bevis.

Det samme design giver dig begrænsningen gratis: hvis kanalen eller mutex'en er tilgængelig via en global variabel, eller via en lokal variabel i en goroutine, der stadig kører, kan GC stadig se den, så runtime kan ikke konkludere noget. Bemærk, at selv legetøjseksemplet ovenfor kræver to runtime.GC()-kald for at rapportere. Det fanger ikke alt.

Det vil dog fange den klassiske "glemte at annullere contexten, worker-goroutine lever evigt" — hvilket er de fleste lækager det meste af tiden.

Hvis du importerer net/http/pprof, findes den også på /debug/pprof/goroutineleak. Forbind det til staging, tjek det ugentligt, og bliv stille og roligt forfærdet.

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

2.3. Tracebacks fortæller dig nu, hvilken request der døde

For moduler, der deklarerer Go 1.27 eller nyere, inkluderer traceback-overskriftslinjer 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...

Den vigtige detalje: labels nedarves af underordnede 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 identiske ud, 4.000 goroutines tagget med, hvilken request og hvilken tenant de tilhører. Tre linjer pprof.Do ved dit request-indgangspunkt giver dig dette.

Da labels kan indeholde ting, du hellere vil undgå at dumpe til stderr, slår GODEBUG=tracebacklabels=0 det fra, og den fravalgsmulighed er eksplicit beregnet til at blive ved med at eksistere for evigt.

2.4. asynctimerchan er væk for altid

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

Den interessante del er politikken, der blev introduceret sammen med den. En fjernet GODEBUG, der er tilbage i din go.mod, ødelægger ikke automatisk buildet — det går kun i stykker, 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(bygger fint)

Hvilket betyder, at de eneste, der får en skideballe, er dem, der faktisk stolede på den fjernede adfærd. Alle, der satte den til den endelige standard og glemte alt om den, kan fortsætte med ikke at tænke på den. Det er et tankevækkende stykke API-arkæologi.

3. Standardbibliotek

3.1. encoding/json/v2: De udskiftede motoren midt i flyvningen

Dette er den store nyhed. Mere end to års forslagsdiskussion er endelig landet.

Der er nu tre pakker, og at forstå opdelingen er det halve af kampen:

PakkeOpgave
encoding/jsonDet v1 API, du kender. Adfærden er 100% uændret. Nu implementeret oven på v2
encoding/json/v2Semantisk behandling. Go-værdier ↔ JSON
encoding/json/jsontextSyntaktisk behandling. JSON som en token-strøm

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

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

Hver eneste legacy v1-særhed blev kodet som en option, bundlet ind i DefaultOptionsV1(), og v1 API'et anvender altid bundtet. 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 lydløst duplikerede nøgler og tager den sidste. Det gjorde det altid. Det gør det stadig. Kompatibilitet betyder også kompatibilitet med de dårlige dele.

v2 har derimod holdninger:

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

Duplikerede nøgler og ugyldig UTF-8 afvises. Begge er oprigtigt farlige, ikke bare rodede — 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 validatoren læste den ene, og lagringslaget læste den anden. At afvise dem er den rette beslutning.

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 migrationshistorien 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 duplikerede 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 porte en 200k-linjers kodebase til v2 for at stoppe med at acceptere duplikerede nøgler. Du skifter én option. For alt større er dette den realistiske vej.

Ydeevne: marshaling er omtrent på niveau, unmarshaling er væsentligt hurtigere. Hvis noget går galt, gendanner GOEXPERIMENT=nojsonv2 den gamle implementering — og den fravalgsmulighed er også på vej mod huggeblokken, så indsend en fejlrapport frem for at slå dig til tåls.

jsontext er det lavniveau-lag: Encoder/Decoder der går igennem JSON som Tokens og Values med en tilstandsmaskine, der holder dig ærlig. Brug den, når du skriver en stream-transformer eller et JSON-filter og slet ikke ønsker at materialisere Go-værdier.

3.2. En standard uuid-pakke

Endelig. RFC 9562 i standardbiblioteket, ingen 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 bits 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 det. Du kan læse hele pakkedokumentationen, mens din kaffe køler af.

Detaljer værd at kende:

  • UUID er [16]byte, så == virker, og den er direkte brugbar som en map-nøgle. Samme design som google/uuid.
  • Nil og Max er funktioner, ikke variabler. Fordi en var Nil UUID på pakkeniveau er en ladt pistol rettet mod din fod, og før eller siden ville nogen tildele noget til den.
  • 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 database-primærnøgler. Tidsordning betyder, at dit B-træ-indeks stopper 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 parsing-muligheder, har tredjepartsbiblioteker stadig en berettigelse.

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å de tal. En enkelt signatur er 3.309 bytes. Ed25519 er 64. Det er en 50x stigning, og den offentlige nøgle er næsten 2 KB oveni. Put et par stykker af disse ind i en certifikatkæde, og dit TLS-handshake begynder at få brug for sin egen MTU-strategi.

Dette er den faktiske pris for kvante-resistens i dag, og det er grunden til, at ingen skifter alt over i morgen. Men den er i standardbiblioteket nu, hvilket er, hvor du vil have den, før du får brug for den.

Options.Context er domæneseparation: signer med den samme nøgle til forskellige formål, brug en anden kontekst til hver, og en signatur fra én kontekst vil ikke verificere i en anden. Eksemplet ovenfor viser præcis det — samme nøgle, samme besked, forskellig 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 MLDSA44/MLDSA65/MLDSA87-signaturskemaerne i TLS 1.3.

Der er også SignDeterministic, som springer tilfældigheden over — praktisk 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 simdportabel og vektor-bredde-agnostisk. Aktiver 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 resten med en delvis load/store — ingen skalær 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]

Det designvalg, der betyder noget: vektorbredden er aldrig hardcoded. Du spørger va.Len() ved runtime. VectorBitSize() fortæller dig den faktiske bredde, Emulated() fortæller dig, om du har fået ægte hardware eller en høflig software-efterligning. 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 ende af arrayet, og dette håndterer det uden en separat skalær løkke.

Stadig eksperimentelt, API stadig ustabilt, sæt det ikke i produktion. Men det faktum, at dette er Go-kode og ikke assembly eller cgo, er en oprigtig stor sag.

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? Go's indbyggede map accepterer kun comparable nøgler. Du kan ikke bruge en slice som nøgle. Du kan ikke definere "lig med, uanset 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)

Hagen: Hasher er et interface, ikke en datastruktur. Der er endnu ikke nogen standard-hashtabel eller Bloom-filter, der bruger det. Dette er fundamentet for en fremtidig container-pakke. I dag ville du bruge den, når du bygger din egen struktur, eller bruge en eksisterende implementering som go/types.Hasher (hvilket lader dig bruge types.Type som en map-nøgle, der respekterer Identical).

Seed-styring er op til dig. Hvis hashOf ovenfor lavede et frisk seed ved hvert kald, ville identiske værdier hashe forskelligt, og eksemplet ville udskrive false — hvilket præcis er den bug, jeg skrev i mit første forsøg. Ét seed per container. (Seedet er randomiseret for at modvirke hash-flooding DoS-angreb, hvilket er grunden til, at det ikke bare er en konstant.)

3.6. httptest.NewTestServer + synctest.Sleep: Det skjulte hit

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

testing/synctest blev færdigudviklet i Go 1.25 og gav os et falsk ur til test af concurrent kode. Men i det øjeblik din test rørte ved et rigtigt netværk, kollapsede illusionen. Go 1.27's httptest.NewTestServer bruger et in-memory falsk netværk, så boblen forbliver intakt. Og synctest.Sleep (= time.Sleep + synctest.Wait) fuldender det.

Her er en retry-med-eksponentiel-backoff-test:

 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 bør 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 blev færdig på 0,00 sekunder. Og assertionen er elapsed == 3*time.Secondpræcis tre sekunder, ikke "mindst tre sekunder, plus/minus scheduler-jitter". Falske ure jitter ikke.

synctest.Sleep eksisterer af en specifik grund: hvis din test sover i samme varighed som den kode, der testes, er det umuligt at sige, hvem der vågner først. synctest.Sleep sover og derefter venter, indtil enhver anden goroutine i boblen er stabilt blokeret, så du observerer systemet, efter det har sat sig.

Timeouts, retries, circuit breakers, rate limiters — hver tidsafhængig HTTP-klient-test, 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 vej ud.

3.7. net/http-ændringer, der faktisk påvirker produktionen

Stille, men de vil dukke op i dine målinger.

HTTP/1 response-bodies auto-drains ved Close. Ulæst indhold bliver nu drænet (op til en konservativ grænse), når du lukker bodyen, 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// ikke længere nødvendigt
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, som release-noterne høfligt beskriver: Transport.MaxIdleConns sat til 0, eller en ny Client per request, hvilket omgår grænsen for inaktive forbindelser fuldstændigt. Transport.DisableKeepAlives = true vil dække over det, men release-noternes faktiske råd er, at "et dybere kig sandsynligvis ville være gavnligt", hvilket er Go-team-sprog for du har større problemer.

HTTP/2 klient-prioritet (RFC 9218). Serveren respekterer nu klientens prioritets-signaler. Hvis du foretrak den gamle round-robin-planlægning, brug Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Begrænser hvor mange header-værdier serveren vil acceptere, med DefaultMaxHeaderValueCount som standard. 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å custom dialers, der går gennem en proxy, stadig kan forhandle HTTP/2.

3.8. De små ting, der skaber glæde

strings.CutLast / bytes.CutLast. Cut splitter ved den første separator. Der var ingen "sidste"-variant, så alle håndbyggede LastIndex plus slicing, og omkring 30 % af os fik en off-by-one fejl i første forsøg.

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

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

math/big.Int.Divide. Division med en eksplicit afrundings-tilstand, der erstatter møntkastet "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 inden for et domæne, hvor afrundingsreglen er skrevet ned i et regulativ, er dette for dig.

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

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

net.UnixConn read-metoder returnerer nu io.EOF direkte i stedet for at pakke den ind i net.OpError. err == io.EOF virker nu, som det altid burde have gjort.

database/sql.ConvertAssign og driver.RowsColumnScanner. Driver-forfatter-funktioner. Den første eksponerer de typekonverteringer, som 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 spring. String-klassificering og normaliseringsadfærd kan ændre sig subtilt. Hvis du har tests, der afhænger af det, vil du opdage det.

compress/flate er blevet hurtigere — og dens output-bytes 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 er ikke en regression. Tjek for dette, før du opgraderer, ikke under incident-reviewet.

4. Toolchain

4.1. go fix har fået flere modernisatorer

go fix er i stilhed ved at blive et værktøj til kodemodernisering. Go 1.27 tilføjer atomictypes, embedlit, slicesbackward og unsafefuncs. Brug -diff til at få vist en forhåndsvisning:

 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 modernisatorer blev affyret på én lille fil:

  • embedlit — omskriver embeddede literaler ved hjælp af den nye syntaks fra §1.2
  • atomictypesatomic.AddInt64(&x, 1) bliver til atomic.Int64.Add(1), hvilket gør ikke-atomisk adgang umulig og i stilhed fikser 32-bit alignment-fejl, du ikke vidste, du havde
  • slicesbackward — baglæns løkker bliver til slices.Backward
  • waitgroupgoAdd/go/Done-ritualet bliver til wg.Go (omdøbt fra 1.26's waitgroup for at undgå flertydighed)

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

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

4.2. go test kører stdversion som standard

Dette er ændringen, der er mest sandsynlig til at afbryde din dag.

go test kører nu stdversion vet-tjekket som standard, hvilket markerer symboler fra standardbiblioteket, der er nyere end det, dit go.mod's 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 fejltilstand, hvor alt virker på din maskine (nyeste toolchain) og eksploderer i CI eller i et ældre miljø hos en bruger. Hvis du udgiver biblioteker med et konservativt go-direktiv, er dette en gave.

4.3. go test -json har fået OutputType

"Action":"output"-linjer 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'

t.Log-output (intet felt), t.Error-output (error) og framework-genererede linjer (frame) er nu mulige at skelne. Enhver, der har skrevet en regex for at finde ud af, hvilke linjer af test-output der er "rigtige", kan nu slette den.

4.4. go doc-forbedringer

package@version-syntaks. Læs dokumentationen for en specifik 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 ændrede sig før en opgradering.

-ex og udskrivning af eksempel-kildekode:

 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

Eksempel-kildekode og forventet output, i terminalen, uden at åbne en browser-fane, der 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 nyere samler go mod tidy spredte require-blokke til højst to (direkte og indirekte), mens vedhæftede kommentarer bevares.

 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 // networking-kommentaren overlevede. Dette rydder primært op efter Git merge-konfliktløsning, hvilket er der, hvor løse require-blokke opstår. Hvis dit team har mere end tre personer, der tilføjer afhængigheder, er dine go.mod-konflikter ved at blive mærkbart mere støjsvage.

4.6. Diverse

  • bzr-understøttelse fjernet. Hvis dette påvirker dig, vil jeg oprigtigt gerne høre historien.
  • go tool trace -http binder til localhost, når kun en port er angivet. -http=:6060 lytter ikke længere på alle interfaces. Brug -http=0.0.0.0:6060, hvis det var din hensigt. 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 kommandolinjens længdegrænser — hej, Bazel.

5. Compiler, Linker, Ports

Compiler. Relative filnavne i //line-direktiver opløses nu i forhold til mappen for den indeholdende fil, hvilket matcher go/scanner. Relevant hvis du skriver kodegeneratorer.

Funktions-literal (closure) symbolnavne er også simplere nu — det samme navn uanset inlining, og flere instanser af den samme literal kan dele kode i binærfilen. Ingen funktionel ændring, undtagen: kode, der sammenligner funktionsidentitet via reflect.Value.Pointer, vil se "lig med" oftere end før. Den sammenligning var aldrig valid, men hvis du har den, er det nu, den begynder at lyve for dig højere.

Linker. Nye -macos og -macsdk indstillinger sætter OS- og SDK-versioner i macOS LC_BUILD_VERSION load-kommandoen.

Ports.

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

6. Tjekliste for opgradering

Go 1.27 tager kompatibilitet seriøst, men tjek disse, før du opgraderer versionen:

  • Golden-file-tests på komprimeret output. compress/flate ændrede encodere; gzip/zip/png-bytes kan afvige.
  • Tests, der matcher JSON-fejlmeddelelsesstrenge. Adfærden er identisk; fejlteksten er det ikke.
  • Fjernede GODEBUGs i go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — disse fejler kun buildet, hvis de er sat til deres gamle værdier.
  • stdversion-overtrædelser. go test fanger dem nu. Kør den tidligt, hvis du vedligeholder et bibliotek med et konservativt go-direktiv.
  • macOS 12 eller ældre CI-runners. Understøttelse er væk.
  • Tests, der asserterer på funktions-literal symbolnavne, og reflect.Value.Pointer funktions-sammenligninger.
  • Transport.MaxIdleConns = 0 eller en Client per request. Kombineret med auto-draining response-bodies kan dette blive langsommere.

Afsluttende tanker

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

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

Hvis du vil have en prioritetsrækkefølge for rent faktisk at få værdi ud af dette:

  1. httptest.NewTestServer + synctest.Sleep — tidsafhængige HTTP-tests bliver hurtige og deterministiske. Bedste indsats-til-gevinst-ratio i hele releasen.
  2. Goroutine-lækageprofilen — eksponer ét endpoint i staging og forbered dig på at blive ydmyget.
  3. Traceback goroutine-labels — tre linjer pprof.Do ved dit request-indgangspunkt forvandler dit næste stack-dump kl. 03:00 til noget læsbart.
  4. go fix ./... — lad værktøjet stå for moderniseringen.
  5. encoding/json/v2 — ingen hast. Adopter den én option ad gangen.

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

Referencer