Go 1.27, eller hur jag slutade oroa mig och lärde mig älska Generic Methods

Go 1.27 landar i augusti 2026, och för en gångs skull har själva språket förändrats. Inte "vi lade till en ny funktion i slices"-förändrat — faktiskt förändrat. Generiska metoder är här. Ett elva år gammalt ärende har stängts. encoding/json ersattes i tysthet med en ny motor medan planet var i luften.
Varje exempel nedan kördes på go1.27rc3 (darwin/arm64). Varje utdatablock är verklig utdata, kopierad och inklistrad, inklusive felmeddelandena. Om något ser märkligt ut beror det på att det är vad kompilatorn faktiskt angav.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Språket har förändrats (Ja, verkligen)
1.1. Generiska metoder
Sedan Go 1.18 har vi haft generiska typer, och sedan Go 1.18 har vi haft "The Conversation". Den går så här:
"Låt mig bara skriva en
Map-metod för min slice-typ—"
method must have no type parameters"...bra. Det får bli en funktion på paketnivå."
Metoder kunde endast använda typparametrar deklarerade av mottagaren (receiver). Din egen metod kunde inte introducera nya. Så varje generisk transformation förvisades till paketomfånget, där den placerades bredvid sjutton andra fritt flytande hjälpare med namn som någon variant av MapSlice.
Go 1.27 åtgärdar detta:
1type List[E any] []E
2
3// F är en typparameter deklarerad av själva 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}
Mottagaren behöver inte ens vara generisk. En enkel struct kan ha en generisk metod:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Innan du refaktorerar hela din kodbas i eftermiddag finns det två begränsningar, och de betyder mer än de ser ut att göra:
- Gränssnittsmetoder (interface methods) kan inte deklarera typparametrar.
- Generiska metoder kan inte implementera gränssnittsmetoder.
Den andra punkten är den som kommer att ställa till problem:
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å generiska metoder och dynamisk dispatch fungerar inte ihop. Det beror inte på att Go-teamet är snåla — det beror på att en generisk metod har oändligt många instansieringar, och en tabell för gränssnittsmetoder måste vara ändlig och känd vid kompileringstillfället. Du kan inte placera en oändlig sak i en ändlig tabell. Universum sade nej.
Praktisk tolkning: generiska metoder är till för konkreta typer med verktygsliknande API:er. Allt som måste döljas bakom ett gränssnitt kräver fortfarande det gamla tillvägagångssättet.
Standardbiblioteket har redan utnyttjat detta. math/rand/v2:s Rand fick en generisk metod:
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
Tidigare var endast rand.N på paketnivå generisk, vilket innebar att man använde den globala källan. Nu får din egen seedade *Rand samma bekvämlighet. En liten sak. En trevlig sak.
1.2. Fältväljare för struct-literaler, eller: Ärende #9859 får äntligen vila
Inbäddning (embedding) ger dig u.ID. Inbäddning ger dig inte User{ID: 1}. Istället ger det dig User{Base: Base{ID: 1}}, vilket är Gos sätt att fråga om du verkligen menade det.
Ärende #9859 registrerades 2015. Det har nu stängts. Någonstans får en gopher, som har bytt karriär två gånger sedan dess, en GitHub-notis.
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: använd det främjade fältet direkt
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name är inte ett fält i Line. Det är ett fält i det inbäddade Object. Tidigare skrev man Line{Object: Object{name: "diagonal"}, ...}.
Nu till det finstilta, vilket jag kontrollerade genom att faktiskt försöka kompilera allt.
(a) Nyckeln är fortfarande en enkel identifierare. Du kan inte skriva en godtycklig väljarväg (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å funktionen är inte "nycklar fungerar som fältåtkomst nu". Det är specifikt "implicit främjade fältnamn är tillåtna". Något snävare än rubriken antyder.
(b) Du kan inte ange ett inbäddat fält och något som främjats från det samtidigt.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Rimligt. Du gav den två motstridiga instruktioner om samma minne och den vägrade att gissa.
(c) Pekarinbäddning är inte inbjuden.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
För att följa en pekare behöver du en pekare att följa, och vid tidpunkten för literal-konstruktion finns ingen ännu. Också rättvist.
Goda nyheter: go fix skriver om dina gamla literaler åt dig. Mer om det senare.
1.3. Funktions-typinferens blev mindre godtycklig
Typinferens för generiska funktioner fungerade tidigare på vissa ställen men inte andra, utan någon urskiljbar princip. Tilldelning till en variabel? Inga problem. Att placera exakt samma funktion i ett struct-fält? Kompileringsfel, skriv gärna double[int] som om det vore 2022.
Go 1.27 gör att inferens fungerar i varje sammanhang där måltypen är otvetydigt känd:
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: behövde double[int]
8 a := A{double} // 1.26: behövde double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: behövde double[int]
12
13 var fn func(float64) float64 = double // detta fungerade alltid
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
En double, instansierad som func(int) int på tre ställen och func(float64) float64 på ett fjärde, helt baserat på sammanhanget. Om du skriver options-structs eller handler-tabeller fulla av funktionsvärden, kommer en hel del [T]-brus snart att försvinna från din kodbas.
2. Runtime: Gratis prestanda och en läckagedetektor
2.1. Storleksspecialiserad allokering
Kompilatorn skickar nu anrop till storleksspecialiserade allokeringsrutiner för små objekt. Release-noteringarna hävdar upp till 30 % besparing på allokeringar under 80 bytes, och cirka 1 % totalt för allokeringsintensiva program.
Jag tror inte på release-noteringar utan att mäta, 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 (storleksspecialiserad 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 % snabbare i ett mikrobänkmärke som bara allokerar på Apple Silicon. Det vill säga: detta är det bästa scenariot, uppnått under laboratorieförhållanden av ett bänkmärke designat för att få siffran att se bra ut. I ett verkligt program kommer GC och faktiskt arbete att dölja det mesta. Påståendet om ~1 % är det ärliga.
Kostnaden är binärstorlek. Hello World:
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Cirka 50 KB, fast, oavsett ditt program. Du kan välja bort det med GOEXPERIMENT=nosizespecializedmalloc, men denna nödutgång är schemalagd för borttagning i Go 1.28, så betrakta det som en workaround för felrapportering snarare än en livsstil.
2.2. Goroutine-läckageprofilen är verklig nu
Experimentell i Go 1.26, allmänt tillgänglig i 1.27. goroutineleakprofile GOEXPERIMENT är borta; den bara fungerar.
1func leak() {
2 ch := make(chan int) // ingen kommer någonsin att skicka. 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åsta, med ett filnamn och radnummer som pekar exakt på var du gjorde det.
Tricket bakom detta är genuint smart: det återanvänder garbage collectorns räckviddsanalys. Om goroutine G är blockerad på primitiv P, och P är oåtkomlig från någon körbar goroutine (eller något som dessa goroutines skulle kunna väcka), så kan ingenting någonsin röra P igen, så G kommer aldrig att vakna. Det är inte en heuristik — det är ett bevis.
Samma design ger dig begränsningen gratis: om kanalen eller mutexen är nåbar via en global variabel, eller via en lokal variabel i någon goroutine som fortfarande körs, kan GC fortfarande se den, så runtime kan inte dra någon slutsats. Lägg märke till att även toy-exemplet ovan behöver två runtime.GC()-anrop för att rapportera. Den fångar inte allt.
Den kommer dock att fånga den klassiska "glömde att avbryta kontexten, worker-goroutine lever för evigt" — vilket är de flesta läckor för det mesta.
Om du importerar net/http/pprof finns den också på /debug/pprof/goroutineleak. Koppla in det i staging, kontrollera det varje vecka, bli tyst förskräckt.
Detta arbete bidrogs av Vlad Saioc på Uber, som förtjänar en dryck efter eget val.
2.3. Tracebacks talar nu om vilken förfrågan som dog
För moduler som deklarerar Go 1.27 eller senare inkluderar traceback-huvudrader nu goroutine-etiketter från 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...
Den viktiga detaljen: etiketter ärvs av underordnade goroutines. Goroutine 3 satte aldrig en etikett. Den fick en från sin förälder.
Så nästa gång produktionen får ett deadlock och någon kör en SIGQUIT på processen, får du inte 4 000 goroutines som alla ser identiska ut, utan 4 000 goroutines taggade med vilken förfrågan och vilken tenant de tillhör. Tre rader pprof.Do vid din förfrågnings ingångspunkt ger dig det.
Eftersom etiketter kan innehålla saker du hellre inte vill dumpa till stderr, stänger GODEBUG=tracebacklabels=0 av det, och det bortvalet är uttryckligen avsett att finnas kvar för evigt.
2.4. asynctimerchan är borta för gott
Go 1.23 gjorde timer-kanaler obuffrade (synkrona) och erbjöd asynctimerchan=1 som en väg tillbaka till det gamla beteendet. I 1.27 är den inställningen permanent borttagen. Kanaler i time-paketet är synkrona, punkt slut, inga förhandlingar.
Den intressanta delen är policyn som introducerades vid sidan av. En borttagen GODEBUG kvar i din go.mod bryter inte automatiskt bygget — det bryts bara om den är inställd på det gamla värdet:
1# go.mod innehåller: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/godebug#go-127)
4
5# go.mod innehåller: godebug asynctimerchan=0
6(bygger fint)
Vilket betyder att de enda som blir utskällda är de som faktiskt förlitade sig på det borttagna beteendet. Alla som ställde in det på standardvärdet och glömde bort det kan fortsätta att inte tänka på det. Det är en eftertänksam bit API-arkeologi.
3. Standardbibliotek
3.1. encoding/json/v2: De ersatte motorn mitt under flygning
Detta är den stora nyheten. Över två års förslagshantering landade äntligen.
Det finns nu tre paket, och att förstå uppdelningen är halva striden:
| Paket | Uppgift |
|---|---|
encoding/json | v1-API:et du känner till. Beteendet 100 % oförändrat. Nu implementerat ovanpå v2 |
encoding/json/v2 | Semantisk bearbetning. Go-värden ↔ JSON |
encoding/json/jsontext | Syntaktisk bearbetning. JSON som en token-ström |
Rubriken är att encoding/json byggdes om på en helt annan implementation och beter sig identiskt. Så här:
1// Go 1.27's encoding/json.Unmarshal, förkortad
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Varje legacy v1-egenhet kodades som ett alternativ, paketerat i DefaultOptionsV1(), och v1-API:et tillämpar alltid paketet. Vilket betyder att detta beter sig likadant i 1.26 och 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Ja, v1 accepterar fortfarande tyst dubbla nycklar och tar den sista. Det gjorde det alltid. Det gör det fortfarande. Kompatibilitet innebär kompatibilitet med de dåliga delarna också.
v2, under tiden, har åsikter:
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
Dubbla nycklar och ogiltig UTF-8 avvisas. Båda är genuint farliga, inte bara stökiga — när två parsers inte är överens om vilken dubbel nyckel som vinner, får du säkerhetsbuggar. CouchDB:s CVE-2017-12635 var precis detta: en JSON-kropp med två roles-nycklar, där valideraren läste en och lagringslagret läste den andra. Att avvisa är rätt beslut.
Alternativ är variadiska:
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// sorterade map-nycklar + indentering
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// avvisa okända fält — krävde tidigare en Decoder och 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 flesta av de saker du tidigare löste med ett tredjepartsbibliotek eller en handskriven MarshalJSON är nu flaggor.
Och migreringshistorien är den bästa delen. Senare alternativ vinner, så du kan anta v2:s strikthet ett beteende i taget:
1// behåll v1-semantik, men avvisa dubbla nycklar som v2 gör
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Du behöver inte porta en kodbas på 200 000 rader till v2 för att sluta acceptera dubbla nycklar. Du ändrar ett alternativ. För allt av större storlek är detta den realistiska vägen.
Prestanda: marshalling är ungefär i paritet, unmarshalling är betydligt snabbare. Om det går fel återställer GOEXPERIMENT=nojsonv2 den gamla implementationen — och det bortvalet är också på väg att tas bort så småningom, så skapa ett ärende istället för att slå dig till ro.
jsontext är lagret på låg nivå: Encoder/Decoder som går igenom JSON som Tokens och Values med en tillståndsmaskin som håller dig ärlig. Använd det när du skriver en strömmande transformator eller ett JSON-filter och inte vill materialisera Go-värden överhuvudtaget.
3.2. Ett standardpaket för uuid
Äntligen. RFC 9562, i standardbiblioteket, ingen go get krävs.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: de översta 48 bitarna är en tidsstämpel, så dessa sorteras alltid i skapelseordning
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}
Hela API:et är New, NewV4, NewV7, Nil, Max, Parse, MustParse, och en typ: type UUID [16]byte. Det är allt. Du kan läsa hela paketdokumentationen medan ditt kaffe svalnar.
Detaljer värda att känna till:
UUIDär[16]byte, så==fungerar och den är direkt användbar som en map-nyckel. Samma design somgoogle/uuid.NilochMaxär funktioner, inte variabler. Eftersom envar Nil UUIDpå paketnivå är en laddad pistol riktad mot din fot, och någon, någonstans, skulle förr eller senare tilldela den ett värde.- Slumpmässiga bitar kommer från en ** kryptografiskt säker** generator.
- Den implementerar
encoding.TextMarshaler/TextUnmarshaler/TextAppender, så den passar direkt in i JSON-structs. NewV7är den du vill ha för primärnycklar i databaser. Tidsordnad betyder att ditt B-träd-index slutar fragmenteras, vilket v4 UUID-er är ökända för att göra.
Du kan nu ta bort ett beroende. Om du behöver v1/v3/v5 eller mer avancerade parsing-alternativ har tredjepartsbibliotek fortfarande en roll att spela.
3.3. crypto/mldsa: Post-kvant-signaturer, och de är enorma
Go 1.24 gav oss crypto/mlkem för post-kvant-nyckelutbyte. Go 1.27 ger oss den andra halvan: ML-DSA-signaturer, standardiserade 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
Titta på de siffrorna. En enda signatur är 3 309 bytes. Ed25519 är 64. Det är en 50-faldig ökning, och den publika nyckeln är nästan 2 KB utöver det. Stoppa in några av dessa i en certifikatkedja och din TLS-handskakning börjar behöva en egen MTU-strategi.
Detta är den faktiska kostnaden för kvantresistens idag, och det är därför ingen byter ut allt imorgon. Men det finns i standardbiblioteket nu, vilket är där du vill att det ska finnas innan du behöver det.
Options.Context är domänseparation: signera med samma nyckel för olika ändamål, använd en annan kontext för varje, och en signatur från en kontext kommer inte att verifieras i en annan. Exemplet ovan visar precis det — samma nyckel, samma meddelande, olika kontext, avvisad.
PrivateKey implementerar crypto.Signer, så den passar in i existerande gränssnitt. crypto/x509 hanterar ML-DSA-nycklar och signaturer, och crypto/tls stöder signatur-scheman MLDSA44/MLDSA65/MLDSA87 i TLS 1.3.
Det finns också SignDeterministic, som hoppar över slumpmässigheten — praktiskt för tester och reproducerbara byggen.
3.4. simd: Vektorinstruktioner utan assembly
Go 1.26 introducerade den arkitekturspecifika simd/archsimd som ett experiment. Go 1.27 lägger till simd — portabel och vektor-bredd-agnostisk. Aktivera 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 // hantera svansen med en partiell laddning/lagring — ingen skalär upprensningsloop
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]
Designvalet som betyder något: vektorbredden är aldrig hårdkodad. Du frågar va.Len() vid körning. VectorBitSize() talar om för dig den faktiska bredden, Emulated() talar om för dig om du fick äkta hårdvara eller en artig mjukvaru-efterlikning. Min Mac rapporterade 128-bitars NEON; en AVX-512-maskin rapporterar mer; en maskin utan något rapporterar emulering och koden körs fortfarande.
LoadFloat32sPart/StorePart förtjänar ett särskilt omnämnande. Den mest irriterande delen av handskriven SIMD är alltid den trasiga svansen i slutet av arrayen, och detta hanterar det utan en separat skalär loop.
Fortfarande experimentell, API fortfarande instabilt, sätt det inte i produktion. Men faktumet att detta är Go-kod och inte assembly eller cgo är en genuint stor sak.
3.5. hash/maphash.Hasher
Ett nytt gränssnitt som beskriver kontraktet mellan en värdetyp och hash-baserade behållare:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Varför? Gos inbyggda map accepterar endast comparable-nycklar. Du kan inte använda en slice som nyckel. Du kan inte definiera "lika oavsett skiftläge". Hasher löser båda på en gång:
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) // samma seed, annars betyder inget av detta något
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
För vanliga ==-semantiker finns ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Haken: Hasher är ett gränssnitt, inte en datastruktur. Det finns ingen standard-hashtabell eller Bloom-filter som konsumerar det ännu. Detta är grundarbete för ett framtida container-paket. Idag använder du det när du bygger din egen struktur, eller konsumerar en existerande implementation som go/types.Hasher (vilket låter dig använda types.Type som en map-nyckel, med respekt för Identical).
Seed-hantering ligger på dig. Om hashOf ovan skapade ett nytt seed varje anrop, skulle identiska värden hashas annorlunda och exemplet skulle skriva ut false — vilket är exakt buggen jag skrev vid mitt första försök. Ett seed per behållare. (Seedet är randomiserat för att motverka hash-flooding DoS-attacker, vilket är anledningen till att det inte bara är en konstant.)
3.6. httptest.NewTestServer + synctest.Sleep: Den oväntade succén
Om du bara antar en sak från denna release, gör det till denna.
testing/synctest tog examen i Go 1.25 och gav oss en falsk klocka för att testa samtidig kod. Förutom det ögonblick då ditt test rörde vid ett verkligt nätverk, kollapsade illusionen. Go 1.27:s httptest.NewTestServer använder ett in-memory falskt nätverk, så bubblan förblir intakt. Och synctest.Sleep (= time.Sleep + synctest.Wait) rundar av det.
Här är ett retry-med-exponentiell-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 vara exakt 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 två gånger. Det simulerade tre sekunders backoff mot en verklig HTTP-server, och avslutades på 0,00 sekunder. Och påståendet är elapsed == 3*time.Second — exakt tre sekunder, inte "minst tre sekunder, plus eller minus scheduler-jitter". Falska klockor jitterar inte.
synctest.Sleep finns av en specifik anledning: om ditt test sover under samma tid som koden som testas, är det någon annans gissning vilken som vaknar först. synctest.Sleep sover och sedan väntar tills varje annan goroutine i bubblan är varaktigt blockerad, så du observerar systemet efter att det har satt sig.
Timeouts, retries, circuit breakers, rate limiters — varje tidsberoende HTTP-klienttest du äger kan bli snabbt och deterministiskt. Om din testsvit för närvarande hålls ihop med time.Sleep(100 * time.Millisecond) och hopp, är detta din utgång.
3.7. net/http-förändringar som faktiskt påverkar produktionen
Tysta, men de kommer att dyka upp i din mätdata.
HTTP/1-svarskroppar dräneras automatiskt vid Close. Oläst innehåll dräneras nu (upp till en konservativ gräns) när du stänger kroppen, så anslutningen kan återanvändas. Vilket betyder att du äntligen kan radera denna besvärjelse som alla har kopierat från samma Stack Overflow-svar sedan 2016:
1// inte längre nödvändigt
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
För de flesta program är detta en no-op eller en liten vinst. Om det gör saker värre, är du förmodligen i den hink som release-noteringarna artigt beskriver: Transport.MaxIdleConns inställd på 0, eller en ny Client per förfrågan, vilket helt kringgår gränsen för inaktiva anslutningar. Transport.DisableKeepAlives = true kommer att dölja det, men release-noteringarnas faktiska råd är att "en djupare titt sannolikt skulle vara fördelaktig", vilket är Go-team-språk för du har större problem.
HTTP/2-klientprioritet (RFC 9218). Servern respekterar nu klientens prioritetssignaler. Om du föredrog den gamla round-robin-schemaläggningen, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Begränsar hur många huvudvärden servern accepterar, med standardvärdet DefaultMaxHeaderValueCount. En dörr till stängd för "skicka tiotusen headers och titta vad som händer".
ALPN på användartillhandahållna anslutningar. Om din net.Conn implementerar ConnectionState() tls.ConnectionState, kommer Transport och Server att utföra TLS ALPN-förhandling på den — så anpassade dialers som går genom en proxy kan fortfarande förhandla HTTP/2.
3.8. De små sakerna som skapar glädje
strings.CutLast / bytes.CutLast. Cut delar vid den första separatorn. Det fanns ingen "sista"-variant, så alla handskrev LastIndex plus slicing, och ungefär 30 % av oss fick ett off-by-one-fel vid första försöket.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Bra för filändelser och för parsing av host:port — där du måste hitta det sista kolon-tecknet, eftersom IPv6-adresser är fulla av dem.
math/big.Int.Divide. Division med ett explicit avrundningsläge, som ersätter "är det Quo eller Div jag vill ha?"-slantkastningen:
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
Om du arbetar inom ett domän där avrundningsregeln är nedskriven i en förordning, är detta för dig.
url.URL.Clone och url.Values.Clone. Djupa kopior. Den gamla *u ytliga kopian delade Userinfo-pekaren, vilket producerade exakt den typ av bugg som tar en dag att hitta och fem sekunder att fixa.
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-läsmetoder returnerar nu io.EOF direkt istället för att wrappa den i net.OpError. err == io.EOF fungerar nu, som det alltid borde ha gjort.
database/sql.ConvertAssign och driver.RowsColumnScanner. Funktioner för drivrutinsförfattare. Den första exponerar typkonverteringarna Rows.Scan utför; den andra låter drivrutiner skanna direkt till användarens destinationer, vilket hoppar över en mellanliggande allokering.
unicode 15 → 17. Två versioner i ett hopp. Strängklassificering och normaliseringsbeteende kan skifta subtilt. Om du har tester som beror på det, kommer du att märka det.
compress/flate blev snabbare — och dess utdatabytes kan skilja sig från Go 1.26. Detta kaskaderar till archive/zip, compress/gzip, compress/zlib och image/png. Om du har golden-file-tester som hashar komprimerad utdata, kommer de att gå sönder, och det kommer inte att vara en regression. Kontrollera detta innan du uppgraderar, inte under incidentgenomgången.
4. Verktygskedja
4.1. go fix fick fler moderniserare
go fix håller på att tyst förvandlas till ett kodmoderniseringsverktyg. Go 1.27 lägger till atomictypes, embedlit, slicesbackward och unsafefuncs. Använd -diff för förhandsgranskning:
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 }
Fyra moderniserare kördes på en liten fil:
embedlit— skriver om inbäddade literaler med den nya syntaxen från §1.2atomictypes—atomic.AddInt64(&x, 1)bliratomic.Int64.Add(1), vilket gör icke-atomisk åtkomst omöjlig och tyst fixar 32-bitars justeringsbuggar du inte visste att du hadeslicesbackward— bakåtriktade loopar blirslices.Backwardwaitgroupgo—Add/go/Done-ritualen blirwg.Go(omdöpt från 1.26:swaitgroupför att undvika tvetydighet)
go tool fix help listar alla 26; go tool fix help <name> förklarar en. Dessutom togs fmtappendf bort "av stilistiska skäl", vilket är ett vackert diplomatiskt sätt att beskriva vad som än hände i den ärendetråden.
Att köra go fix ./... på en gammal kodbas är en genuint tillfredsställande eftermiddag. Läs diffen först, uppenbarligen.
4.2. go test kör stdversion som standard
Detta är den förändring som mest sannolikt kommer att avbryta din dag.
go test kör nu stdversion vet-kontrollen som standard, och flaggar symboler i standardbiblioteket som är nyare än vad din go.mod:s go-direktiv tillåter:
1// go.mod säger: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast är en 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]
Detta dödar det klassiska felläget där allt fungerar på din maskin (senaste verktygskedjan) och exploderar i CI eller i en användares äldre miljö. Om du publicerar bibliotek med ett konservativt go-direktiv är detta en gåva.
4.3. go test -json fick OutputType
"Action":"output"-rader bär nu ett valfritt "OutputType"-fält: "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 (inget fält), t.Error-utdata (error) och ramverksgenererade rader (frame) är nu urskiljbara. Alla som har skrivit en regex för att räkna ut vilka rader i testutdata som är "riktiga" kan nu radera den.
4.4. Förbättringar i go doc
package@version-syntax. Läs dokumentationen för en specifik version utan att lägga till den i din modul:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Praktiskt för att kontrollera vad som ändrades innan en uppgradering.
-ex och utskrift av källkod för exempel:
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
Exempelkällkod och förväntad utdata, i terminalen, utan att öppna en webbläsarflik som fortfarande kommer att vara öppen nästa torsdag.
4.5. go mod tidy städar upp dina require-block
För moduler på go 1.27 eller senare slår go mod tidy ihop utspridda require-block till som mest två (direkta och indirekta), samtidigt som bifogade kommentarer bevaras.
1// Före
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)
Notera att // networking-kommentaren överlevde. Detta städar främst upp efter Git-konflikthantering, vilket är där herrelösa require-block föds. Om ditt team har fler än tre personer som lägger till beroenden kommer dina go.mod-konflikter snart att bli märkbart tystare.
4.6. Diverse
- Stöd för
bzrborttaget. Om detta påverkar dig vill jag genuint höra historien. go tool trace -httpbinder till localhost när bara en port anges.-http=:6060lyssnar inte längre på alla gränssnitt. Använd-http=0.0.0.0:6060om det var vad du menade. Detta matchar nugo tool pprofoch förhindrar den tillfälliga oavsiktliga publika profileraren.- Svarsfiler (
@file) stöds avcompile,link,asm,cgo,coverochpack, i ett GCC-kompatibelt format. För byggsystem som spränger kommandoradens längdgränser — hej, Bazel.
5. Kompilator, länkare, portar
Kompilator. Relativa filnamn i //line-direktiv löses nu mot katalogen för den innehållande filen, vilket matchar go/scanner. Relevant om du skriver kodgeneratorer.
Symbolnamn för funktionsliteraler (closures) är också enklare nu — samma namn oavsett inlining, och flera instanser av samma literal kan dela kod i binären. Ingen funktionell förändring, förutom: kod som jämför funktionsidentitet via reflect.Value.Pointer kommer att se "lika" oftare än tidigare. Den jämförelsen var aldrig giltig, men om du har den, är det nu den börjar ljuga för dig högre.
Länkare. Nya -macos och -macsdk alternativ ställer in OS- och SDK-versioner i macOS LC_BUILD_VERSION-laddningskommandot.
Portar.
- Darwin kräver nu macOS 13 Ventura eller senare, som annonserades i 1.26. Kontrollera dina CI-runners.
- PowerPC (
GOOS=linux GOARCH=ppc64) växlade till ELFv2 ABI. Kräver Linux-kärna 3.13+ (RHEL7 backportad till 3.10). Cgo, PIE och extern länkning stöds nu. Om du använder cgo men behöver en statisk ren Go-binär, ställ inCGO_ENABLED=0.
6. Checklista för uppgradering
Go 1.27 tar kompatibilitet på allvar, men kontrollera dessa innan du höjer versionen:
- Golden-file-tester på komprimerad utdata.
compress/flateändrade kodare; gzip/zip/png-bytes kan skilja sig. - Tester som matchar felmeddelandesträngar för JSON. Beteendet är identiskt; feltexten är det inte.
- Borttagna GODEBUGs i
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— dessa får bygget att misslyckas endast om de är inställda på sina gamla värden. -
stdversion-överträdelser.go testfångar dessa nu. Kör det tidigt om du underhåller ett bibliotek med ett konservativtgo-direktiv. - macOS 12 eller äldre CI-runners. Stödet är borta.
- Tester som gör anspråk på symbolnamn för funktionsliteraler, och
reflect.Value.Pointer-funktionsjämförelser. -
Transport.MaxIdleConns = 0eller enClientper förfrågan. Kombinerat med automatisk dränering av svarskroppar kan detta bli långsammare.
Avslutande tankar
Go 1.27 är releasen där en hel del uppskjutet underhåll förföll samtidigt.
Generiska metoder var den saknade pusselbiten i 1.18:s generiska arbete. Fältväljare för struct-literaler stängde ett elva år gammalt ärende. encoding/json/v2 var två år av förslagshantering. Och uuid fyllde ett hål som i princip varje Go-projekt hade lappat med samma tredjepartsberoende i ett decennium.
Om du vill ha en prioritetsordning för att faktiskt få ut värde av detta:
httptest.NewTestServer+synctest.Sleep— tidsberoende HTTP-tester blir snabba och deterministiska. Bästa ansträngning-till-nytta-förhållandet i hela releasen.- Goroutine-läckageprofilen — exponera en endpoint i staging och förbered dig på att bli ödmjuk.
- Traceback goroutine-etiketter — tre rader
pprof.Dovid din förfrågnings ingångspunkt gör din nästa stackdump kl 03:00 läsbar. go fix ./...— låt verktyget göra moderniseringen.encoding/json/v2— ingen brådska. Adoptera det ett alternativ i taget.
Hämta go1.27rc3 och kör din testsvit mot den nu. Allt på den checklistan är betydligt trevligare att upptäcka en tisdagseftermiddag än under en incident.
Referenser