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 kommer i august 2026, og for en gangs skyld har selve språket endret seg. Ikke bare «vi har lagt til en ny funksjon i slices», men faktisk endret. Generiske metoder er på plass. En elleve år gammel issue ble lukket. encoding/json ble stille erstattet med en ny motor mens flyet var i luften.

Alle eksemplene under ble kjørt på go1.27rc3 (darwin/arm64). Alle output-blokkene er ekte output, kopiert rett inn, inkludert feilmeldingene. Hvis noe ser merkelig ut, er det fordi det er det kompilatoren faktisk sa.

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

1. Språket endret seg (ja, virkelig)

1.1. Generiske metoder

Siden Go 1.18 har vi hatt generics, og siden Go 1.18 har vi hatt Samtalen. Den foregår slik:

"La meg bare skrive en Map-metode på slice-typen min..."

method must have no type parameters

"...greit. Da blir det en funksjon på pakkenivå."

Metoder kunne bare bruke typeparametere deklarert av receiveren. Din egen metode kunne ikke introdusere nye. Så hver generisk transformasjon ble forvist til pakkescope, der den sto ved siden av sytten andre frittstående hjelpefunksjoner med navn som er en variant av MapSlice.

Go 1.27 fikser dette:

 1type List[E any] []E
 2
 3// F er en typeparameter deklarert av metoden selv
 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 trenger ikke engang å være generisk. En kjedelig 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 i ettermiddag, finnes det imidlertid to begrensninger, og de betyr mer enn de ser ut til:

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

Den andre er den som vil bite deg:

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 passer ikke sammen. Dette er ikke Go-teamet som er gjerrige: en generisk metode har uendelig mange instansieringer, mens en interface-metodetabell må være endelig og kjent ved kompileringstid. Man kan ikke legge noe uendelig i en endelig tabell. Universet sa nei.

Praktisk tolkning: generiske metoder er til for konkrete typer med API-er i form av verktøyfunksjoner. Alt som må skjules bak et interface, trenger fortsatt den gamle tilnærmingen.

Standardbiblioteket har allerede utnyttet dette. Rand i math/rand/v2 fikk 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 den pakkenivå-baserte rand.N generisk, noe som innebar at man måtte bruke den globale kilden. Nå får din egen seedede *Rand samme bekvemmelighet. En liten ting. En fin ting.

1.2. Feltvelgere i struct-literaler, eller: Issue #9859 får endelig hvile

Embedding gir deg u.ID. Embedding gir ikke deg 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 sted gjør en gopher som siden har byttet karriere to ganger, og som fortsatt har 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 promoterte 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 innebygde Object. Tidligere måtte du skrive Line{Object: Object{name: "diagonal"}, ...}.

Nå til det med småskriften, som jeg sjekket ved faktisk å prøve å kompilere alt.

(a) Nøkkelen er fortsatt en vanlig 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å funksjonen er ikke «nøkler fungerer nå som feltaksess». Den er spesifikt at implisitt promoterte feltnavn er tillatt. Litt smalere enn overskriften antyder.

(b) Du kan ikke spesifisere et innebygd felt og noe promotert fra 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 fortalte den to motstridende ting om samme minne, og den valgte å ikke gjette.

(c) Pekerembedding er ikke velkomment.

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 på konstruksjonstidspunktet for literalen finnes den ikke ennå. Også rimelig.

Gode nyheter: go fix kan skrive om de gamle literalene dine for deg. Mer om det senere.

1.3. Typeinferens for funksjonstyper er mindre tilfeldig

Typeinferens for generiske funksjoner fungerte tidligere på noen steder og ikke andre, uten noe synlig prinsipp bak hvilke som var hvilke. Tilordne til en variabel? Fint. Legge den nøyaktig samme funksjonen i et struct-felt? Kompileringsfeil, vennligst skriv double[int] som i 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: krevde double[int]
 8	a := A{double}             // 1.26: krevde double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: krevde 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 på tre steder og func(float64) float64 på et fjerde, utelukkende ut fra kontekst. Hvis du skriver optionstrukturer eller handler-tabeller fulle av funksjonsverdier, er en hel del [T]-støy i ferd med å forsvinne fra kodebasen din.

2. Runtime: Gratis ytelse og en lekkasjedetektor

2.1. Størrelsesspesialisert allokering

Kompilatoren sender nå kall til størrelsesspesialiserte allokeringsrutiner for små objekter. Release notes hevder opptil 30 % raskere allokeringer under 80 byte, og omtrent 1 % totalt for allokeringstunge programmer.

Jeg tror ikke på release notes uten å 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ø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 mikrobenchmark som bare allokerer på Apple Silicon. Det vil si: dette er best case, oppnådd under laboratorieforhold av en benchmark designet for å få tallet til å se bra ut. I et reelt program vil GC og faktisk arbeid begrave mesteparten av det. Påstanden om ~1 % er den ærlige.

Kostnaden er binærstørrelse. Hello World:

12413202 bytes  (standard)
22361826 bytes  (nosizespecializedmalloc)

Omtrent 50 KB, fast, uavhengig av programmet ditt. Du kan velge bort dette med GOEXPERIMENT=nosizespecializedmalloc, men den nødutgangen er planlagt fjernet i Go 1.28, så behandle den som en workaround for feilrapporter, ikke som en livsstil.

2.2. Goroutine-lekkasjeprofilen er ekte nå

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

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

Triksene bak dette er virkelig smarte: det gjenbruker garbage collectorens analyse av nåbarhet. Hvis goroutine G er blokkert på primitiven P, og P ikke kan nås fra noen kjørbar goroutine (eller noe disse goroutinene kunne vekke), så kan ingenting noensinne berøre P igjen, og G våkner derfor aldri. Dette er ikke en heuristikk, det er et bevis.

Det samme designet gir deg begrensningen gratis: hvis kanalen eller mutexen kan nås via en global variabel, eller via en lokal variabel i en goroutine som fortsatt kjører, kan GC fortsatt se den, og runtime kan ikke konkludere noe. Legg merke til at selv det lille eksemplet over trenger to kall til runtime.GC() for å rapportere. Den fanger ikke alt.

Den vil imidlertid fange den klassiske «glemte å kansellere konteksten, worker-goroutinen lever evig», som er de fleste lekkasjene de fleste ganger.

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

Dette arbeidet ble bidratt av Vlad Saioc hos Uber, som fortjener en drikke etter eget valg.

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

For moduler som deklarerer Go 1.27 eller senere, inkluderer traceback-headerlinjer nå goroutine-labels fra runtime/pprof.

 1func handle(ctx context.Context) {
 2	var wg sync.WaitGroup
 3	wg.Go(func() {
 4		buf := make([]byte, 4<<10)
 5		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
 6	})
 7	wg.Wait()
 8}
 9
10func main() {
11	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12	pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3	/tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5	...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...

Det viktige detaljen: labels arves av barnegoroutiner. Goroutine 3 satte aldri en label. Den fikk en fra sin forelder.

Så neste gang produksjonen deadlocker og noen sender SIGQUIT til prosessen, får du i stedet for 4 000 goroutines som alle ser like ut, 4 000 goroutines merket med hvilken forespørsel og hvilken tenant de tilhører. Tre linjer med pprof.Do ved inngangspunktet for forespørselen din kjøper deg dette.

Siden labels kan inneholde ting du helst ikke vil dumpe til stderr, slår GODEBUG=tracebacklabels=0 det av, og denne opt-out-en er uttrykkelig ment å bli værende for alltid.

2.4. asynctimerchan er borte for godt

Go 1.23 gjorde timerkanaler ubufrede (synkrone) og tilbød asynctimerchan=1 som en vei tilbake til den gamle oppførselen. I 1.27 er den innstillingen permanent fjernet. Kanalene i pakken time er synkrone, punktum, ingen forhandling.

Det interessante er policyen som ble introdusert sammen med dette. En fjernet GODEBUG som står igjen i go.mod bryter ikke automatisk bygget. 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)

Det betyr at de eneste som får kjeft, er de som faktisk var avhengige av den fjernede oppførselen. Alle som satte den til den endelige standardverdien og glemte det, får fortsette å ikke tenke på det. Det er en gjennomtenkt bit av API-arkeologi.

3. Standardbiblioteket

3.1. encoding/json/v2: De byttet ut motoren underveis

Dette er den store. Over to års diskusjon om proposal landet endelig.

Det finnes nå tre pakker, og å forstå oppdelingen er mesteparten av jobben:

PakkeOppgave
encoding/jsonv1-API-et du kjenner. Oppførselen er 100 % uendret. Implementert på toppen av v2
encoding/json/v2Semantisk prosessering. Go-verdier ↔ JSON
encoding/json/jsontextSyntaktisk prosessering. JSON som en tokenstrøm

Hovedpoenget er at encoding/json ble bygget om på en helt annen implementasjon og oppfører seg identisk. Slik:

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

Hver eneste legacy v1-kuriositet ble kodet som en option, samlet i DefaultOptionsV1(), og v1-API-et bruker alltid denne pakken. Det 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 stille dupliserte nøkler og tar den siste. Det har alltid gjort det. Det gjør det fortsatt. Kompatibilitet betyr kompatibilitet også med de dårlige delene.

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

Dupliserte nøkler og ugyldig UTF-8 avvises. Begge er virkelig farlige, ikke bare rotete. Når to parsere er uenige om hvilken duplisert nøkkel som vinner, får du sikkerhetsfeil. CouchDBs CVE-2017-12635 var nettopp dette: en JSON-body med to roles-nøkler, der valideringen leste den ene og lagringslaget leste den andre. Å avvise er riktig 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// 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; tidligere krevde dette 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. Senere options vinner, så du kan ta i bruk v2 sin strenghet én oppførsel om gangen:

1// behold v1-semantikk, men avvis dupliserte 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 å portere en kodebase på 200 000 linjer til v2 for å slutte å akseptere dupliserte nøkler. Du slår av én option. For alt stort er dette den realistiske veien.

Ytelse: marshaling er omtrent på paritet, unmarshaling er betydelig raskere. Hvis noe går galt, gjenoppretter GOEXPERIMENT=nojsonv2 den gamle implementasjonen. Også denne opt-out-en står på huggeblokken etter hvert, så opprett en issue heller enn å slå deg til ro med den.

jsontext er det lavnivålaget: Encoder/Decoder som går gjennom JSON som Tokens og Values, med en tilstandsmaskin som holder deg ærlig. Ta det i bruk når du skriver en streaming-transformator eller et JSON-filter og ikke vil materialisere Go-verdier i det hele tatt.

3.2. En standard uuid-pakke

Endelig. RFC 9562, i standardbiblioteket, ingen go get nødvendig.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: de øverste 48 bitene er en 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 avkjøles.

Detaljer verdt å vite:

  • UUID er [16]byte, så == fungerer, og den kan brukes direkte som map-nøkkel. Samme design som google/uuid.
  • Nil og Max er funksjoner, ikke variabler. Fordi en pakkenivå-var Nil UUID er en ladd pistol rettet mot foten din, og noen, et sted, ville før eller siden tilordne til den.
  • Tilfeldige bits 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. Tidsordnet betyr at B-tree-indeksen din slutter å fragmentere, noe v4-UUID-er er beryktet for.

Du kan nå fjerne en avhengighet. Hvis du trenger v1/v3/v5 eller mer avanserte parsealternativer, har tredjepartsbibliotekene 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 leverer 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å tallene. En enkelt signatur er 3 309 byte. Ed25519 er 64. Det er en økning på 50 ganger, og den offentlige nøkkelen er nesten 2 KB i tillegg. Stapler du noen av disse i en sertifikatkjede, må TLS-handshaken din begynne å ha sin egen MTU-strategi.

Dette er den faktiske prisen for kvanteresistens i dag, og det er grunnen til at ingen bytter alt i morgen. Men det er nå i standardbiblioteket, som er der du vil at det skal være før du trenger det.

Options.Context er domeneseparasjon: signer med samme nøkkel for ulike formål, bruk en ulik kontekst for hvert, og en signatur fra én kontekst vil ikke verifiseres i en annen. Eksemplet over viser nettopp dette: samme nøkkel, samme melding, ulik kontekst, avvist.

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

Det finnes også SignDeterministic, som hopper over tilfeldigheten. Nyttig 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 simd, som er portabel og uavhengig av vektorbredde. 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, uten 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, og Emulated() forteller om du fikk ekte maskinvare eller en høflig software-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 arrayet, og dette håndterer den uten en egen skalar løkke.

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

3.5. hash/maphash.Hasher

Et nytt interface som beskriver kontrakten mellom en verditype og hash-baserte containere:

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

Hvorfor? Gos innebygde map godtar bare comparable-nøkler. Du kan ikke bruke en slice som nøkkel. Du kan ikke definere «lik ignorert store/små bokstaver». Hasher løser begge 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 ingenting av dette noe
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

For vanlig ==-semantikk finnes ComparableHasher[T]:

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

Fellen: Hasher er et interface, ikke en datastruktur. Det finnes ingen standard hashtabell eller Bloom-filter som bruker det ennå. Dette er grunnarbeid for en fremtidig containerpakke. I dag bruker du det når du bygger din egen struktur, eller konsumerer en eksisterende implementasjon som go/types.Hasher (som lar deg bruke types.Type som map-nøkkel, med respekt for Identical).

Seed-håndtering er ditt ansvar. Hvis hashOf over lagde en ny seed hver gang den ble kalt, ville identiske verdier gitt ulike hasher, og eksemplet ville skrevet ut false. Det er nøyaktig feilen jeg skrev i mitt første forsøk. Én seed per container. (Seeden er randomisert for å motvirke hash-flooding-DoS-angrep, og det er grunnen til at den ikke bare er en konstant.)

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

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

testing/synctest ble stabil i Go 1.25 og ga oss en falsk klokke for testing av samtidig kode. Men så snart testen din berørte et reelt nettverk, falt illusjonen sammen. httptest.NewTestServer i Go 1.27 bruker et nettverk i minnet, så boblen forblir intakt. Og synctest.Sleep (= time.Sleep + synctest.Wait) fullfører bildet.

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 sekunders backoff mot en ekte HTTP-server, og var ferdig på 0,00 sekunder. Og påstanden er elapsed == 3*time.Second, nøyaktig tre sekunder, ikke «minst tre sekunder, gitt eller tatt scheduler-jitter». Falske klokker har ingen jitter.

synctest.Sleep finnes av en spesifikk grunn: hvis testen din sover like lenge som koden som testes, er det noens gjetning hvilken som våkner først. synctest.Sleep sover og venter deretter til alle andre goroutines i boblen er varig blokkert, slik at du observerer systemet etter at det har satt seg.

Timeouts, retries, circuit breakers, rate limiters: hver tidsavhengige HTTP-klienttest du eier, kan bli rask og deterministisk. Hvis testsuiten din i dag holdes sammen med time.Sleep(100 * time.Millisecond) og håp, er dette utveien din.

3.7. Endringer i net/http som faktisk påvirker produksjon

Stille, men de vil dukke opp i metrikkene dine.

HTTP/1-responsebodies tømmes automatisk ved Close. Uleste data tømmes nå (opp til en konservativ grense) når du lukker bodyen, slik at forbindelsen kan gjenbrukes. Det betyr at du endelig kan slette denne besvergelsen som alle har kopiert fra samme Stack Overflow-svar siden 2016:

1// er 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 bøtta release notes beskriver høflig: Transport.MaxIdleConns satt til 0, eller en ny Client per forespørsel, som omgår grensen for inaktive forbindelser fullstendig. Transport.DisableKeepAlives = true vil tildekke problemet, men release notes sitt faktiske råd er at «a deeper look would likely be beneficial», som på Go-teamspråk betyr du har større problemer.

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

Server.MaxHeaderValueCount. Begrenser hvor mange headerverdier serveren aksepterer, med standard DefaultMaxHeaderValueCount. Enda en dør lukket for «send ti tusen headere og se hva som skjer».

ALPN på brukerleverte forbindelser. Hvis din net.Conn implementerer ConnectionState() tls.ConnectionState, vil Transport og Server utføre TLS ALPN-forhandling på den, slik at egendefinerte dialere som går via en proxy fortsatt kan forhandle frem HTTP/2.

3.8. Det lille som gir glede

strings.CutLast / bytes.CutLast. Cut deler ved det første skilletegnet. Det fantes ingen «last»-variant, så alle skrev LastIndex og slicing for hånd, og omtrent 30 % av oss fikk en off-by-one første gang.

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 det siste kolonet, fordi IPv6-adresser er fulle av dem.

math/big.Int.Divide. Divisjon med en eksplisitt avrundingsmodus, som erstatter spørsmålet «er det Quo eller Div jeg vil ha?»:

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 står skrevet i en forskrift, er dette for deg.

url.URL.Clone og url.Values.Clone. Dype kopier. Det gamle grunne kopieringen med *u delte Userinfo-pekeren, noe som ga 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

Lesemetodene til net.UnixConn returnerer nå io.EOF direkte i stedet for å pakke den inn i net.OpError. err == io.EOF fungerer nå, slik det alltid burde ha gjort.

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

unicode 15 → 17. To versjoner i ett hopp. Oppførselen for strengklassifisering og normalisering kan endre seg subtilt. Hvis du har tester som avhenger av det, vil du finne det ut.

compress/flate ble raskere, og utdata-bytene kan avvike fra Go 1.26. Dette slår ut i archive/zip, compress/gzip, compress/zlib og image/png. Hvis du har golden-file-tester som hasher komprimert utdata, vil de brekke, og det vil ikke være en regresjon. Sjekk dette før du oppgraderer, ikke under gjennomgangen etter hendelsen.

4. Verktøykjede

4.1. go fix fikk flere modernizere

go fix blir stille til 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 modernizere slo ut på én liten fil:

  • embedlit: skriver om innebygde literaler til den nye syntaksen fra §1.2
  • atomictypes: atomic.AddInt64(&x, 1) blir atomic.Int64.Add(1), som gjør ikke-atomisk tilgang umulig og stille retter 32-bit-justeringsfeil du ikke visste at du hadde
  • slicesbackward: baklengs løkker blir slices.Backward
  • waitgroupgo: ritualet Add/go/Done blir wg.Go (omdøpt fra waitgroup i 1.26 for å unngå tvetydighet)

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

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

4.2. go test kjører stdversion som standard

Dette er endringen som mest sannsynlig avbryter dagen din.

go test kjører nå vet-sjekken stdversion som standard, og flagger standardbibliotek-symboler som er nyere enn det go-direktivet i go.mod 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 eliminerer den klassiske feilen der alt fungerer på maskinen din (nyeste toolchain) og eksploderer i CI eller i en brukers eldre miljø. Hvis du publiserer biblioteker med et konservativt go-direktiv, er dette en gave.

4.3. go test -json fikk OutputType

Linjer med "Action":"output" har nå et valgfritt felt "OutputType": "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 (uten felt), output fra t.Error (error) og rammeverkgenererte linjer (frame) kan nå skilles fra hverandre. Alle som har skrevet et regulært uttrykk for å finne ut hvilke linjer i testoutput som er «ekte», kan nå slette det.

4.4. Forbedringer i go doc

Syntaksen package@version. Les dokumentasjonen for en bestemt 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 er endret før en oppgradering.

-ex og utskrift av 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, uten å åpne en nettleserfane som fortsatt vil være åpen neste torsdag.

4.5. go mod tidy rydder i require-blokkene dine

For moduler med go 1.27 eller nyere slår go mod tidy sammen spredte require-blokker til maksimalt to (direkte og indirekte), og bevarer tilknyttede kommentarer.

 1// Før
 2module tidydemo
 3
 4go 1.27
 5
 6require golang.org/x/sync v0.10.0
 7
 8// nettverk
 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	// nettverk
 8	golang.org/x/net v0.33.0
 9	golang.org/x/sync v0.10.0
10)

Merk at kommentaren // nettverk overlevde. Dette rydder mest opp etter Git-merge-konflikter, som er der løse require-blokker oppstår. Hvis teamet ditt er mer enn tre personer som legger til avhengigheter, vil konfliktene i go.mod bli merkbart roligere.

4.6. Småting

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

5. Kompilator, linker, porter

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

Symbolnavn for funksjonsliteraler (closures) er også enklere nå: det samme navnet uavhengig av inlining, og flere forekomster av samme literal kan dele kode i binæren. Ingen funksjonell endring, bortsett fra: kode som sammenligner funksjonsidentitet via reflect.Value.Pointer vil se «lik» oftere enn før. Den sammenligningen har aldri vært gyldig, men hvis du har den, er nå tiden da den begynner å lyve høyere.

Linker. Nye alternativer -macos og -macsdk setter OS- og SDK-versjonene i LC_BUILD_VERSION-lastekommandoen i macOS.

Porter.

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

6. Oppgraderingssjekkliste

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

  • Golden-file-tester på komprimert output. compress/flate endret enkodere; gzip-/zip-/png-bytes kan avvike.
  • Tester som matcher JSON-feilmeldingstekster. Oppførselen er identisk; feilteksten er ikke det.
  • Fjernede GODEBUG-er i go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf: disse feiler bygget bare hvis de er satt til sine gamle verdier.
  • stdversion-brudd. go test fanger disse nå. Kjør det tidlig hvis du vedlikeholder et bibliotek med et konservativt go-direktiv.
  • CI-runnere med macOS 12 eller eldre. Støtten er borte.
  • Tester som påstår noe om symbolnavn for funksjonsliteraler, og sammenligninger av funksjoner med reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 eller en Client per forespørsel. Kombinert med automatisk tømming av responsebodies kan dette bli tregere.

Avsluttende tanker

Go 1.27 er utgivelsen der mye utsatt vedlikehold forfalt samtidig.

Generiske metoder var den manglende biten i generics-arbeidet fra 1.18. Feltvelgere i struct-literaler lukket en elleve år gammel issue. encoding/json/v2 var to års diskusjon om proposal. Og uuid fylte et hull som nesten alle Go-prosjekter har tettet med samme tredjepartsavhengighet i et tiår.

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

  1. httptest.NewTestServer + synctest.Sleep: tidsavhengige HTTP-tester blir raske og deterministiske. Beste forhold mellom innsats og gevinst i hele utgivelsen.
  2. Goroutine-lekkasjeprofilen: eksponer ett endepunkt i staging og gjør deg klar til å bli ydmyket.
  3. Goroutine-labels i tracebacks: tre linjer med pprof.Do ved inngangspunktet for forespørselen din gjør din neste stack-dump klokken 03 om til noe lesbart.
  4. go fix ./...: la verktøyet gjøre moderniseringen.
  5. encoding/json/v2: ingen hast. Ta det i bruk én option om gangen.

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

Referanser