Go 1.27, eller Hur jag lärde mig att sluta oroa mig och älska generiska metoder


Go 1.27 släpps i augusti 2026, och för en gångs skull har själva språket förändrats. Inte "vi har lagt till en ny funktion i slices"-förändring, utan verklig förändring. 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 verkligt utdata, kopierat direkt, inklusive felmeddelandena. Om något ser konstigt ut beror det på att det är exakt vad kompilatorn faktiskt rapporterade.
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 generics, och sedan Go 1.18 har vi haft Samtalet. Det går till så här:
"Låt mig bara skriva en
Map-metod på min slice-typ—"
method must have no type parameters"...okej. Då blir det en funktion på paketnivå."
Metoder kunde bara använda typparametrar som deklarerats av mottagaren. Den egna metoden kunde inte introducera nya. Så varje generisk transformation landade i exil på paketnivå, där den satt bredvid sjutton andra fritt svävande hjälpfunktioner med någon variant av namnet MapSlice.
Go 1.27 åtgärdar detta:
1type List[E any] []E
2
3// F är en typparameter som deklareras av metoden själv
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 dock två restriktioner, och de är viktigare än de ser ut att vara:
- Gränssnittsmetoder kan inte deklarera typparametrar.
- Generiska metoder kan inte implementera gränssnittsmetoder.
Den andra är den som kommer att bita dig:
1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2 G does not implement Adder (wrong type for method Add)
3 have Add[T any](T) T
4 want Add(int) int
Generiska metoder och dynamisk dispatch går alltså inte ihop. Detta är inte Go-teamet som är snålt: en generisk metod har oändligt många instansieringar, och en gränssnittsmetodtabell måste vara ändlig och känd vid kompileringstid. Man kan inte lägga något oändligt i en ändlig tabell. Universum sa nej.
Praktisk tolkning: generiska metoder är till för konkreta typer med nyttoorienterade API:er. Allt som måste döljas bakom ett gränssnitt behö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 bara paketfunktionen rand.N generisk, vilket innebar att man använde den globala källan. Nu får din egen seedade *Rand samma bekvämlighet. Liten sak. Trevlig sak.
1.2. Selektorer för fält i struct-literaler, eller: Ärende #9859 får äntligen vila
Inbäddning ger dig u.ID. Inbäddning ger inte dig User{ID: 1}. I stället ger den dig User{Base: Base{ID: 1}}, vilket är Go:s sätt att fråga om du verkligen menade det.
Ärende #9859 skapades 2015. Det har nu stängts. Någonstans får en gopher som sedan dess bytt karriär två gånger ett GitHub-meddelande.
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 befordrade 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 den inbäddade Object. Tidigare skrev man Line{Object: Object{name: "diagonal"}, ...}.
Nu till det finstilta, som jag kontrollerade genom att faktiskt försöka kompilera allt.
(a) Nyckeln är fortfarande en vanlig identifierare. Du kan inte skriva en godtycklig selektorsökväg.
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
Funktionen handlar alltså inte om att "nycklar fungerar som fältåtkomst nu". Det handlar specifikt om att implicit befordrade fältnamn är tillåtna. Något smalare än rubriken antyder.
(b) Du kan inte ange ett inbäddat fält och något som befordrats ur 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 två motstridiga uppgifter om samma minne, och kompilatorn valde att inte gissa.
(c) Pekarinbäddning är inte välkommen.
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 man en pekare att följa, och vid konstruktion av literalen finns det ännu ingen. Också rimligt.
Goda nyheter: go fix kommer att skriva om dina gamla literaler åt dig. Mer om det senare.
1.3. Typinferens för funktionstyper blev mindre godtycklig
Typinferens för generiska funktioner fungerade tidigare på vissaställen och inte på andra, utan någon märkbar princip bakom vilket som var vilket. Tilldelning till en variabel? Fungerar. Att lägga exakt samma funktion i ett struct-fält? Kompilatorfel, skriv double[int] som om det vore 2022.
Go 1.27 gör att inferens fungerar i varje kontext där måltypen är entydigt 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: krävde double[int]
8 a := A{double} // 1.26: krävde double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: krä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 och samma double, instansierad som func(int) int på tre ställen och func(float64) float64 på ett fjärde, helt utifrån kontexten. Om du skriver optionsstrukturer eller handlertabeller fulla med funktionsvärden är en massa [T]-brus på väg att försvinna ur din kodbas.
2. Runtime: Gratis prestanda och en läckdetektor
2.1. Storleksspecialiserad allokering
Kompilatorn anropar nu storleksspecialiserade allokeringsrutiner för små objekt. Release notes påstår upp till 30 % snabbare allokeringar under 80 byte, och ungefär 1 % totalt för allokeringstunga program.
Jag tror inte på release notes utan mätning, 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 (size-specialized malloc on)
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 en mikrobenchmark som inget annat gör än allokerar, på Apple Silicon. Vilket innebär: detta är bästa tänkbara fall, uppnått under laboratorieförhållanden av ett benchmark utformat för att få siffran att se bra ut. I ett verkligt program kommer GC och faktiskt arbete att dölja större delen av det. Påståendet om ~1 % är det ärliga.
Kostnaden är binärstorlek. Hello World:
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Ungefär 50 KB, fast, oavsett ditt program. Du kan välja bort detta med GOEXPERIMENT=nosizespecializedmalloc, men den nödutgången är schemalagd för borttagning i Go 1.28, så behandla den som en lösning för felrapporter snarare än en livsstil.
2.2. Goroutine-läckprofilen är verklig nu
Experimentell i Go 1.26, allmänt tillgänglig i 1.27. GOEXPERIMENT-flaggan goroutineleakprofile är borta; den fungerar bara.
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 goroutiner, permanent fast, med en fil och ett radnummer som pekar exakt på var du gjorde det.
Tricket bakom detta är verkligen genialt: det återanvänder skräpsamlarens analys av nåbarhet. Om goroutinen G är blockerad på primitiven P, och P inte kan nås från någon körbar goroutin (eller något dessa goroutiner skulle kunna väcka), så kan ingenting någonsin röra P igen, och 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 kan nås 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 det enkla exemplet ovan behöver två anrop till runtime.GC() för att rapportera. Den fångar inte allt.
Den fångar dock den klassiska "glömde avbryta kontexten, arbetargoroutinen lever för evigt", vilket är de flesta läckor de flesta gånger.
Om du importerar net/http/pprof finns den också på /debug/pprof/goroutineleak. Koppla in det i staging, kontrollera det varje vecka och var tyst förfärad.
Detta arbete bidrog Vlad Saioc på Uber med, som förtjänar en dryck av eget val.
2.3. Tracebacks berättar nu vilken förfrågan som dog
För moduler som deklarerar Go 1.27 eller senare innehåller traceback-rubrikraderna nu runtime/pprof-goroutinetiketter.
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 goroutiner. Goroutine 3 satte aldrig någon etikett. Den fick en från sin förälder.
Så nästa gång produktionen deadlockar och någon skickar SIGQUIT till processen får du, i stället för 4 000 goroutiner som alla ser identiska ut, 4 000 goroutiner märkta med vilken förfrågan och vilken tenant de tillhör. Tre rader pprof.Do vid din ingångspunkt för förfrågningar ger dig det.
Eftersom etiketter kan innehålla saker du helst inte vill dumpa till stderr stänger GODEBUG=tracebacklabels=0 av funktionen, och denna utvägsmöjlighet är uttryckligen avsedd att finnas kvar för all framtid.
2.4. asynctimerchan är borta för gott
Go 1.23 gjorde timerkanaler 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. Kanalerna i paketet time är synkrona, punkt slut, utan förhandling.
Det intressanta är policyn som introducerades tillsammans med detta. En borttagen GODEBUG kvar i din go.mod bryter inte automatiskt bygget; den bryter det bara om den är satt till det gamla värdet:
1# go.mod contains: 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 contains: godebug asynctimerchan=0
6(builds fine)
Vilket innebär att de enda som får skäll är de som faktiskt förlitade sig på det borttagna beteendet. Alla som satte den till det slutliga standardvärdet och glömde bort det får fortsätta att slippa tänka på det. Det är en genomtänkt bit API-arkeologi.
3. Standardbiblioteket
3.1. encoding/json/v2: De bytte motor mitt i flygningen
Detta är den stora förändringen. Över två års diskussion kring förslaget har äntligen landat.
Det finns nu tre paket, och att förstå uppdelningen är större delen av jobbet:
| Paket | Uppgift |
|---|---|
encoding/json | v1-API:et du känner. Beteendet är 100 % oförändrat. Implementerat ovanpå v2 |
encoding/json/v2 | Semantisk bearbetning. Go-värden ↔ JSON |
encoding/json/jsontext | Syntaktisk bearbetning. JSON som en tokenström |
Huvudpoängen ä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 äldre v1-egenhet har kodats som ett alternativ, samlats i DefaultOptionsV1(), och v1-API:et tillämpar alltid samlingen. Vilket innebär 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 även med de dåliga delarna.
v2 har däremot å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 verkligt farliga, inte bara ovårdade: när två parsrar är oense om vilken dubblett som gäller uppstår säkerhetsbuggar. CouchDB:s CVE-2017-12635 var exakt detta: en JSON-body med två roles-nycklar, där valideraren läste den ena och lagringslagret 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 mapnycklar + indrag
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 — tidigare krävdes 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 saker du tidigare löste med ett tredjepartsbibliotek eller en handskriven MarshalJSON är nu flaggor.
Och migreringsberättelsen är det bästa. Senare alternativ vinner, så du kan anta v2:s strikthet ett beteende i taget:
1// behåll v1-semantiken, 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 vänder på ett alternativ. För allt stort är detta den realistiska vägen.
Prestanda: marshaling ligger ungefär på paritet, unmarshaling är betydligt snabbare. Om något går fel återställer GOEXPERIMENT=nojsonv2 den gamla implementationen, och även den utvägsmöjligheten står på tur att tas bort så småningom, så öppna ett ärende i stället för att slå dig till ro.
jsontext är lågnivålagret: Encoder/Decoder som går igenom JSON som Token och Value med en tillståndsmaskin som håller dig ärlig. Ta fram det när du skriver en strömmande transformerare eller ett JSON-filter och inte vill materialisera Go-värden alls.
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 övre 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 består av 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 som är värt att känna till:
UUIDär[16]byte, så==fungerar och den kan användas direkt som mapnyckel. Samma design somgoogle/uuid.NilochMaxär funktioner, inte variabler. Eftersom en paketnivåvariabelvar Nil UUIDär ett laddat gevär riktat mot din fot, och någon, någonstans, så småningom skulle tilldela den.- Slumpmässiga bitar kommer från en kryptografiskt säker generator.
- Den implementerar
encoding.TextMarshaler/TextUnmarshaler/TextAppender, så den passar direkt i JSON-structar. NewV7är den du vill ha för primärnycklar i databaser. Tidsordning innebär att ditt B-träds index slutar fragmenteras, vilket v4-UUID:er är ökänt dåliga på.
Du kan nu ta bort ett beroende. Om du behöver v1/v3/v5 eller mer avancerade parsningsalternativ har tredjepartsbibliotek fortfarande en uppgift.
3.3. crypto/mldsa: Postkvantsignaturer, och de är enorma
Go 1.24 gav oss crypto/mlkem för postkvant-nyckelutbyte. Go 1.27 levererar 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å dessa siffror. En enda signatur är 3 309 byte. Ed25519 är 64. Det är en ökning med 50 gånger, och den publika nyckeln är nästan 2 KB därutöver. Stoppa in några av dessa i en certifikatkedja så börjar din TLS-handskakning 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 i morgon. Men det finns nu i standardbiblioteket, 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, så verifieras inte en signatur från en kontext i en annan. Exemplet ovan visar precis detta: samma nyckel, samma meddelande, annan kontext, avvisad.
PrivateKey implementerar crypto.Signer, så den passar in i befintliga gränssnitt. crypto/x509 hanterar ML-DSA-nycklar och signaturer, och crypto/tls stöder signaturschemana MLDSA44/MLDSA65/MLDSA87 i TLS 1.3.
Det finns också SignDeterministic, som hoppar över slumpmässigheten, användbar för tester och reproducerbara byggen.
3.4. simd: Vektorinstruktioner utan assembler
Go 1.26 introducerade det arkitekturspecifika simd/archsimd som ett experiment. Go 1.27 lägger till simd, portabel och oberoende av vektorbredd. Aktiveras 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 inläsning/utskrivning — ingen skalär städloop
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 spelar roll: vektorbredden är aldrig hårdkodad. Du frågar va.Len() vid körning. VectorBitSize() berättar den faktiska bredden, Emulated() berättar om du fick riktig hårdvara eller en artig programvaruimitation. Min Mac rapporterade 128-bitars NEON; en AVX-512-maskin rapporterar mer; en maskin utan något rapporterar emulering och koden körs ändå.
LoadFloat32sPart/StorePart förtjänar särskild omnämnande. Det mest irriterande med handskriven SIMD är alltid den ojämna svansen i slutet av arrayen, och detta hanterar den utan en separat skalär loop.
Fortfarande experimentellt, API:et är fortfarande instabilt, lägg inte det i produktion. Men faktumet att detta är Go-kod och inte assembler eller cgo är verkligen en stor sak.
3.5. hash/maphash.Hasher
Ett nytt gränssnitt som beskriver kontraktet mellan en värdetyp och hashbaserade containrar:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Varför? Gos inbyggda map accepterar bara comparable-nycklar. Du kan inte använda en slice som nyckel. Du kan inte definiera "lika utan hänsyn till gemener och versaler". 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 vanlig ==-semantik finns ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Haken: Hasher är ett gränssnitt, inte en datastruktur. Det finns ännu ingen standardhashtabell eller Bloomfilter som använder det. Detta är grundarbete för ett framtida containerpaket. Idag använder du det när du bygger din egen struktur, eller konsumerar en befintlig implementation som go/types.Hasher (som låter dig använda types.Type som mapnyckel, med respekt för Identical).
Hantering av seed ligger på dig. Om hashOf ovan skapade en ny seed vid varje anrop skulle identiska värden få olika hash, och exemplet skulle skriva ut false, vilket är exakt buggen jag skrev i mitt första försök. En seed per container. (Seeden randomiseras för att motverka hash-flooding-DoS-attacker, vilket är anledningen till att den inte bara är en konstant.)
3.6. httptest.NewTestServer + synctest.Sleep: Sömnlöparens triumf
Om du bara tar till dig en sak från denna utgåva, gör det till denna.
testing/synctest blev allmänt tillgänglig i Go 1.25 och gav oss en falsk klocka för att testa samtidig kod. Men så fort ditt test rörde ett riktigt 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) kompletterar bilden.
Här är ett test för återförsök med exponentiell 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 // backoffen ska 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. Den simulerade tre sekunders backoff mot en riktig HTTP-server och var klar på 0,00 sekunder. Och påståendet är elapsed == 3*time.Second, exakt tre sekunder, inte "minst tre sekunder, med viss schemaläggarjitter". Falska klockor jitter inte.
synctest.Sleep finns av en särskild anledning: om ditt test sover lika länge som koden under test, är det ren gissning vilken som vaknar först. synctest.Sleep sover och väntar sedan tills varje annan goroutine i bubblan är varaktigt blockerad, så du observerar systemet efter att det har stabiliserat sig.
Timeouts, återförsök, kretsbrytare, hastighetsbegränsare: varje tidsberoende HTTP-klienttest du äger kan bli snabbt och deterministiskt. Om din testsvit för närvarande hålls ihop av time.Sleep(100 * time.Millisecond) och hopp, är detta din utväg.
3.7. Ändringar i net/http som faktiskt påverkar produktion
Tysta, men de kommer att synas i dina mätvärden.
HTTP/1-svarskroppar töms automatiskt vid Close. Oläst innehåll töms nu (upp till en försiktig gräns) när du stänger kroppen, så att anslutningen kan återanvändas. Vilket innebär att du äntligen kan ta bort den här besvärjelsen som alla har kopierat från samma Stack Overflow-svar sedan 2016:
1// inte längre nödvändig
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 sämre befinner du dig troligen i den grupp som release notes artigt beskriver: Transport.MaxIdleConns satt till 0, eller en ny Client per förfrågan, vilket helt kringgår gränsen för inaktiva anslutningar. Transport.DisableKeepAlives = true döljer problemet, men release notes faktiska råd är att "en djupare titt sannolikt skulle vara fördelaktig", vilket på Go-teamets språk betyder du har större problem.
Prioritet för HTTP/2-klient (RFC 9218). Servern respekterar nu klientens prioritetssignaler. Om du föredrog den gamla round-robin-schemaläggningen, sätt Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Begränsar hur många headervärden servern accepterar, med standardvärdet DefaultMaxHeaderValueCount. Ännu en dörr stängd för "skicka tiotusen headers och se vad som händer".
ALPN på användarleverantörade anslutningar. Om din net.Conn implementerar ConnectionState() tls.ConnectionState kommer Transport och Server att göra TLS ALPN-förhandling på den, så egna dialers som går via en proxy kan fortfarande förhandla fram HTTP/2.
3.8. Det lilla som gläder
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 en off-by-one på första försöket.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Utmärkt för filändelser och för att tolka host:port, där du måste hitta den sista kolonen, eftersom IPv6-adresser är fulla av sådana.
math/big.Int.Divide. Division med ett explicit avrundningsläge, som ersätter kastandet av slant mellan "är det Quo eller Div jag vill 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
Om du arbetar i en domän där avrundningsregeln är nedskriven i en förordning är detta till för dig.
url.URL.Clone och url.Values.Clone. Djupa kopior. Den gamla grunda kopian *u delade Userinfo-pekaren, vilket gav precis den sortens bugg som tar en dag att hitta och fem sekunder att åtgärda.
1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.Println(orig.Host, clone.Host) // gosuda.org example.com
Läsmetoderna för net.UnixConn returnerar nu io.EOF direkt i stället för att linda in det i net.OpError. err == io.EOF fungerar nu, som det alltid borde ha gjort.
database/sql.ConvertAssign och driver.RowsColumnScanner. Funktioner för drivrutinsutvecklare. Den första exponerar typkonverteringarna som Rows.Scan utför; den andra låter drivrutiner skanna direkt till användarens mål och hoppa över en mellanliggande allokering.
unicode 15 → 17. Två versioner i ett hopp. Strängklassificering och normaliseringsbeteende kan förskjutas subtilt. Om du har tester som förlitar sig på detta får du reda på det.
compress/flate blev snabbare, och dess utdatabyte 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 tyst på att bli ett verktyg för kodmodernisering. Go 1.27 lägger till atomictypes, embedlit, slicesbackward och unsafefuncs. Använd -diff för att förhandsgranska:
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 utlöstes 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-atomär åtkomst omöjlig och tyst åtgärdar 32-bitars justeringsbuggar du inte visste att du hadeslicesbackward: baklänges loopar blirslices.Backwardwaitgroupgo: ritualenAdd/go/Doneblirwg.Go(omdöpt från 1.26:swaitgroupför att undvika tvetydighet)
go tool fix help listar alla 26; go tool fix help <namn> förklarar en. Dessutom togs fmtappendf bort "på grund av stilistiska betänkligheter", vilket är ett förtjänstfullt diplomatiskt sätt att beskriva vad som hände i den tråden.
Att köra go fix ./... på en gammal kodbas är en verkligt tillfredsställande eftermiddag. Läs diffen först, uppenbarligen.
4.2. go test kör stdversion som standard
Detta är förändringen som mest sannolikt kommer att avbryta din dag.
go test kör nu vet-kontrollen stdversion som standard, och flaggar standardbibliotekssymboler 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 symbol från 1.27
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 tar kål på det klassiska felscenariot 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
Rader "Action":"output" innehåller nu ett valfritt fält "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'
Utdata från t.Log (inget fält), utdata från t.Error (error) och ramverksgenererade rader (frame) kan nu särskiljas. Den som har skrivit ett reguljärt uttryck för att räkna ut vilka rader i testutdata som är "riktiga" kan nu ta bort det.
4.4. Förbättringar i go doc
Syntaxen package@version. 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 ändrats före en uppgradering.
-ex och utskrift av exempelkod:
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
Exempelkod 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 med go 1.27 eller senare slår go mod tidy ihop spridda require-block till högst två (direkta och indirekta), med bevarade kommentarer.
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)
Lägg märke till att kommentaren // networking överlevde. Detta städar främst upp efter lösning av Git-sammanslagningskonflikter, vilket är där lösa require-block föds. Om ditt team har fler än tre personer som lägger till beroenden kommer konflikterna i din go.mod att bli märkbart tystare.
4.6. Småsaker
- Stödet för
bzrär borttaget. Om detta påverkar dig vill jag verkligen höra historien. go tool trace -httpbinder till localhost när endast en port anges.-http=:6060lyssnar inte längre på alla gränssnitt. Använd-http=0.0.0.0:6060om du menade det. Detta matchar nugo tool pprofoch förhindrar den enstaka oavsiktliga offentliga profileraren.- Svarsfiler (
@file) stöds avcompile,link,asm,cgo,coverochpack, i ett GCC-kompatibelt format. För byggsystem som överskrider gränserna för kommandoradens längd: hej, Bazel.
5. Kompilator, länkare, portar
Kompilator. Relativa filnamn i //line-direktiv löses nu mot katalogen för den innehållande filen, i överensstämmelse med go/scanner. Relevant om du skriver kodgeneratorer.
Namnen på 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 har aldrig varit giltig, men om du har den börjar den nu ljuga för dig högljutt.
Länkare. Nya alternativen -macos och -macsdk sätter OS- och SDK-versionerna i load-kommandot LC_BUILD_VERSION för macOS.
Portar.
- Darwin kräver nu macOS 13 Ventura eller senare, vilket tillkännagavs i 1.26. Kontrollera dina CI-runners.
- PowerPC (
GOOS=linux GOARCH=ppc64) bytte till ELFv2-ABI:n. Kräver Linux-kärna 3.13+ (RHEL7 backporterade 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, sättCGO_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/flatebytte kodare; gzip-/zip-/png-byte kan skilja sig. - Tester som matchar JSON-felmeddelandesträngar. Beteendet är identiskt; feltexten är det inte.
- Borttagna GODEBUG i
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf. Dessa gör att bygget misslyckas endast om de är satta till sina gamla värden. - Överträdelser av
stdversion.go testfångar dessa nu. Kör det tidigt om du underhåller ett bibliotek med ett konservativtgo-direktiv. - CI-runners med macOS 12 eller äldre. Stödet är borta.
- Tester som förutsätter symbolnamn för funktionsliteraler, och jämförelser av funktioner med
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0eller enClientper förfrågan. Tillsammans med automatisk tömning av svarskroppar kan detta bli långsammare.
Avslutande reflektioner
Go 1.27 är den version där mycket uppskjuten underhållning förföll samtidigt.
Generiska metoder var den saknade pusselbiten i generics-arbetet från 1.18. Selektorer för fält i struct-literaler stängde ett elva år gammalt ärende. encoding/json/v2 var två års diskussion om förslag. Och uuid fyllde ett hål som i stort sett varje Go-projekt i ett decennium har lappat med samma tredjepartsberoende.
Om du vill ha en prioritetsordning för att faktiskt få nytta av detta:
httptest.NewTestServer+synctest.Sleep: tidsberoende HTTP-tester blir snabba och deterministiska. Bäst förhållande mellan insats och utbyte i hela utgåvan.- Goroutine-läckprofilen: exponera en endpoint i staging och förbered dig på att bli ödmjuk.
- Goroutinetiketter i tracebacks: tre rader
pprof.Dovid din ingångspunkt för förfrågningar gör din nästa stackdump klockan tre på natten läsbar. go fix ./...: låt verktyget sköta moderniseringen.encoding/json/v2: ingen brådska. Anta 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