GoSuda

Go 1.27, alebo ako som sa prestal báť a zamiloval si Generic Methods

By iwanhae
views ...

Prehľad Go 1.27

Go 1.27 prichádza v auguste 2026 a tentoraz sa zmenil samotný jazyk. Nie spôsobom „pridali sme novú funkciu do slices“ — zmenil sa skutočne. Generické metódy sú tu. Jedenásť rokov starý problém bol uzavretý. encoding/json bol potichu nahradený novým enginom, zatiaľ čo lietadlo bolo vo vzduchu.

Každý príklad uvedený nižšie bol spustený na go1.27rc3 (darwin/arm64). Každý výstupný blok je skutočný výstup, skopírovaný a vložený, vrátane chybových hlásení. Ak niečo vyzerá zvláštne, je to preto, lebo presne tak to uviedol kompilátor.

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

1. Jazyk sa zmenil (Áno, skutočne)

1.1. Generické metódy

Od verzie Go 1.18 máme generiká a od verzie Go 1.18 máme aj „Konverzáciu“. Znie takto:

„Dovoľte mi len napísať metódu Map pre môj typ slice—“

method must have no type parameters

„...dobre. Bude to funkcia na úrovni balíka.“

Metódy mohli používať iba typové parametre deklarované prijímateľom (receiver). Vaša vlastná metóda nemohla zaviesť nové. Takže každá generická transformácia bola vykázaná do rozsahu balíka, kde sedela vedľa sedemnástich ďalších voľne plávajúcich pomocných funkcií s názvami ako variácie 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}

Prijímateľ nemusí byť ani 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) }

Teraz, skôr než dnes popoludní refaktorujete celú svoju kódovú základňu, existujú dve obmedzenia, a tie sú dôležitejšie, než sa zdá:

  1. Metódy rozhrania (interface methods) nemôžu deklarovať typové parametre.
  2. Generické metódy nemôžu implementovať metódy rozhrania.

To druhé je to, čo vás zahryzne:

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

Takže generické metódy a dynamické odosielanie (dynamic dispatch) nejdú dokopy. Toto nie je tým, že by bol tím Go skúpy — je to preto, že generická metóda má nekonečne veľa inštancií a tabuľka metód rozhrania musí byť konečná a známa v čase kompilácie. Nemôžete vložiť nekonečnú vec do konečnej tabuľky. Vesmír povedal nie.

Praktické čítanie: generické metódy sú pre konkrétne typy s utilitnými API. Čokoľvek, čo sa musí skrývať za rozhraním, stále potrebuje starý prístup.

Štandardná knižnica to už využila. Rand v 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 rand.N na úrovni balíka, čo znamenalo použitie globálneho zdroja. Teraz váš vlastný osetý (seeded) *Rand získava rovnaké pohodlie. Malá vec. Pekná vec.

1.2. Selektory polí v štruktúrnych literáloch, alebo: Problém #9859 konečne odpočíva

Vnorenie (embedding) vám dáva u.ID. Vnorenie vám nedáva User{ID: 1}. Namiesto toho vám dáva User{Base: Base{ID: 1}}, čo je spôsob Go, ako sa opýtať, či ste to naozaj mysleli vážne.

Problém #9859 bol nahlásený v roku 2015. Teraz bol uzavretý. Niekde dostáva gopher, ktorý odvtedy dvakrát zmenil kariéru, upozornenie na GitHub.

 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 propagované pole priamo
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name nie je pole Line. Je to pole vnoreného Object. Predtým by ste napísali Line{Object: Object{name: "diagonal"}, ...}.

Teraz k drobnému písmu, ktoré som skontroloval tak, že som sa pokúsil všetko skompilovať.

(a) Kľúčom je stále jednoduchý identifikátor. Nemôžete napí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 „kľúče teraz fungujú ako prístup k poliam“. Je to špecificky „implicitne propagované názvy polí sú povolené“. O niečo užšie, než naznačuje nadpis.

(b) Nemôžete špecifikovať vnorené pole a zároveň niečo, čo je z neho propagované.

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 pozvané.

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

Ak chcete nasledovať ukazovateľ, potrebujete ukazovateľ, ktorý môžete nasledovať, a v čase konštrukcie literálu ešte žiadny neexistuje. Tiež fér.

Dobrá správa: go fix prepíše vaše staré literály za vás. Viac o tom neskôr.

1.3. Odvodzovanie typov funkcií je menej svojvoľné

Odvodzovanie typov pre generické funkcie fungovalo na niektorých miestach a na iných nie, bez zjavného princípu. Priradenie k premennej? V poriadku. Vloženie tej istej funkcie do poľa štruktúry? Chyba kompilátora, napíšte double[int] ako v roku 2022.

Go 1.27 robí odvodzovanie funkčným 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: potrebovalo double[int]
 8	a := A{double}             // 1.26: potrebovalo double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: potrebovalo double[int]
12
13	var fn func(float64) float64 = double  // toto vždy fungovalo
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

Jedno double, inštancované ako func(int) int na troch miestach a func(float64) float64 na štvrtom, úplne z kontextu. Ak píšete konfiguračné štruktúry alebo tabuľky obsluhy plné funkčných hodnôt, z vašej kódovej základne zmizne množstvo šumu [T].

2. Runtime: Voľný výkon a detektor únikov

2.1. Alokácia špecializovaná na veľkosť

Kompilátor teraz generuje volania rutín na alokáciu špecializovaných na veľkosť pre malé objekty. Poznámky k vydaniu uvádzajú až 30 % úsporu pri alokáciách pod 80 bajtov a celkovo približne 1 % pre programy náročné na alokácie.

Neverím poznámkam k vydaniu 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# 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 % rýchlejšie v mikrobenchmarku, ktorý nerobí nič iné, len alokuje na Apple Silicon. Inými slovami: toto je najlepší možný prípad, dosiahnutý v laboratórnych podmienkach benchmarkom navrhnutým tak, aby číslo vyzeralo dobre. V skutočnom programe väčšinu z toho pochová GC a skutočná práca. Tvrdenie o ~1 % je to úprimné.

Cenou je veľkosť binárneho súboru. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Približne 50 KB, pevne, bez ohľadu na váš program. Môžete sa odhlásiť pomocou GOEXPERIMENT=nosizespecializedmalloc, ale táto úniková cesta je naplánovaná na odstránenie v Go 1.28, takže ju berte skôr ako riešenie na nahlásenie chyby než ako životný štýl.

2.2. Profil úniku gorutín je teraz skutočný

Experimentálny v Go 1.26, všeobecne dostupný v 1.27. GOEXPERIMENT pre goroutineleakprofile je preč; jednoducho to 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 gorutíny, natrvalo zaseknuté, s číslom súboru a riadku, ktoré presne ukazujú, kde ste to urobili.

Trik za tým je skutočne šikovný: znovu používa analýzu dosiahnuteľnosti garbage collectora. Ak je gorutína G zablokovaná na primitíve P a P je nedosiahnuteľná z akejkoľvek spustiteľnej gorutíny (alebo čohokoľvek, čo by tieto gorutíny mohli zobudiť), potom sa P už nikdy nič nedotkne, takže G sa nikdy nezobudí. To nie je heuristika — to je dôkaz.

Rovnaký dizajn vám dáva obmedzenie zadarmo: ak je kanál alebo mutex dosiahnuteľný cez globálnu premennú alebo cez lokálnu premennú nejakej gorutíny, ktorá stále beží, GC ho stále vidí, takže runtime nemôže nič vyvodiť. Všimnite si, že aj príklad hračky vyššie potrebuje dve volania runtime.GC() na nahlásenie. Nezachytí všetko.

Zachytí však klasické „zabudol som zrušiť kontext, pracovná gorutína žije navždy“ — čo je väčšina únikov väčšinu času.

Ak importujete net/http/pprof, je to tiež na /debug/pprof/goroutineleak. Zapojte to do stagingu, kontrolujte to týždenne, buďte potichu zhrození.

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 zomrela

Pre moduly deklarujúce Go 1.27 alebo novšie, hlavičkové riadky tracebacku teraz obsahujú štítky gorutí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...

Dôležitý detail: štítky sú dedené podriadenými gorutínami. Gorutína 3 nikdy nenastavila štítok. Dostala ho od svojho rodiča.

Takže keď nabudúce produkcia zablokuje (deadlock) a niekto pošle SIGQUIT procesu, namiesto 4 000 gorutín, ktoré vyzerajú identicky, dostanete 4 000 gorutín označených tým, ku ktorej požiadavke a ktorému nájomníkovi patria. Tri riadky pprof.Do vo vašom vstupnom bode požiadavky vám to zaistia.

Pretože štítky môžu obsahovať veci, ktoré by ste radšej nevypisovali na stderr, GODEBUG=tracebacklabels=0 to vypne a toto odhlásenie je explicitne určené na to, aby zostalo navždy.

2.4. asynctimerchan je definitívne preč

Go 1.23 urobilo časovačové kanály nebufferovanými (synchrónnymi) a ponúklo asynctimerchan=1 ako cestu späť k starému správaniu. V 1.27 je toto nastavenie natrvalo odstránené. Kanály balíka time sú synchrónne, bodka, žiadne vyjednávanie.

Zaujímavou časťou je politika zavedená popri tom. Odstránený GODEBUG ponechaný vo vašom go.mod automaticky nezastaví zostavenie — zlyhá 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(zostaví sa v poriadku)

Čo znamená, že jediní ľudia, na ktorých sa kričí, sú tí, ktorí sa skutočne spoliehali na odstránené správanie. Každý, kto to nastavil na prípadný predvolený stav a zabudol na to, na to nemusí myslieť. To je premyslený kus archeológie API.

3. Štandardná knižnica

3.1. encoding/json/v2: Počas letu vymenili motor

Toto je tá veľká vec. Dva a viac rokov diskusie o návrhu konečne pristáli.

Teraz existujú tri balíky a pochopenie rozdelenia je polovica bitky:

BalíkÚloha
encoding/jsonV1 API, ktoré poznáte. Správanie 100% nezmenené. Teraz implementované nad v2
encoding/json/v2Sémantické spracovanie. Hodnoty Go ↔ JSON
encoding/json/jsontextSyntaktické spracovanie. JSON ako prúd tokenov

Hlavnou správou je, že encoding/json bol prestavaný na úplne inej implementácii a správa sa identicky. Tu je ako:

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

Každý starý výstrelok v1 bol zakódovaný ako možnosť, zbalený do DefaultOptionsV1() a v1 API vždy aplikuje tento balík. Čo 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 ticho akceptuje duplicitné kľúče a berie ten posledný. Vždy to robilo. Stále to robí. Kompatibilita znamená kompatibilitu aj so zlými časťami.

v2 má medzitý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 neúhľadné — keď sa dva parsery nezhodnú na tom, ktorý duplicitný kľúč vyhráva, dostanete bezpečnostné chyby. CVE-2017-12635 v CouchDB bolo presne toto: JSON telo s dvoma kľúčmi roles, kde validátor prečí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// sorted map keys + indentation
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// reject unknown fields — previously required a Decoder and 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, sú teraz príznaky.

A príbeh migrácie je najlepšia časť. Posledné možnosti vyhrávajú, takže môžete prijímať prísnosť v2 po jednom správaní:

1// keep v1 semantics, but reject duplicate keys like v2 does
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Nemusíte portovať 200-tisícovú kódovú základňu na v2, aby ste prestali akceptovať duplicitné kľúče. Preklopíte jednu možnosť. Pre niečo veľké je toto realistická cesta.

Výkon: marshalling je zhruba na rovnakej úrovni, unmarshalling je výrazne rýchlejší. Ak sa niečo pokazí, GOEXPERIMENT=nojsonv2 obnoví starú implementáciu — a aj toto odhlásenie je nakoniec na odstrel, takže radšej pošlite hlásenie o chybe, než aby ste sa usadili.

jsontext je nízkoúrovňová vrstva: Encoder/Decoder prechádzajúci JSON ako Tokeny a Valuey so stavovým automatom, ktorý vás drží v poctivosti. Siahnite po ňom, keď píšete streamovací transformátor alebo JSON filter a nechcete vôbec materializovať hodnoty Go.

3.2. Štandardný balík uuid

Konečne. RFC 9562, v štandardnej knižnici, žiadne go get nie je potrebné.

 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 sa vždy triedia v poradí vytvorenia
 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 je 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.

Detaily, ktoré stojí za to vedieť:

  • UUID je [16]byte, takže == funguje a je priamo použiteľný ako kľúč mapy. Rovnaký dizajn ako google/uuid.
  • Nil a Max sú funkcie, nie premenné. Pretože var Nil UUID na úrovni balíka je nabitá zbraň namierená na vašu nohu a niekto, niekde, by do nej nakoniec priradil.
  • Náhodné bity pochádzajú z kryptograficky bezpečného generátora.
  • Implementuje encoding.TextMarshaler/TextUnmarshaler/TextAppender, takže padá priamo do JSON štruktúr.
  • NewV7 je ten, ktorý chcete pre primárne kľúče databáz. Časové zoradenie znamená, že váš B-stromový index prestane fragmentovať, v čom sú v4 UUID notoricky zlé.

Teraz môžete odstrániť závislosť. Ak potrebujete v1/v3/v5 alebo vymyslenejšie možnosti parsovania, knižnice tretích strán majú stále prácu.

3.3. crypto/mldsa: Post-kvantové podpisy a sú obrovské

Go 1.24 nám dalo crypto/mlkem pre post-kvantovú výmenu kľúčov. Go 1.27 prináša druhú polovicu: ML-DSA podpisy, š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 tie čísla. Jeden podpis má 3 309 bajtov. Ed25519 má 64. To je 50-násobný nárast a verejný kľúč má k tomu takmer 2 KB. Vložte niekoľko z nich do certifikačného reťazca a váš TLS handshake začne potrebovať vlastnú stratégiu MTU.

Toto je skutočná cena kvantovej odolnosti dnes a preto nikto zajtra všetko neprepína. Ale je to v štandardnej knižnici teraz, čo je tam, kde to chcete mať predtým, než to budete potrebovať.

Options.Context je doménová separácia: podpíšte rovnakým kľúčom na rôzne účely, použite iný kontext pre každý a podpis z jedného kontextu sa neoverí v inom. Príklad vyššie ukazuje presne to — rovnaký kľúč, rovnaká správa, iný kontext, odmietnuté.

PrivateKey implementuje crypto.Signer, takže zapadá do existujúcich rozhraní. crypto/x509 zvláda ML-DSA kľúče a podpisy a crypto/tls podporuje schémy podpisov MLDSA44/MLDSA65/MLDSA87 v TLS 1.3.

Existuje aj SignDeterministic, ktorý preskakuje náhodnosť — šikovné pre testy a reprodukovateľné zostavenia.

3.4. simd: Vektorové inštrukcie bez assembleru

Go 1.26 predstavilo architektúrne špecifické simd/archsimd ako experiment. Go 1.27 pridáva simdprenosné a nezávislé od šírky vektora. Povoľte 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	// spracujte chvost s č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]

Dizajnová voľba, na ktorej záleží: šírka vektora nie je nikdy pevne zakódovaná. Pýtate sa va.Len() za behu. VectorBitSize() vám povie skutočnú šírku, Emulated() vám povie, či ste dostali skutočný hardvér alebo zdvorilú softvérovú imitáciu. Môj Mac nahlásil 128-bitový NEON; stroj s AVX-512 nahlási viac; stroj s ničím nahlá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 rozstrapkaný chvost na konci poľa a toto ho zvláda bez samostatnej skalárnej slučky.

Stále experimentálne, API stále nestabilné, nedávajte to do produkcie. Ale fakt, že toto je kód Go a nie assembler alebo cgo, je skutočne veľká vec.

3.5. hash/maphash.Hasher

Nové rozhranie opisujúce kontrakt medzi typom hodnoty a kontajnermi založenými na hashovaní:

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

Prečo? Vstavaná mapa Go akceptuje iba comparable kľúče. Nemôžete kľúčovať podľa slice. Nemôžete definovať „rovnaké bez ohľadu na veľkosť písmen“. Hasher rieši obe 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 to 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)

Háčik: Hasher je rozhranie, nie dátová štruktúra. Zatiaľ neexistuje žiadna štandardná hashovacia tabuľka alebo Bloomov filter, ktorý by ho konzumoval. Toto sú základy pre budúci balík kontajnerov. Dnes by ste ho použili pri budovaní vlastnej štruktúry alebo konzumovali existujúcu implementáciu ako go/types.Hasher (ktorý vám umožňuje použiť types.Type ako kľúč mapy, rešpektujúc Identical).

Správa seedu je na vás. Ak by hashOf vyššie vytvoril nový seed pri každom volaní, identické hodnoty by sa hashovali inak a príklad by vypísal false — čo je presne tá chyba, ktorú som napísal pri prvom pokuse. Jeden seed na kontajner. (Seed je randomizovaný, aby sa prekazili DoS útoky hash-floodingom, čo je dôvod, prečo to nie je len konštanta.)

3.6. httptest.NewTestServer + synctest.Sleep: Hit, ktorý spí

Ak si z tohto vydania osvojíte len jednu vec, urobte to túto.

testing/synctest promoval v Go 1.25 a dal nám falošné hodiny pre testovanie súbežného kódu. Až na to, že v momente, keď sa váš test dotkol skutočnej siete, ilúzia sa zrútila. httptest.NewTestServer v Go 1.27 používa falošnú sieť v pamäti, takže bublina zostáva neporušená. 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 by mal 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. Simulovalo to tri sekundy backoffu proti skutočnému HTTP serveru a skončilo to za 0,00 sekundy. A tvrdenie je elapsed == 3*time.Secondpresne tri sekundy, nie „aspoň tri sekundy, plus-mínus jitter plánovača“. Falošné hodiny nejitrujú.

synctest.Sleep existuje z konkrétneho dôvodu: ak váš test spí rovnako dlho ako testovaný kód, to, kto sa zobudí skôr, je hádanka. synctest.Sleep spí a potom čaká, kým každá iná gorutína v bubline nie je trvalo zablokovaná, takže pozorujete systém potom, čo sa usadil.

Časové limity, opakovania, ističe, obmedzovače rýchlosti — každý test HTTP klienta závislý od času, ktorý vlastníte, sa môže stať rýchlym a deterministickým. Ak je vaša testovacia sada momentálne držaná pokope pomocou time.Sleep(100 * time.Millisecond) a nádeje, toto je váš východ.

3.7. Zmeny v net/http, ktoré skutočne ovplyvňujú produkciu

Tiché, ale ukážu sa vo vašich metrikách.

Telá odpovedí HTTP/1 sa automaticky vypustia pri Close. Neprečítaný obsah sa teraz vypustí (až do konzervatívneho limitu), keď zatvoríte telo, takže pripojenie môže byť znovu použité. Čo znamená, že konečne môžete vymazať toto zaklínadlo, ktoré všetci kopírovali 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 no-op alebo malá výhra. Ak to robí veci horšími, pravdepodobne ste v kategórii, ktorú poznámky k vydaniu zdvorilo opisujú: Transport.MaxIdleConns nastavené na 0 alebo nový Client na požiadavku, obchádzajúci limit nečinných pripojení úplne. Transport.DisableKeepAlives = true to zalepí, ale skutočná rada v poznámkach k vydaniu je, že „hlbší pohľad by bol pravdepodobne prospešný“, čo je tím Go pre máte väčšie problémy.

Priorita klienta HTTP/2 (RFC 9218). Server teraz rešpektuje signály priority klienta. Ak ste preferovali staré plánovanie round-robin, použite Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Obmedzuje, koľko hodnôt hlavičiek server prijme, predvolene na DefaultMaxHeaderValueCount. Ďalšie dvere zatvorené pred „pošli desaťtisíc hlavičiek a sleduj, čo sa stane“.

ALPN na užívateľom poskytnutých conns. Ak váš net.Conn implementuje ConnectionState() tls.ConnectionState, Transport a Server na ňom vykonajú TLS ALPN vyjednávanie — takže vlastné dialery prechádzajúce cez proxy stále môžu vyjednať HTTP/2.

3.8. Drobnosti, ktoré prinášajú radosť

strings.CutLast / bytes.CutLast. Cut delí na prvom separátore. Neexistoval žiadny variant „posledný“, takže všetci ručne písali LastIndex plus krájanie (slicing) a zhruba 30 % z nás urobilo chybu o jeden (off-by-one) na prvý pokus.

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, pretože IPv6 adresy sú nimi plné.

math/big.Int.Divide. Delenie s explicitným režimom zaokrúhľovania, nahrádzajúce hod 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 oblasti, kde je pravidlo zaokrúhľovania napísané v predpise, toto je pre vás.

url.URL.Clone a url.Values.Clone. Hlboké kópie. Stará plytká kópia *u zdieľala ukazovateľ Userinfo, čo produkovalo presne ten druh chyby, ktorú trvá deň nájsť a päť sekúnd opraviť.

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 metódy čítania teraz vracajú io.EOF priamo namiesto toho, aby ho obalili do net.OpError. err == io.EOF teraz funguje, ako by vždy malo.

database/sql.ConvertAssign a driver.RowsColumnScanner. Funkcie pre autorov ovládačov. Prvá odhaľuje typové konverzie, ktoré vykonáva Rows.Scan; druhá umožňuje ovládačom skenovať priamo do cieľov používateľa, čím sa preskočí medzialokácia.

unicode 15 → 17. Dve verzie v jednom skoku. Klasifikácia reťazcov a správanie normalizácie sa môžu jemne posunúť. Ak máte testy, ktoré od toho závisia, zistíte to.

compress/flate sa zrýchlilo — a jeho výstupné bajty sa môžu líšiť od Go 1.26. To kaskáduje do archive/zip, compress/gzip, compress/zlib a image/png. Ak máte testy so zlatými súbormi (golden-file), ktoré hashujú komprimovaný výstup, tie sa pokazia a nebude to regresia. Skontrolujte to predtým, než upgradujete, nie počas kontroly incidentu.

4. Toolchain

4.1. go fix získal viac modernizátorov

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 pre 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 }

Štyria modernizátori vystrelili na jeden malý súbor:

  • embedlit — prepisuje vložené literály pomocou novej syntaxe z §1.2
  • atomictypesatomic.AddInt64(&x, 1) sa stáva atomic.Int64.Add(1), čo znemožňuje neatomický prístup a potichu opravuje chyby zarovnania na 32 bitov, o ktorých ste nevedeli
  • slicesbackward — spätné slučky sa stávajú slices.Backward
  • waitgroupgo — rituál Add/go/Done sa stáva wg.Go (premenované z waitgroup z 1.26, aby sa vyhlo nejednoznačnosti)

go tool fix help vypisuje všetkých 26; go tool fix help <name> vysvetľuje jeden. Tiež fmtappendf bol odstránený „kvôli štylistickým obavám“, čo je krásne diplomatický spôsob, ako opísať čokoľvek, čo sa stalo v tom vlákne problému.

Spustenie go fix ./... na starej kódovej základni je skutočne uspokojujúce popoludnie. Prečítajte si diff, samozrejme.

4.2. go test spúšťa stdversion predvolene

Toto je zmena, ktorá s najväčšou pravdepodobnosťou preruší váš deň.

go test teraz predvolene spúšťa kontrolu vet stdversion, označujúcu symboly štandardnej knižnice novšie, než povoľuje direktíva go vo vašom go.mod:

1// go.mod says: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLast je symbol 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 zabíja klasický režim zlyhania, kde všetko funguje na vašom stroji (najnovší toolchain) a vybuchne v CI alebo v staršom prostredí používateľa. Ak publikujete knižnice s konzervatívnou direktívou go, toto je 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 (žiadne pole), výstup t.Error (error) a riadky generované frameworkom (frame) sú teraz rozlíšiteľné. Každý, kto napísal regex, aby zistil, ktoré riadky testovacieho výstupu sú „skutočné“, ho teraz môže vymazať.

4.4. Vylepšenia go doc

Syntax package@version. Prečítajte si dokumentáciu pre konkrétnu verziu 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...

Šikovné na kontrolu toho, čo sa zmenilo pred upgradom.

-ex a tlač 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

Zdroj príkladu a očakávaný výstup, v termináli, bez otvárania karty prehliadača, ktorá bude otvorená aj na budúci štvrtok.

4.5. go mod tidy čistí vaše bloky require

Pre moduly na go 1.27 alebo novších go mod tidy zlučuje rozptýlené bloky require do maximálne dvoch (priamych a nepriamych), pričom zachováva pripojené komentáre.

 1// Before
 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// After: 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 prežil. Toto väčšinou upratuje po riešení konfliktov zlúčenia v Gite, kde sa rodia zatúlané bloky require. Ak má váš tím viac ako troch ľudí pridávajúcich závislosti, vaše konflikty v go.mod budú citeľne tichšie.

4.6. Zvyšky

  • Podpora bzr odstránená. Ak sa vás to týka, skutočne by som si rád vypočul ten príbeh.
  • go tool trace -http sa viaže na localhost, keď dostane iba port. -http=:6060 už nepočúva na každom rozhraní. Použite -http=0.0.0.0:6060, ak ste to mysleli vážne. Toto teraz zodpovedá go tool pprof a zabraňuje občasnému náhodnému verejnému profileru.
  • Súbory odpovedí (@file) sú podporované nástrojmi compile, link, asm, cgo, cover a pack vo formáte kompatibilnom s GCC. Pre systémy zostavenia, ktoré prekračujú limity dĺžky príkazového riadku — ahoj, Bazel.

5. Kompilátor, Linker, Porty

Kompilátor. Relatívne názvy súborov v direktívach //line sa teraz rozlišujú voči adresáru obsahujúceho súboru, čo zodpovedá go/scanner. Relevantné, ak píšete generátory kódu.

Symbolické názvy literálov funkcií (uzáverov) sú teraz tiež jednoduchšie — rovnaký názov bez ohľadu na inlining a viac inštancií toho istého literálu môže zdieľať kód v binárnom súbore. Žiadna funkčná zmena, okrem: kód, ktorý porovnáva identitu funkcií cez reflect.Value.Pointer, uvidí „rovnaké“ častejšie než predtým. Toto porovnanie nikdy nebolo platné, ale ak ho máte, teraz je čas, kedy vám začne klamať hlasnejšie.

Linker. Nové možnosti -macos a -macsdk nastavujú verzie OS a SDK v príkaze načítania LC_BUILD_VERSION pre macOS.

Porty.

  • Darwin teraz vyžaduje macOS 13 Ventura alebo novší, ako bolo oznámené v 1.26. Skontrolujte svojich CI bežcov.
  • PowerPC (GOOS=linux GOARCH=ppc64) prešiel na ELFv2 ABI. Vyžaduje jadro Linuxu 3.13+ (RHEL7 backportované na 3.10). Cgo, PIE a externé linkovanie sú teraz podporované. Ak používate cgo, ale potrebujete statickú čistú binárku Go, nastavte CGO_ENABLED=0.

6. Kontrolný zoznam upgradu

Go 1.27 berie kompatibilitu vážne, ale skontrolujte tieto veci predtým, než zvýšite verziu:

  • Testy zlatých súborov na komprimovanom výstupe. compress/flate zmenilo enkodéry; bajty gzip/zip/png sa môžu líšiť.
  • Testy porovnávajúce reťazce chybových správ JSON. Správanie je identické; text chyby nie je.
  • Odstránené GODEBUGy v go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — tieto zlyhajú pri zostavení iba ak sú nastavené na svoje staré hodnoty.
  • Porušenia stdversion. go test ich teraz zachytáva. Spustite ho včas, ak udržiavate knižnicu s konzervatívnou direktívou go.
  • CI bežci s macOS 12 alebo starším. Podpora je preč.
  • Testy vyžadujúce názvy symbolov literálov funkcií a porovnania funkcií reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 alebo Client na požiadavku. V kombinácii s automatickým vypúšťaním tiel odpovedí to môže byť pomalšie.

Záverečné myšlienky

Go 1.27 je vydanie, kde sa veľa odloženej údržby stalo splatnou naraz.

Generické metódy boli chýbajúcim kúskom práce na generikách z 1.18. Selektory polí štruktúrnych literálov uzavreli jedenásť rokov starý problém. encoding/json/v2 boli dva roky diskusie o návrhu. A uuid vyplnilo dieru, ktorú v podstate každý projekt Go záplatoval rovnakou závislosťou tretej strany po desaťročie.

Ak chcete poradie priorít pre skutočné získanie hodnoty z tohto:

  1. httptest.NewTestServer + synctest.Sleep — HTTP testy závislé od času sa stávajú rýchlymi a deterministickými. Najlepší pomer úsilia k zisku v celom vydaní.
  2. Profil úniku gorutín — vystavte jeden endpoint v stagingu a pripravte sa na pokoru.
  3. Štítky gorutín v tracebacku — tri riadky pprof.Do vo vašom vstupnom bode požiadavky zmenia váš ďalší stack dump o 3 ráno na niečo čitateľné.
  4. go fix ./... — nechajte nástroj urobiť modernizáciu.
  5. encoding/json/v2 — žiadny zhon. Osvojte si ho po jednej možnosti.

Zoberte go1.27rc3 a spustite proti nemu svoju testovaciu sadu teraz. Všetko v tom kontrolnom zozname je výrazne príjemnejšie objaviť v utorok popoludní než počas incidentu.

Referencie