GoSuda

Go 1.27, eller hvordan jeg lærte å slutte å bekymre meg og elske Generic Methods

By iwanhae
views ...

Oversikt over Go 1.27

Go 1.27 lanseres i august 2026, og for en gangs skyld har selve språket endret seg. Ikke slik at «vi la til en ny funksjon i slices» – det har faktisk endret seg. Generiske metoder er nå inkludert. Et elleve år gammelt problem er løst. encoding/json ble i stillhet erstattet med en ny motor mens flyet var i luften.

Alle eksemplene nedenfor ble kjørt på go1.27rc3 (darwin/arm64). Hver utdatablokk er reell utdata, kopiert og limt inn, inkludert feilmeldingene. Hvis noe ser merkelig ut, er det fordi det var det kompilatoren faktisk rapporterte.

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

1. Språket har endret seg (ja, faktisk)

1.1. Generiske metoder

Siden Go 1.18 har vi hatt generiske typer, og siden Go 1.18 har vi hatt diskusjonen. Den lyder slik:

«La meg bare skrive en Map-metode på min slice-type –»

method must have no type parameters

«... greit. Da blir det en funksjon på pakkenivå.»

Metoder kunne kun bruke typeparametre deklarert av mottakeren (receiver). Din egen metode kunne ikke introdusere nye. Derfor ble hver generisk transformasjon forvist til pakkeomfanget, hvor den satt ved siden av sytten andre frittstående hjelpefunksjoner med navn som en variant av MapSlice.

Go 1.27 løser dette:

 1type List[E any] []E
 2
 3// F er en typeparameter deklarert av 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}

Mottakeren trenger ikke engang å være generisk. En enkel struct kan ha 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 kodebasen din i ettermiddag, er det to begrensninger, og de er viktigere enn de ser ut til:

  1. Grensesnittmetoder (interface methods) kan ikke deklarere typeparametre.
  2. Generiske metoder kan ikke implementere grensesnittmetoder.

Det siste punktet er det som vil skape 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 utsending (dynamic dispatch) fungerer ikke sammen. Dette er ikke Go-teamet som er gjerrige – det er fordi en generisk metode har uendelig mange instansieringer, og en tabell for grensesnittmetoder må være endelig og kjent ved kompileringstidspunktet. Du kan ikke plassere noe uendelig i en endelig tabell. Universet sa nei.

Praktisk tolkning: generiske metoder er for konkrete typer med verktøylignende API-er. Alt som må skjule seg bak et grensesnitt trenger fortsatt den gamle tilnærmingen.

Standardbiblioteket har allerede tatt dette i bruk. math/rand/v2 sin Rand har fått 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 bare rand.N på pakkenivå generisk, noe som betydde bruk av den globale kilden. Nå får din egen seedede *Rand samme bekvemmelighet. En liten, men fin ting.

1.2. Struct-literal feltvelgere, eller: Issue #9859 får endelig hvile

Embedding gir deg u.ID. Embedding gir deg ikke User{ID: 1}. I stedet gir det deg User{Base: Base{ID: 1}}, som er Go sin måte å spørre om du virkelig mente det.

Issue #9859 ble opprettet i 2015. Den er nå lukket. Et eller annet sted får en gopher, som har skiftet karriere to ganger siden den gang, et GitHub-varsel.

 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: bruk det promoverte feltet 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 skrevet Line{Object: Object{name: "diagonal"}, ...}.

Nå til detaljene, som jeg sjekket ved å faktisk kompilere alt sammen.

(a) Nøkkelen er fortsatt en enkel identifikator. Du kan ikke skrive en vilkårlig velgersti (selector path).

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å funksjonen er ikke «nøkler fungerer som felttilgang nå». Det er spesifikt «implisitt promoverte feltnavn er tillatt». Litt smalere enn overskriften antyder.

(b) Du kan ikke spesifisere et embedded felt og noe promoverte ut av det samtidig.

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

Rimelig. Du ga den to motstridende instruksjoner om samme minneområde, og den valgte å ikke gjette.

(c) Pointer-embedding er ikke inkludert.

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

For å følge en peker trenger du en peker å følge, og ved konstruksjonstidspunktet for literalen finnes det ingen ennå. Også rettferdig.

Gode nyheter: go fix vil skrive om dine gamle literaler for deg. Mer om det senere.

1.3. Funksjonstypeinferens ble mindre vilkårlig

Typeinferens for generiske funksjoner fungerte tidligere noen steder og ikke andre, uten noen tydelig prinsipp bak hva som var hva. Tilordning til en variabel? Greit. Plassere den nøyaktige samme funksjonen i et struct-felt? Kompileringsfeil, vennligst skriv double[int] som om det er 2022.

Go 1.27 gjør at inferens fungerer i alle kontekster der måltypen er entydig kjent:

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

Én double, instansiert som func(int) int tre steder og func(float64) float64 på et fjerde, utelukkende basert på kontekst. Hvis du skriver option-structs eller handler-tabeller fulle av funksjonsverdier, vil en mengde [T]-støy forsvinne fra kodebasen din.

2. Runtime: Gratis ytelse og en lekkasjedetektor

2.1. Størrelsesspesialisert allokering

Kompilatoren genererer nå kall til størrelsesspesialiserte allokeringsrutiner for små objekter. Utgivelsesnotatene hevder opptil 30 % reduksjon i allokeringer under 80 bytes, og ca. 1 % totalt for allokeringsintensive programmer.

Jeg tror ikke på utgivelsesnotater uten å måle selv, 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ørrelsesspesialisert malloc på)
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 % raskere i en mikrobenchemark som ikke gjør annet enn å allokere på Apple Silicon. Det vil si: dette er det beste tilfellet, oppnådd under laboratorieforhold av en benchmark designet for å få tallet til å se bra ut. I et reelt program vil GC og faktisk arbeid skjule det meste av dette. Påstanden om ca. 1 % er den ærlige.

Kostnaden er binærstørrelse. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Ca. 50 KB, fast, uavhengig av programmet ditt. Du kan velge bort dette med GOEXPERIMENT=nosizespecializedmalloc, men denne utveien er planlagt fjernet i Go 1.28, så behandle det som en løsning for feilrapportering snarere enn en livsstil.

2.2. Goroutine-lekkasjeprofilen er reell nå

Eksperimentell i Go 1.26, generelt tilgjengelig i 1.27. goroutineleakprofile GOEXPERIMENT er borte; det bare fungerer.

 1func leak() {
 2	ch := make(chan int) // ingen vil noen gang 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 goroutiner, permanent fastlåst, med fil og linjenummer som peker nøyaktig på hvor du gjorde det.

Trikset bak dette er genuint smart: den gjenbruker søppelhåndtererens (GC) tilgjengelighetsanalyse. Hvis goroutine G er blokkert på primitiv P, og P er utilgjengelig fra enhver kjørbar goroutine (eller alt de gorutinene kan vekke), så kan ingenting noen gang røre P igjen, så G vil aldri våkne. Det er ikke en heuristikk – det er et bevis.

Samme design gir deg begrensningen gratis: hvis kanalen eller mutexen er tilgjengelig via en global variabel, eller via en lokal variabel i en goroutine som fortsatt kjører, kan GC-en fortsatt se den, så runtime kan ikke konkludere med noe. Merk at selv leketøyeksemplet ovenfor trenger to runtime.GC()-kall for å rapportere. Den vil ikke fange alt.

Den vil imidlertid fange den klassiske «glemte å kansellere konteksten, arbeider-goroutine lever evig» – som utgjør de fleste lekkasjer mesteparten av tiden.

Hvis du importerer net/http/pprof, er den også tilgjengelig på /debug/pprof/goroutineleak. Koble den til staging, sjekk den ukentlig, og bli stille skrekkslagen.

Dette arbeidet ble bidratt av Vlad Saioc hos Uber, som fortjener en valgfri forfriskning.

2.3. Tracebacks forteller deg nå hvilken forespørsel som døde

For moduler som deklarerer Go 1.27 eller nyere, inkluderer traceback-overskriftslinjer nå runtime/pprof goroutine-etiketter.

 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 viktige detaljen: etiketter arves av underordnede goroutiner. Goroutine 3 satte aldri en etikett. Den fikk en fra sin forelder.

Så neste gang produksjonsmiljøet låser seg og noen sender SIGQUIT til prosessen, får du i stedet for 4 000 identiske goroutiner, 4 000 goroutiner tagget med hvilken forespørsel og hvilken leietaker de tilhører. Tre linjer med pprof.Do ved inngangspunktet for forespørselen din kjøper deg dette.

Siden etiketter kan inneholde ting du ikke ønsker å dumpe til stderr, slår GODEBUG=tracebacklabels=0 det av, og den utveien er eksplisitt ment å bestå for alltid.

2.4. asynctimerchan er borte for godt

Go 1.23 gjorde timer-kanaler ubufrede (synkrone) og tilbød asynctimerchan=1 som en vei tilbake til gammel oppførsel. I 1.27 er den innstillingen permanent fjernet. time-pakkens kanaler er synkrone, punktum, ingen forhandling.

Det interessante er policyen som ble introdusert sammen med den. En fjernet GODEBUG som ligger igjen i din go.mod bryter ikke byggingen automatisk – den bryter bare hvis den er satt til den gamle verdien:

1# go.mod inneholder: 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 inneholder: godebug asynctimerchan=0
6(bygger fint)

Noe som betyr at de eneste som får kjeft, er de som faktisk stolte på den fjernede oppførselen. Alle som satte den til den endelige standardverdien og glemte det, kan fortsette å ikke tenke på det. Det er et gjennomtenkt stykke API-arkeologi.

3. Standardbiblioteket

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

Dette er den store nyheten. To pluss år med forslagsdiskusjon landet endelig.

Det er nå tre pakker, og å forstå oppdelingen er halve kampen:

PakkeOppgave
encoding/jsonv1-API-et du kjenner. Oppførsel 100 % uendret. Nå implementert oppå v2
encoding/json/v2Semantisk prosessering. Go-verdier ↔ JSON
encoding/json/jsontextSyntaktisk prosessering. JSON som en token-strøm

Overskriften er at encoding/json ble bygget på nytt med en helt annen implementering og oppfører seg identisk. Her er hvordan:

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

Hver eneste v1-særhet ble kodet som et alternativ, samlet i DefaultOptionsV1(), og v1-API-et bruker alltid denne samlingen. Noe som betyr at dette oppfører seg likt 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 aksepterer fortsatt duplikate nøkler stille og tar den siste. Det gjorde den alltid. Det gjør den fortsatt. Kompatibilitet betyr kompatibilitet med de dårlige delene også.

v2 har derimot 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

Duplikate nøkler og ugyldig UTF-8 avvises. Begge er genuint farlige, ikke bare uordentlige – når to parsere er uenige om hvilken duplikatnøkkel som vinner, får du sikkerhetshull. CouchDBs CVE-2017-12635 var nettopp dette: en JSON-kropp med to roles-nøkler, der validatoren leste én og lagringslaget leste den andre. Å avvise er det riktige valget.

Opsjoner 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// sorterte map-nøkler + innrykk
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// avvis ukjente felt — krevde 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 tingene du tidligere løste med et tredjepartsbibliotek eller en håndskrevet MarshalJSON, er nå flagg.

Og migreringshistorien er den beste delen. Siste opsjon vinner, så du kan ta i bruk v2s strenghet én oppførsel av gangen:

1// behold v1-semantikk, men avvis duplikate nøkler slik v2 gjør
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Du trenger ikke å porte en kodebase på 200 000 linjer til v2 for å slutte å akseptere duplikate nøkler. Du endrer ett alternativ. For alt som er stort, er dette den realistiske veien.

Ytelse: marshaling er omtrent på samme nivå, unmarshaling er betydelig raskere. Hvis noe går galt, gjenoppretter GOEXPERIMENT=nojsonv2 den gamle implementeringen – og den utveien er også på vei ut etter hvert, så send inn en feilrapport fremfor å slå deg til ro.

jsontext er lavnivålaget: Encoder/Decoder som går gjennom JSON som Tokens og Values med en tilstandsmaskin som holder deg ærlig. Bruk den når du skriver en strømmende transformator eller et JSON-filter og ikke ønsker å materialisere Go-verdier i det hele tatt.

3.2. En standard uuid-pakke

Endelig. RFC 9562, i standardbiblioteket, ingen go get kreves.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: de øverste 48 bitene er et tidsstempel, så disse sorteres alltid i opprettelsesrekkefø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 lese hele pakkedokumentasjonen mens kaffen din kjøles ned.

Detaljer verdt å vite:

  • UUID er [16]byte, så == fungerer og den er direkte brukbar som en map-nøkkel. Samme design som google/uuid.
  • Nil og Max er funksjoner, ikke variabler. Fordi en var Nil UUID på pakkenivå er en ladd pistol rettet mot foten din, og noen, et eller annet sted, ville til slutt tilordnet den.
  • Tilfeldige biter kommer fra en kryptografisk sikker generator.
  • Den implementerer encoding.TextMarshaler/TextUnmarshaler/TextAppender, så den passer rett inn i JSON-structs.
  • NewV7 er den du vil ha for primærnøkler i databaser. Tidsorden betyr at B-tre-indeksen din slutter å fragmentere, noe v4 UUID-er er kjent for å være dårlige på.

Du kan nå slette en avhengighet. Hvis du trenger v1/v3/v5 eller mer avanserte parse-alternativer, har tredjepartsbiblioteker fortsatt en jobb.

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

Go 1.24 ga oss crypto/mlkem for post-kvante-nøkkelutveksling. Go 1.27 bringer den andre halvdelen: ML-DSA-signaturer, standardisert 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 tallene. En enkelt signatur er 3 309 bytes. Ed25519 er 64. Det er en 50x økning, og den offentlige nøkkelen er nesten 2 KB i tillegg. Putt noen av disse inn i en sertifikatkjede, og din TLS-håndtrykk begynner å trenge sin egen MTU-strategi.

Dette er den faktiske kostnaden for kvantemotstand i dag, og det er grunnen til at ingen bytter alt over i morgen. Men det er i standardbiblioteket nå, som er der du vil ha det før du trenger det.

Options.Context er domeneseparasjon: signer med samme nøkkel for ulike formål, bruk en ulik kontekst for hver, og en signatur fra én kontekst vil ikke verifisere i en annen. Eksemplet ovenfor viser nøyaktig dette – samme nøkkel, samme melding, ulik kontekst, avvist.

PrivateKey implementerer crypto.Signer, så den passer inn i eksisterende grensesnitt. crypto/x509 håndterer ML-DSA-nøkler og signaturer, og crypto/tls støtter MLDSA44/MLDSA65/MLDSA87 signaturskjemaer i TLS 1.3.

Det finnes også SignDeterministic, som hopper over tilfeldigheten – praktisk for tester og reproduserbare bygg.

3.4. simd: Vektorinstruksjoner uten assembly

Go 1.26 introduserte den arkitekturspesifikke simd/archsimd som et eksperiment. Go 1.27 legger til 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 halen med en delvis last/lagring — ingen skalar opprydningslø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 som betyr noe: vektorbredden er aldri hardkodet. Du spør va.Len() ved kjøretid. VectorBitSize() forteller deg den faktiske bredden, Emulated() forteller deg om du fikk ekte maskinvare eller en høflig programvare-etterligning. Min Mac rapporterte 128-bit NEON; en AVX-512-maskin rapporterer mer; en maskin uten noe rapporterer emulering og koden kjører fortsatt.

LoadFloat32sPart/StorePart fortjener spesiell omtale. Det mest irriterende med håndskrevet SIMD er alltid den ujevne halen på slutten av arrayen, og dette håndterer det uten en separat skalar løkke.

Fortsatt eksperimentell, API-et fortsatt ustabilt, ikke sett den i produksjon. Men det faktum at dette er Go-kode og ikke assembly eller cgo er en genuint stor sak.

3.5. hash/maphash.Hasher

Et nytt grensesnitt som beskriver kontrakten mellom en verditype og hash-baserte beholdere:

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

Hvorfor? Go sin innebygde map aksepterer bare comparable nøkler. Du kan ikke bruke en slice som nøkkel. Du kan ikke definere «lik uavhengig av store/små bokstaver». Hasher løser begge deler samtidig:

 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 betyr ikke dette noe
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

For ordinær == semantikk finnes ComparableHasher[T]:

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

Haken: Hasher er et grensesnitt, ikke en datastruktur. Det finnes ingen standard hashtabell eller Bloom-filter som bruker den ennå. Dette er grunnarbeid for en fremtidig beholderpakke. I dag ville du brukt den når du bygger din egen struktur, eller konsumert en eksisterende implementering som go/types.Hasher (som lar deg bruke types.Type som map-nøkkel, respekterende Identical).

Seed-håndtering er opp til deg. Hvis hashOf ovenfor laget en ny seed for hvert kall, ville identiske verdier fått ulik hash og eksemplet ville skrevet false – som er nøyaktig feilen jeg skrev i mitt første forsøk. Én seed per beholder. (Seeden er randomisert for å overvinne hash-flooding DoS-angrep, som er grunnen til at den ikke bare er en konstant.)

3.6. httptest.NewTestServer + synctest.Sleep: Den skjulte perlen

Hvis du bare tar i bruk én ting fra denne utgivelsen, gjør det til denne.

testing/synctest ble ferdigstilt i Go 1.25 og ga oss en falsk klokke for testing av samtidig kode. Bortsett fra det øyeblikket testen din rørte et ekte nettverk, kollapset illusjonen. Go 1.27 sin httptest.NewTestServer bruker et in-memory falskt nettverk, så boblen forblir intakt. Og synctest.Sleep (= time.Sleep + synctest.Wait) runder det av.

Her er en test for retry-med-eksponentiell-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 nøyaktig 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)

Les det to ganger. Den simulerte tre sekunder med backoff mot en ekte HTTP-server, og ble ferdig på 0,00 sekunder. Og påstanden er elapsed == 3*time.Secondnøyaktig tre sekunder, ikke «minst tre sekunder, pluss/minus jitter fra planleggeren». Falske klokker jitter ikke.

synctest.Sleep eksisterer av en spesifikk grunn: hvis testen din sover i samme varighet som koden som testes, er det umulig å vite hvem som våkner først. synctest.Sleep sover og deretter venter til hver eneste andre goroutine i boblen er varig blokkert, slik at du observerer systemet etter at det har falt til ro.

Timeouter, retries, circuit breakers, rate limiters – hver tidsavhengig HTTP-klienttest du eier kan bli rask og deterministisk. Hvis testsuiten din for øyeblikket holdes sammen med time.Sleep(100 * time.Millisecond) og håp, er dette utgangen din.

3.7. net/http-endringer som faktisk påvirker produksjon

Stille, men de vil dukke opp i målingene dine.

HTTP/1-svarkropper tømmes automatisk ved Close. Ulest innhold tømmes nå (opptil en konservativ grense) når du lukker kroppen, slik at tilkoblingen kan gjenbrukes. Noe som betyr at du endelig kan slette denne besvergelsen som alle har kopiert fra samme Stack Overflow-svar siden 2016:

1// ikke lenger 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 liten gevinst. Hvis det gjør ting verre, er du sannsynligvis i den kategorien utgivelsesnotatene høflig beskriver: Transport.MaxIdleConns satt til 0, eller en ny Client per forespørsel, som omgår grensen for inaktive tilkoblinger helt. Transport.DisableKeepAlives = true vil skjule det, men utgivelsesnotatenes faktiske råd er at «en dypere titt sannsynligvis ville vært gunstig», som er Go-team-språk for du har større problemer.

HTTP/2 klientprioritet (RFC 9218). Serveren respekterer nå klientens prioritetsignaler. Hvis du foretrakk den gamle round-robin-planleggingen, bruk Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Setter en øvre grense for hvor mange header-verdier serveren vil akseptere, med DefaultMaxHeaderValueCount som standard. Én dør til er lukket for «send ti tusen headere og se hva som skjer».

ALPN på bruker-leverte tilkoblinger. Hvis din net.Conn implementerer ConnectionState() tls.ConnectionState, vil Transport og Server utføre TLS ALPN-forhandling på den – så egendefinerte dialere som går gjennom en proxy fortsatt kan forhandle HTTP/2.

3.8. Småtingene som gir glede

strings.CutLast / bytes.CutLast. Cut deler ved den første separatoren. Det fantes ingen «siste»-variant, så alle håndskrev LastIndex pluss slicing, og omtrent 30 % av oss fikk en avvik-med-én-feil på første forsøk.

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

Flott for filendelser og for parsing av host:port – der du må finne den siste kolonnen, fordi IPv6-adresser er fulle av dem.

math/big.Int.Divide. Divisjon med en eksplisitt avrundingsmodus, som erstatter «er det Quo eller Div jeg vil ha?»-myntkastet:

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 jobber i et domene der avrundingsregelen er nedfelt i et regelverk, er dette for deg.

url.URL.Clone og url.Values.Clone. Dype kopier. Den gamle *u grunne kopien delte Userinfo-pekeren, noe som produserte nøyaktig den typen feil som tar en dag å finne og fem sekunder å 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-lesemetoder returnerer nå io.EOF direkte i stedet for å pakke den inn i net.OpError. err == io.EOF fungerer nå, slik det alltid burde gjort.

database/sql.ConvertAssign og driver.RowsColumnScanner. Funksjoner for driver-forfattere. Den første eksponerer typekonverteringene Rows.Scan utfører; den andre lar drivere skanne rett inn i brukerdestinasjoner, og hopper over en mellomliggende allokering.

unicode 15 → 17. To versjoner i ett sprang. Strengklassifisering og normaliseringsoppførsel kan endre seg subtilt. Hvis du har tester som avhenger av det, vil du finne ut av det.

compress/flate ble raskere – og utdata-bytes kan avvike fra Go 1.26. Dette påvirker archive/zip, compress/gzip, compress/zlib og image/png. Hvis du har gull-fil-tester som hasher komprimert utdata, kommer de til å bryte, og det vil ikke være en regresjon. Sjekk dette før du oppgraderer, ikke under hendelsesgjennomgangen.

4. Verktøykjede

4.1. go fix har fått flere moderniseringsverktøy

go fix holder på å bli et verktøy for kodemodernisering. Go 1.27 legger til atomictypes, embedlit, slicesbackward og unsafefuncs. Bruk -diff for å 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 moderniseringsverktøy ble kjørt på én liten fil:

  • embedlit — skriver om embeddede literaler ved bruk av den nye syntaksen fra §1.2
  • atomictypesatomic.AddInt64(&x, 1) blir til atomic.Int64.Add(1), noe som gjør ikke-atomisk tilgang umulig og fikser stille 32-bit justeringsfeil du ikke visste at du hadde
  • slicesbackward — bakover-løkker blir til slices.Backward
  • waitgroupgo — ritualet med Add/go/Done blir til wg.Go (omdøpt fra 1.26 sin waitgroup for å unngå tvetydighet)

go tool fix help lister opp alle 26; go tool fix help <name> forklarer én. Også, fmtappendf ble fjernet «av stilistiske hensyn», som er en vakkert diplomatisk måte å beskrive hva enn som skjedde i den tråden.

Å kjøre go fix ./... på en gammel kodebase er en genuint tilfredsstillende ettermiddag. Les diffen først, selvfølgelig.

4.2. go test kjører stdversion som standard

Dette er endringen som er mest sannsynlig å avbryte dagen din.

go test kjører nå stdversion vet-sjekken som standard, og flagger standardbibliotek-symboler som er nyere enn det din go.mod sitt go-direktiv tillater:

1// go.mod sier: 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 dreper den klassiske feilmodusen der alt fungerer på din maskin (nyeste verktøykjede) og eksploderer i CI eller i et brukeres eldre miljø. Hvis du publiserer biblioteker med et konservativt go-direktiv, er dette en gave.

4.3. go test -json fikk OutputType

"Action":"output"-linjer bærer nå et valgfritt "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-utdata (ingen felt), t.Error-utdata (error) og rammeverksgenererte linjer (frame) er nå skilbare. Alle som har skrevet en regex for å finne ut hvilke linjer av testutdata som er «ekte», kan nå slette den.

4.4. go doc-forbedringer

package@version-syntaks. Les dokumentasjonen for en spesifikk versjon uten å legge den til i modulen din:

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

Praktisk for å sjekke hva som endret seg før en oppgradering.

-ex og utskrift av eksempelkildekode:

 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

Eksempelkildekode og forventet utdata, i terminalen, uten å åpne en nettleserfane som fortsatt vil være åpen neste torsdag.

4.5. go mod tidy rydder opp i dine require-blokker

For moduler på go 1.27 eller senere, slår go mod tidy sammen spredte require-blokker til maksimalt to (direkte og indirekte), samtidig som vedlagte 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// Etter: 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)

Merk at // networking-kommentaren overlevde. Dette rydder stort sett opp etter Git merge-konfliktløsning, som er der herreløse require-blokker blir født. Hvis teamet ditt har mer enn tre personer som legger til avhengigheter, kommer go.mod-konfliktene dine til å bli merkbart roligere.

4.6. Diverse

  • bzr-støtte fjernet. Hvis dette påvirker deg, vil jeg gjerne høre historien.
  • go tool trace -http binder til localhost når den kun får tildelt en port. -http=:6060 lytter ikke lenger på alle grensesnitt. Bruk -http=0.0.0.0:6060 hvis det var det du mente. Dette matcher nå go tool pprof, og forhindrer den sporadiske utilsiktede offentlige profileren.
  • Responsfiler (@file) støttes av compile, link, asm, cgo, cover og pack, i et GCC-kompatibelt format. For byggesystemer som sprenger kommandolinjelengde-grenser — hei, Bazel.

5. Kompilator, Linker, Porter

Kompilator. Relative filnavn i //line-direktiver løses nå mot katalogen til den inneholdende filen, noe som matcher go/scanner. Relevant hvis du skriver kodegeneratorer.

Symbolnavn for funksjonsliteraler (closures) er også enklere nå — samme navn uavhengig av inlining, og flere instanser av den samme literalen kan dele kode i binærfilen. Ingen funksjonell endring, bortsett fra: kode som sammenligner funksjonsidentitet via reflect.Value.Pointer vil se «lik» oftere enn før. Den sammenligningen var aldri gyldig, men hvis du har den, er det nå den begynner å lyve til deg høyere.

Linker. Nye -macos og -macsdk opsjoner setter OS- og SDK-versjoner i macOS sin LC_BUILD_VERSION-innlastingskommando.

Porter.

  • Darwin krever nå macOS 13 Ventura eller senere, som annonsert i 1.26. Sjekk CI-kjørerne dine.
  • PowerPC (GOOS=linux GOARCH=ppc64) byttet til ELFv2 ABI. Krever Linux-kjerne 3.13+ (RHEL7 backportet til 3.10). Cgo, PIE og ekstern linking støttes nå. Hvis du bruker cgo, men trenger en statisk ren-Go-binærfil, sett CGO_ENABLED=0.

6. Sjekkliste for oppgradering

Go 1.27 tar kompatibilitet på alvor, men sjekk disse før du øker versjonen:

  • Gull-fil-tester på komprimert utdata. compress/flate endret encodere; gzip/zip/png-bytes kan avvike.
  • Tester som matcher JSON-feilmeldingstrenger. Oppførselen er identisk; feilteksten er ikke det.
  • Fjernede GODEBUG-er i go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — disse bryter byggingen bare hvis de er satt til sine gamle verdier.
  • stdversion-brudd. go test fanger disse nå. Kjør den tidlig hvis du vedlikeholder et bibliotek med et konservativt go-direktiv.
  • macOS 12 eller eldre CI-kjørere. Støtten er borte.
  • Tester som hevder noe om symbolnavn for funksjonsliteraler, og reflect.Value.Pointer funksjonssammenligninger.
  • Transport.MaxIdleConns = 0 eller en Client per forespørsel. Kombinert med automatisk tømming av svarkropper kan dette bli tregere.

Avsluttende tanker

Go 1.27 er utgivelsen der mye utsatt vedlikehold kom til forfall på en gang.

Generiske metoder var den manglende brikken i 1.18-arbeidet med generiske typer. Struct-literal feltvelgere lukket et elleve år gammelt problem. encoding/json/v2 var to år med forslagsdiskusjon. Og uuid fylte et hull som stort sett alle Go-prosjekter hadde lappet med den samme tredjepartsavhengigheten i et tiår.

Hvis du vil ha en prioritert rekkefølge for å faktisk få verdi ut av dette:

  1. httptest.NewTestServer + synctest.Sleep — tidsavhengige HTTP-tester blir raske og deterministiske. Beste innsats-til-gevinst-forhold i hele utgivelsen.
  2. Goroutine-lekkasjeprofilen — eksponer ett endepunkt i staging og forbered deg på å bli ydmyket.
  3. Traceback goroutine-etiketter — tre linjer med pprof.Do ved inngangspunktet for forespørselen din gjør din neste stack dump kl. 03:00 lesbar.
  4. go fix ./... — la verktøyet gjøre moderniseringen.
  5. encoding/json/v2 — ingen hast. Ta den i bruk ett alternativ av gangen.

Hent go1.27rc3 og kjør testsuiten din mot den nå. Alt på den sjekklisten er betydelig hyggeligere å oppdage en tirsdag ettermiddag enn under en hendelse.

Referanser