Go 1.27, alebo Ako som sa naučil prestať sa obávať a milovať generické metódy


Go 1.27 prichádza v auguste 2026 a tentoraz sa zmenil samotný jazyk. Nie zmena typu „pridali sme novú funkciu do slices“, ale skutočná zmena. Generické metódy sú zavedené. Jedenásťročná otvorená otázka bola uzavretá. encoding/json bol potichu nahradený novým enginom, kým lietadlo ešte letelo.
Každý príklad nižšie bol spustený na go1.27rc3 (darwin/arm64). Každý výstupný blok je skutočný výstup, skopírovaný vrátane chybových hlásení. Ak sa niečo javí zvláštne, je to preto, lebo kompilátor skutočne vypísal práve toto.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Jazyk sa zmenil (áno, naozaj)
1.1. Generické metódy
Od Go 1.18 máme generiky a od Go 1.18 máme aj Rozhovor. Priebeh je tento:
„Len si napíšem metódu
Mappre môj typ slice…“
method must have no type parameters„…dobre. Takže funkcia na úrovni balíka.“
Metódy mohli používať iba typové parametre deklarované v receiveri. Vlastná metóda nemohla zaviesť nové. Každá generická transformácia sa preto musela presunúť do rozsahu balíka, kde ležala vedľa ďalších sedemnástich voľne plávajúcich pomocných funkcií s názvami typu MapSlice.
Go 1.27 to opravuje:
1type List[E any] []E
2
3// F je typový parameter deklarovaný samotnou metódou
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}
Receiver ani nemusí byť generický. Obyčajná štruktúra môže mať generickú metódu:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Predtým, než dnes popoludní prepíšete celú kódovú základňu, existujú dve obmedzenia a sú dôležitejšie, než vyzerajú:
- Metódy rozhrania nemôžu deklarovať typové parametre.
- Generické metódy nemôžu implementovať metódy rozhrania.
Práve to druhé vás bude páliť:
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
Generické metódy a dynamické vyhľadávanie metód sa teda nedajú kombinovať. Nejde o lakomosť tímu Go: generická metóda má nekonečne veľa inštanciácií, kým tabuľka metód rozhrania musí byť konečná a známa v čase kompilácie. Nekonečnú vec nemožno uložiť do konečnej tabuľky. Vesmír povedal nie.
Praktické čítanie: generické metódy slúžia pre konkrétne typy s API v štýle pomocných funkcií. Všetko, čo sa musí skrývať za rozhraním, stále potrebuje starý prístup.
Štandardná knižnica to už využila. Rand z math/rand/v2 dostal generickú metódu:
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
Predtým bola generická iba funkcia na úrovni balíka rand.N, ktorá používala globálny zdroj. Teraz si vlastný, seedovaný *Rand získa rovnaké pohodlie. Malá vec. Pekná vec.
1.2. Selektory polí v literáloch štruktúr, alebo: Problém #9859 konečne dostáva pokoj
Vnorenie vám dáva u.ID. Vnorenie vám nedáva User{ID: 1}. Namiesto toho dostanete User{Base: Base{ID: 1}}, čo je spôsob, akým sa Go pýta, či ste to naozaj mysleli.
Problém #9859 bol založený v roku 2015. Teraz je uzavretý. Niekde jeden gopher, ktorý sa medzitým dvakrát prekvalifikoval, dostáva notifikáciu z GitHubu.
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: použite povýšené pole priamo
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name nie je poľom Line. Je poľom vnoreného Object. Predtým ste museli napísať Line{Object: Object{name: "diagonal"}, ...}.
Teraz drobné podrobnosti, ktoré som overil skutočným pokusom o kompiláciu všetkého.
(a) Kľúčom je stále jednoduchý identifikátor. Nemôžete zapísať ľubovoľnú cestu selektora.
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
Funkcia teda nie je v tom, že kľúče fungujú ako prístup k poliam. Ide konkrétne o to, že sú povolené implicitne povýšené názvy polí. Je to o niečo užšie, než naznačuje titulok.
(b) Nemôžete súčasne uviesť vnorené pole a niečo, čo bolo z neho povýšené.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Rozumné. Povedali ste mu dve protichodné veci o tej istej pamäti a on odmietol hádať.
(c) Vnorenie ukazovateľov nie je vítané.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Aby ste mohli nasledovať ukazovateľ, potrebujete ukazovateľ, ktorý nasledujete, a v čase konštrukcie literálu ešte neexistuje. Aj to je férové.
Dobrá správa: go fix vaše staré literály prepíše automaticky. K tomu sa dostaneme neskôr.
1.3. Odvodzovanie typov funkcií je menej svojvoľné
Odvodzovanie typov pre generické funkcie fungovalo kedysi na niektorých miestach a na iných nie, bez akéhokoľvek rozpoznateľného princípu, ktorý určoval, ktoré je ktoré. Priradenie do premennej? V poriadku. Umiestnenie úplne rovnakej funkcie do poľa štruktúry? Chyba kompilátora, prosím napíšte double[int], ako keby bol rok 2022.
Go 1.27 zabezpečuje, že odvodzovanie funguje v každom kontexte, kde je cieľový typ jednoznačne známy:
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: vyžadovalo double[int]
8 a := A{double} // 1.26: vyžadovalo double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: vyžadovalo double[int]
12
13 var fn func(float64) float64 = double // toto fungovalo vždy
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Jedna funkcia double sa inštanciuje ako func(int) int na troch miestach a ako func(float64) float64 na štvrtom, a to výhradne z kontextu. Ak píšete štruktúry s možnosťami alebo tabuľky handlerov plné funkčných hodnôt, množstvo šumu [T] sa z vašej kódovej základne čoskoro vytratí.
2. Runtime: Bezplatný výkon a detektor únikov
2.1. Alokácia špecializovaná podľa veľkosti
Kompilátor teraz generuje volania alokačných rutín špecializovaných podľa veľkosti pre malé objekty. Poznámky k vydaniu sľubujú až 30 % úsporu pri alokáciách pod 80 bajtov a približne 1 % celkovo pre programy náročné na alokácie.
Poznámkam k vydaniu neverím bez merania, takže:
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# predvolené (size-specialized malloc zapnutý)
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
V mikrobenchmarku, ktorý nerobí nič iné než alokácie na Apple Silicon, je to o 30–35 % rýchlejšie. Inými slovami: ide o najlepší prípad, dosiahnutý v laboratórnych podmienkach benchmarkom navrhnutým tak, aby číslo vyzeralo dobre. V reálnom programe väčšinu tohto zisku pohltí GC a skutočná práca. Tvrdenie o ~1 % je to čestné.
Cenou je veľkosť binárky. Hello World:
12413202 bytes (predvolené)
22361826 bytes (nosizespecializedmalloc)
Približne 50 KB, pevne, bez ohľadu na váš program. Môžete sa z toho odhlásiť pomocou GOEXPERIMENT=nosizespecializedmalloc, ale táto únikovú cesta má byť odstránená v Go 1.28, takže ju berte ako obchádzku pre hlásenie chyby, nie ako životný štýl.
2.2. Profil únikov goroutín je teraz skutočný
V Go 1.26 experimentálny, v 1.27 všeobecne dostupný. GOEXPERIMENT goroutineleakprofile zanikol; jednoducho funguje.
1func leak() {
2 ch := make(chan int) // nikto nikdy nepošle. nikto.
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
Tri goroutiny natrvalo uviaznuté, so súborom a číslom riadka ukazujúcimi presne na miesto, kde ste to spravili.
Trik za tým je naozaj vynikajúci: znovu používa analýzu dosiahnuteľnosti garbage collectora. Ak je goroutina G blokovaná na primitíve P a P nie je dosiahnuteľný z nijakej spustiteľnej goroutiny (ani z ničoho, čo by tieto goroutiny mohli zobudiť), potom sa P nikdy nemôže znovu dotknúť nič, a G sa preto nikdy nezobudí. Nie je to heuristika, je to dôkaz.
Rovnaký návrh vám poskytuje aj obmedzenie zadarmo: ak je kanál alebo mutex dosiahnuteľný cez globálnu premennú alebo cez lokálnu premennú nejakej stále bežiacej goroutiny, GC ho stále vidí, takže runtime nemôže nič vyvodiť. Všimnite si, že aj vyššie uvedený hračkársky príklad potrebuje na nahlásenie dve volania runtime.GC(). Nezachytí všetko.
Zachytí však klasické „zabudol som zrušiť kontext, pracovná goroutina žije večne“, čo je väčšina únikov väčšinu času.
Ak importujete net/http/pprof, je dostupný aj na /debug/pprof/goroutineleak. Pripojte to na staging, kontrolujte to raz týždenne a buďte potichu zdesení.
Túto prácu prispel Vlad Saioc z Uberu, ktorý si zaslúži nápoj podľa vlastného výberu.
2.3. Tracebacky teraz povedia, ktorá požiadavka zlyhala
Pre moduly deklarujúce Go 1.27 alebo novší sa hlavičkové riadky tracebackov teraz zobrazujú spolu s návestiami goroutín z 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...
Dôležitý detail: návestia sa dedia do podriadených goroutín. Goroutina 3 nikdy nenastavila návestie. Dostala ho od svojho rodiča.
Takže keď sa produkcia nabudúce zasekne a niekto pošle procesu SIGQUIT, namiesto 4 000 goroutín, ktoré vyzerajú všetky rovnako, dostanete 4 000 goroutín označených tým, ku ktorej požiadavke a ku ktorému tenantovi patria. Tri riadky pprof.Do na vstupnom bode vašich požiadaviek vám to zabezpečia.
Keďže návestia môžu obsahovať veci, ktoré by ste nechceli vypisovať na stderr, GODEBUG=tracebacklabels=0 to vypne, a táto možnosť odhlásenia má explicitne zostať navždy.
2.4. asynctimerchan je navždy preč
Go 1.23 urobil kanály časovačov nebufrovanými (synchrónnymi) a ponúkol asynctimerchan=1 ako spôsob návratu k starému správaniu. V 1.27 je toto nastavenie natrvalo odstránené. Kanály balíka time sú synchrónne, bez akýchkoľvek výnimiek.
Zaujímavá je politika, ktorá sa s tým zaviedla. Odstránený GODEBUG ponechaný vo vašom go.mod automaticky nerozbije zostavenie: rozbije ho iba vtedy, ak je nastavený na starú hodnotu:
1# go.mod obsahuje: 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 obsahuje: godebug asynctimerchan=0
6(builds fine)
To znamená, že kričať na vás budú iba ľudia, ktorí sa skutočne spoliehali na odstránené správanie. Všetci, ktorí to nastavili na neskoršiu predvolenú hodnotu a zabudli na to, môžu ďalej nemyslieť na to. Je to premyslený kus archeológie API.
3. Štandardná knižnica
3.1. encoding/json/v2: Motor vymenili za chodu
Toto je veľká zmena. Viac než dva roky diskusie o návrhu sa konečne zhmotnilo.
Teraz existujú tri balíky a pochopenie rozdelenia je väčšinou bitky:
| Balík | Úloha |
|---|---|
encoding/json | API v1, ktoré poznáte. Správanie je na 100 % nezmenené. Teraz implementované nad v2 |
encoding/json/v2 | Sémantické spracovanie. Hodnoty Go ↔ JSON |
encoding/json/jsontext | Syntaktické spracovanie. JSON ako prúd tokenov |
Hlavnou správou je, že encoding/json bol prestavaný na úplne inú implementáciu a správa sa identicky. Ako to funguje:
1// encoding/json.Unmarshal v Go 1.27, skrátene
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Každá staršia zvláštnosť v1 bola zakódovaná ako možnosť, zabalená do DefaultOptionsV1(), a API v1 ju vždy aplikuje. To znamená, že toto sa správa rovnako v 1.26 aj 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Áno, v1 stále potichu prijíma duplicitné kľúče a berie posledný. Vždy to robil. Stále to robí. Kompatibilita znamená kompatibilitu aj so zlými časťami.
v2 medzitým má názory:
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
Duplicitné kľúče a neplatné UTF-8 sú odmietnuté. Obe sú skutočne nebezpečné, nielen neupravené: keď sa dva parsery nezhodnú v tom, ktorý duplicitný kľúč vyhrá, vznikajú bezpečnostné chyby. CouchDB CVE-2017-12635 bol presne tento prípad: JSON telo s dvoma kľúčmi roles, kde validátor čítal jeden a úložná vrstva druhý. Odmietnutie je správne rozhodnutie.
Možnosti sú variadické:
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// zoradené kľúče mapy + odsadenie
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// odmietnutie neznámych polí — predtým sa vyžadoval Decoder a 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: väčšina vecí, ktoré ste predtým riešili knižnicou tretej strany alebo ručne napísaným MarshalJSON, je teraz príznakmi.
A príbeh migrácie je tou najlepšou časťou. Neskoršie možnosti majú prednosť, takže prísnosť v2 môžete prijať po jednom správaní:
1// zachovať sémantiku v1, ale odmietnuť duplicitné kľúče ako v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Kvôli zastaveniu prijímania duplicitných kľúčov nemusíte portovať 200-tisícriadkovú kódovú základňu do v2. Prepnete jednu možnosť. Pri akomkoľvek väčšom projekte je to realistická cesta.
Výkon: marshalovanie je zhruba na úrovni pôvodného, unmarshalovanie je výrazne rýchlejšie. Ak sa niečo pokazí, GOEXPERIMENT=nojsonv2 obnoví starú implementáciu. Aj táto možnosť odhlásenia je časom na odstránenie, takže radšej založte issue, než sa tomu prispôsobujte.
jsontext je nízkoúrovňová vrstva: Encoder/Decoder prechádzajú JSON ako Token a Value so stavovým automatom, ktorý vás drží v intelektuálnej poctivosti. Siahnite po ňom, keď píšete streamovaný transformátor alebo filter JSON a nechcete vôbec materializovať hodnoty Go.
3.2. Štandardný balík uuid
Konečne. RFC 9562 v štandardnej knižnici, bez potreby go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: horných 48 bitov je časová pečiatka, takže tieto vždy zoradia v poradí vzniku
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}
Celé API tvoria New, NewV4, NewV7, Nil, Max, Parse, MustParse a jeden typ: type UUID [16]byte. To je všetko. Celú dokumentáciu balíka si môžete prečítať, kým vám vychladne káva.
Podrobnosti, ktoré stojí za to vedieť:
UUIDje[16]byte, takže funguje==a priamo sa dá použiť ako kľúč mapy. Rovnaký návrh akogoogle/uuid.NilaMaxsú funkcie, nie premenné. Lebo balíkovávar Nil UUIDje nabitá zbraň namierená na vašu nohu a niekto niekde by do nej nakoniec priradil hodnotu.- Náhodné bity pochádzajú z kryptograficky bezpečného generátora.
- Implementuje
encoding.TextMarshaler/TextUnmarshaler/TextAppender, takže sa bez problémov zmestí do štruktúr JSON. NewV7je ten, ktorý chcete pre primárne kľúče databázy. Časové zoradenie znamená, že váš B-strom index prestane fragmentovať, čo sú UUID v4 preslávene zlé.
Teraz môžete odstrániť jednu závislosť. Ak potrebujete v1/v3/v5 alebo pokročilejšie možnosti parsovania, knižnice tretích strán stále majú svoje miesto.
3.3. crypto/mldsa: Post-kvantové podpisy, a sú obrovské
Go 1.24 nám dal crypto/mlkem pre post-kvantovú výmenu kľúčov. Go 1.27 prináša druhú polovicu: podpisy ML-DSA, štandardizované ako 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
Pozrite sa na tieto čísla. Jediný podpis má 3 309 bajtov. Ed25519 má 64. To je 50-násobný nárast a verejný kľúč má k tomu takmer 2 KB. Napchajte niekoľko takýchto podpisov do reťaze certifikátov a váš handshake TLS začne potrebovať vlastnú stratégiu MTU.
Toto je skutočná cena kvantovej odolnosti dnes a preto sa zatiaľ nikto nepresúva so všetkým hneď zajtra. Ale je už v štandardnej knižnici, kde ju chcete mať predtým, než ju potrebujete.
Options.Context slúži na oddelenie domén: podpíšte rovnakým kľúčom pre rôzne účely, pre každý použite iný kontext a podpis z jedného kontextu sa v inom neoverí. Príklad vyššie to presne ukazuje: rovnaký kľúč, rovnaká správa, iný kontext, odmietnutie.
PrivateKey implementuje crypto.Signer, takže zapadá do existujúcich rozhraní. crypto/x509 spracúva kľúče a podpisy ML-DSA a crypto/tls podporuje schémy podpisov MLDSA44/MLDSA65/MLDSA87 v TLS 1.3.
Existuje aj SignDeterministic, ktorý preskakuje náhodnosť, čo je užitočné pre testy a reprodukovateľné zostavenia.
3.4. simd: Vektorové inštrukcie bez assembleru
Go 1.26 zaviedol architektúre špecifický simd/archsimd ako experiment. Go 1.27 pridáva simd, ktorý je prenosný a nezávislý od šírky vektora. Aktivuje sa pomocou 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 // spracovanie repu čiastočným načítaním/uložením — žiadna skalárna čistiaca slučka
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]
Dôležité návrhové rozhodnutie: šírka vektora nie je nikdy pevne zakódovaná. Hodnotu zisťujete pomocou va.Len() za behu. VectorBitSize() vám povie skutočnú šírku, Emulated() povie, či máte skutočný hardvér alebo zdvorilú softvérovú imitáciu. Môj Mac hlásil 128-bitový NEON; stroj s AVX-512 hlási viac; stroj bez podpory hlási emuláciu a kód stále beží.
LoadFloat32sPart/StorePart si zaslúžia osobitnú zmienku. Najotravnejšou časťou ručne písaného SIMD je vždy nepravidelný koniec poľa a toto to rieši bez samostatnej skalárnej slučky.
Stále experimentálne, API stále nestabilné, nenasadzujte to do produkcie. Ale skutočnosť, že ide o kód v Go a nie o assembler alebo cgo, je naozaj veľký posun.
3.5. hash/maphash.Hasher
Nové rozhranie, ktoré popisuje kontrakt medzi hodnotovým typom a kontajnermi založenými na hašovaní:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Prečo? Vstavaná mapa Go prijíma iba kľúče comparable. Nemôžete použiť slice ako kľúč. Nemôžete definovať „rovnosť bez ohľadu na veľkosť písmen“. Hasher rieši obe veci naraz:
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) // rovnaký seed, inak tento celý výpočet nič neznamená
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Pre bežnú sémantiku == existuje ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Záludnosť: Hasher je rozhranie, nie dátová štruktúra. Zatiaľ neexistuje žiadna štandardná hašovacia tabuľka ani Bloomov filter, ktorý by ho využíval. Ide o základ pre budúci balík kontajnerov. Dnes by ste ho použili pri budovaní vlastnej štruktúry alebo pri použití existujúcej implementácie, napríklad go/types.Hasher (ktorý vám umožňuje použiť types.Type ako kľúč mapy s rešpektovaním Identical).
Správa seedov je na vás. Ak by hashOf vyššie vytvárala pri každom volaní nový seed, rovnaké hodnoty by sa hašovali odlišne a príklad by vypísal false, presne ako chyba, ktorú som urobil pri prvom pokuse. Jeden seed na jeden kontajner. (Seed je náhodný, aby sa zabránilo útokom DoS zahlcovaním hašov, a preto nie je len konštantou.)
3.6. httptest.NewTestServer + synctest.Sleep: Úder spiča
Ak z tohto vydania prijmete len jednu vec, nech je to táto.
testing/synctest sa dostal do stavu stabilnej funkcie v Go 1.25 a poskytol nám falošné hodiny na testovanie súbežného kódu. Avšak v momente, keď sa váš test dotkol skutočnej siete, iluzívny svet sa zrútil. httptest.NewTestServer z Go 1.27 používa pamäťovú falošnú sieť, takže bublina zostáva nedotknutá. A synctest.Sleep (= time.Sleep + synctest.Wait) to dopĺňa.
Tu je test opakovania s exponenciálnym backoffom:
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 má byť presne 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)
Prečítajte si to dvakrát. Simuloval tri sekundy backoffu proti skutočnému HTTP serveru a skončil za 0.00 sekúnd. A tvrdenie je elapsed == 3*time.Second, teda presne tri sekundy, nie „aspoň tri sekundy, plus-mínus jitter plánovača“. Falošné hodiny nemajú jitter.
synctest.Sleep existuje z konkrétneho dôvodu: ak váš test spí rovnako dlho ako testovaný kód, nikto nevie, ktorý sa zobudí prvý. synctest.Sleep spí a potom čaká, kým sa všetky ostatné goroutiny v bublina trvalo nezablokujú, takže pozorujete systém až potom, čo sa ustálil.
Časové limity, opakovania, circuit breakery, rate limitery: každý časovo závislý test HTTP klienta, ktorý vlastníte, sa môže stať rýchlym a deterministickým. Ak je váš testovací balík momentálne držaný pokope pomocou time.Sleep(100 * time.Millisecond) a nádeje, toto je vaša cesta von.
3.7. Zmeny v net/http, ktoré skutočne ovplyvnia produkciu
Tiché, ale objavia sa vo vašich metrikách.
Tela odpovedí HTTP/1 sa pri Close automaticky vyprázdňujú. Neprečítaný obsah sa teraz vyprázdni (až do konzervatívneho limitu), keď zatvoríte telo, takže spojenie sa môže znovu použiť. Čo znamená, že konečne môžete zmazať toto zaklínadlo, ktoré si všetci kopírujú z tej istej odpovede na Stack Overflow od roku 2016:
1// už nie je potrebné
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Pre väčšinu programov je to bez efektu alebo malý zisk. Ak to veci zhorší, pravdepodobne ste v skupine, ktorú poznámky k vydaniu zdvorilo opisujú: Transport.MaxIdleConns nastavené na 0 alebo nový Client pre každú požiadavku, čím sa úplne obíde limit nečinných spojení. Transport.DisableKeepAlives = true to zamaskuje, ale skutočná rada z poznámok k vydaniu je, že „hlbší pohľad by pravdepodobne bol prospešný“, čo je v jazyku tímu Go preklad: máte väčšie problémy.
Priorita klienta HTTP/2 (RFC 9218). Server teraz rešpektuje signály priority od klienta. Ak ste preferovali starý round-robin plánovač, nastavte Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Obmedzuje počet hodnôt hlavičiek, ktoré server prijme, predvolene DefaultMaxHeaderValueCount. Ďalšie zatvorené dvere pre „pošli desaťtisíc hlavičiek a sleduj, čo sa stane“.
ALPN na používateľsky poskytnutých spojeniach. Ak vaše net.Conn implementuje ConnectionState() tls.ConnectionState, Transport a Server na ňom vykonajú vyjednávanie TLS ALPN, takže vlastné dialery, ktoré idú cez proxy, môžu stále vyjednať HTTP/2.
3.8. Drobnosti, ktoré prinášajú radosť
strings.CutLast / bytes.CutLast. Cut rozdelí na prvom oddeľovači. Variant „last“ neexistoval, takže každý ručne písal LastIndex plus rezanie a zhruba 30 % z nás na prvý pokus spravilo chybu o jedno.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Skvelé pre prípony súborov a pre parsovanie host:port, kde musíte nájsť posledný dvojbodku, lebo adresy IPv6 ich obsahujú plno.
math/big.Int.Divide. Delenie s explicitným režimom zaokrúhľovania, ktoré nahrádza hádzanie mincou „chcem Quo alebo Div?“:
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
Ak pracujete v doméne, kde je pravidlo zaokrúhľovania zapísané v nariadení, je toto pre vás.
url.URL.Clone a url.Values.Clone. Hlboké kópie. Staré plytké kopírovanie *u zdieľalo ukazovateľ Userinfo, čo vytváralo presne taký druh chyby, ktorej hľadanie zaberie deň a oprava päť sekúnd.
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
Metódy čítania net.UnixConn teraz vracajú io.EOF priamo namiesto jeho obalenia do net.OpError. err == io.EOF funguje teraz, tak ako malo fungovať vždy.
database/sql.ConvertAssign a driver.RowsColumnScanner. Funkcie pre autorov ovládačov. Prvá sprístupňuje konverzie typov, ktoré vykonáva Rows.Scan; druhá umožňuje ovládačom skenovať priamo do cieľov používateľa a preskočiť medziľahlú alokáciu.
unicode 15 → 17. Dve verzie naraz. Klasifikácia reťazcov a správanie normalizácie sa môžu jemne posunúť. Ak máte testy, ktoré na tom závisia, zistíte to.
compress/flate je rýchlejší a jeho výstupné bajty sa môžu líšiť od Go 1.26. Toto sa prenáša na archive/zip, compress/gzip, compress/zlib a image/png. Ak máte testy so zlatými súbormi, ktoré hašujú komprimovaný výstup, rozbijú sa a nebude to regresia. Skontrolujte to pred aktualizáciou, nie počas revízie incidentu.
4. Toolchain
4.1. go fix získal ďalšie modernizátory
go fix sa potichu mení na nástroj na modernizáciu kódu. Go 1.27 pridáva atomictypes, embedlit, slicesbackward a unsafefuncs. Použite -diff na náhľad:
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 }
Na jednom malom súbore sa spustili štyri modernizátory:
embedlit: prepisuje vnorené literály na novú syntax z §1.2atomictypes:atomic.AddInt64(&x, 1)sa mení naatomic.Int64.Add(1), čo znemožňuje neatomický prístup a potichu opravuje chyby zarovnania na 32-bitových systémoch, o ktorých ste nevedelislicesbackward: spätné slučky sa menia naslices.Backwardwaitgroupgo: rituálAdd/go/Donesa mení nawg.Go(premenované zwaitgroupz 1.26 kvôli nejednoznačnosti)
go tool fix help vypíše všetkých 26; go tool fix help <name> vysvetlí jeden. Okrem toho bol fmtappendf odstránený „z dôvodu štylistických obáv“, čo je nádherne diplomatický spôsob, ako opísať to, čo sa stalo vo vlákne tej issue.
Spustenie go fix ./... na starej kódovej základni je naozaj uspokojivé popoludnie. Samozrejme, najprv si prečítajte diff.
4.2. go test predvolene spúšťa stdversion
Toto je zmena, ktorá s najväčšou pravdepodobnosťou preruší váš deň.
go test teraz predvolene spúšťa kontrolu vet stdversion, ktorá označí symboly štandardnej knižnice novšie, než dovoľuje direktíva go v go.mod:
1// go.mod hovorí: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast je symbol z 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]
Toto eliminuje klasický scenár, keď všetko funguje na vašom počítači (najnovší toolchain) a v CI alebo na staršom prostredí používateľa to exploduje. Ak publikujete knižnice s konzervatívnou direktívou go, je to dar.
4.3. go test -json získal OutputType
Riadky "Action":"output" teraz nesú voliteľné pole "OutputType": "error", "error-continue" alebo "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'
Výstup t.Log (bez poľa), výstup t.Error (error) a riadky generované frameworkom (frame) sa teraz dajú rozlíšiť. Každý, kto napísal regulárny výraz na zistenie, ktoré riadky výstupu testu sú „skutočné“, ho môže teraz zmazať.
4.4. Vylepšenia go doc
Syntax package@version. Prečítajte si dokumentáciu konkrétnej verzie bez toho, aby ste ju pridali do svojho modulu:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Praktické na kontrolu zmien pred aktualizáciou.
-ex a výpis zdrojového kódu príkladov:
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
Zdrojový kód príkladu aj očakávaný výstup, v termináli, bez otvárania karty prehliadača, ktorá bude stále otvorená aj budúci štvrtok.
4.5. go mod tidy upratuje vaše bloky require
Pre moduly s go 1.27 alebo novším go mod tidy zlúči rozptýlené bloky require najviac do dvoch (priame a nepriame), pričom zachová pripojené komentáre.
1// Pred
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// Po: 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)
Všimnite si, že komentár // networking sa zachoval. Toto hlavne upratuje po riešení konfliktov zlúčenia v Gite, kde sa rodia toulavé bloky require. Ak má váš tím viac ako troch ľudí, ktorí pridávajú závislosti, konflikty vo vašom go.mod budú o niečo tichšie.
4.6. Rôzne drobnosti
- Podpora
bzrbola odstránená. Ak sa vás to týka, naozaj by ma zaujímal váš príbeh. go tool trace -httpsa pri zadaní iba portu viaže na localhost.-http=:6060už nepočúva na všetkých rozhraniach. Ak ste to mysleli vážne, použite-http=0.0.0.0:6060. Týmto sa správanie zhoduje sgo tool pprofa zabraňuje to občasnému náhodnému verejnému profilovaču.- Súbory odpovedí (
@file) podporujúcompile,link,asm,cgo,coverapackv formáte kompatibilnom s GCC. Pre build systémy, ktoré prekračujú limity dĺžky príkazového riadka, ahoj, Bazel.
5. Kompilátor, linker, porty
Kompilátor. Relatívne názvy súborov v direktívach //line sa teraz riešia voči adresáru obsahujúceho súboru, v súlade s go/scanner. Relevantné, ak píšete generátory kódu.
Názvy symbolov funkčných literálov (uzáverov) sú tiež jednoduchšie: rovnaký názov bez ohľadu na inlining a viacero inštancií toho istého literálu môže zdieľať kód v binárke. Bez funkčnej zmeny, až na to: kód, ktorý porovnáva identitu funkcií cez reflect.Value.Pointer, uvidí „rovnosť“ častejšie než predtým. Takéto porovnanie nikdy nebolo platné, ale ak ho máte, teraz začne klamať hlasnejšie.
Linker. Nové možnosti -macos a -macsdk nastavujú verzie OS a SDK v load command LC_BUILD_VERSION na macOS.
Porty.
- Darwin teraz vyžaduje macOS 13 Ventura alebo novší, ako bolo oznámené v 1.26. Skontrolujte svoje CI runnery.
- PowerPC (
GOOS=linux GOARCH=ppc64) prešiel na ABI ELFv2. Vyžaduje Linux kernel 3.13+ (RHEL7 spätne portovaný na 3.10). Cgo, PIE a externé linkovanie sú teraz podporované. Ak používate cgo, ale potrebujete statickú čisto Go binárku, nastavteCGO_ENABLED=0.
6. Kontrolný zoznam aktualizácie
Go 1.27 berie kompatibilitu vážne, ale pred zvýšením verzie si skontrolujte tieto body:
- Testy so zlatými súbormi nad komprimovaným výstupom.
compress/flatezmenil kodéry; bajty gzip/zip/png sa môžu líšiť. - Testy porovnávajúce reťazce chybových hlásení JSON. Správanie je identické; text chyby nie je.
- Odstránené GODEBUG v
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— tieto zlyhajú zostavenie iba vtedy, ak sú nastavené na svoje staré hodnoty. - Porušenia
stdversion.go testich teraz zachytáva. Ak udržiavate knižnicu s konzervatívnou direktívougo, spúšťajte to včas. - CI runnery s macOS 12 alebo starším. Podpora zanikla.
- Testy tvrdiace niečo o názvoch symbolov funkčných literálov a porovnania funkcií cez
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0aleboClientpre každú požiadavku. Spolu s automatickým vyprázdňovaním tiel odpovedí to môže spomaliť.
Záverečné myšlienky
Go 1.27 je vydanie, kde naraz prišla splatná veľa odkladanej údržby.
Generické metódy boli chýbajúcim kúskom práce na generikách z 1.18. Selektory polí v literáloch štruktúr uzavreli jedenásťročnú otvorenú otázku. encoding/json/v2 bol dva roky diskusie o návrhu. A uuid zaplnil dieru, ktorú takmer každý projekt v Go desaťročie zakrýval tou istou závislosťou tretej strany.
Ak chcete prioritné poradie pre skutočné získanie hodnoty z tohto vydania:
httptest.NewTestServer+synctest.Sleep: časovo závislé HTTP testy sa stanú rýchlymi a deterministickými. Najlepší pomer úsilia k výsledku v celom vydaní.- Profil úniku goroutín: vystavte jeden endpoint na stagingu a pripravte sa na pokoru.
- Návestia goroutín v tracebackoch: tri riadky
pprof.Dona vstupnom bode vašich požiadaviek zmenia váš ďalší výpis zásobníka o 3:00 ráno na čitateľnú vec. go fix ./...: nechajte nástroj urobiť modernizáciu.encoding/json/v2: nikam sa neponáhľajte. Prijmite ho po jednej možnosti.
Stiahnite si go1.27rc3 a spustite na ňom svoj testovací balík ešte dnes. Všetko na tomto kontrolnom zozname sa dá príjemnejšie objaviť v utorok popoludní než počas incidentu.
Referencie