GoSuda

Go 1.27, aneb jak jsem se přestal bát a začal milovat Generic Methods

By iwanhae
views ...

Přehled Go 1.27

Go 1.27 přichází v srpnu 2026 a poprvé se změnil samotný jazyk. Ne změnil ve smyslu „přidali jsme novou funkci do slices“ — skutečně se změnil. Generické metody jsou zde. Jedenáct let starý problém byl uzavřen. encoding/json byl potichu nahrazen novým enginem za plného provozu.

Každý níže uvedený příklad byl spuštěn na go1.27rc3 (darwin/arm64). Každý výstupní blok je skutečným výstupem, zkopírovaným a vloženým, včetně chybových zpráv. Pokud něco vypadá podivně, je to proto, že přesně tak to uvedl kompilátor.

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

1. Jazyk se změnil (Ano, skutečně)

1.1. Generické metody

Od Go 1.18 máme generika a od Go 1.18 vedeme „tu konverzaci“. Zní následovně:

„Dovolte mi jen napsat metodu Map pro můj typ slice—“

method must have no type parameters

„...dobrá. Bude to tedy funkce na úrovni balíčku.“

Metody mohly používat pouze typové parametry deklarované příjemcem (receiverem). Vaše vlastní metoda nemohla zavádět nové. Takže každá generická transformace byla vykázána do rozsahu balíčku, kde ležela vedle sedmnácti dalších volně stojících pomocných funkcí s názvy jako variace na MapSlice.

Go 1.27 toto opravuje:

 1type List[E any] []E
 2
 3// F je typový parametr deklarovaný samotnou metodou
 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}

Příjemce nemusí být ani generický. Obyčejná struktura může mít generickou metodu:

1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }

Nyní, než dnes odpoledne refaktorujete celou svou kódovou základnu, existují dvě omezení a jsou důležitější, než se zdá:

  1. Metody rozhraní (interface) nemohou deklarovat typové parametry.
  2. Generické metody nemohou implementovat metody rozhraní.

To druhé je to, co vás kousne:

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é metody a dynamické odesílání (dispatch) se tedy neslučují. Nejde o to, že by tým Go byl lakomý — jde o to, že generická metoda má nekonečně mnoho instancí a tabulka metod rozhraní musí být konečná a známá v době kompilace. Nemůžete vložit nekonečnou věc do konečné tabulky. Vesmír řekl ne.

Praktický závěr: generické metody jsou určeny pro konkrétní typy s API ve stylu utility. Cokoli, co se musí skrývat za rozhraním, stále vyžaduje starý přístup.

Standardní knihovna toho již využila. Rand v math/rand/v2 získal generickou metodu:

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

Dříve byla generická pouze rand.N na úrovni balíčku, což znamenalo použití globálního zdroje. Nyní získá váš vlastní inicializovaný *Rand stejné pohodlí. Malá věc. Pěkná věc.

1.2. Selektory polí ve strukturových literálech, aneb: Problém #9859 konečně odpočívá

Vnoření (embedding) vám poskytne u.ID. Vnoření vám neposkytne User{ID: 1}. Místo toho vám poskytne User{Base: Base{ID: 1}}, což je způsob Go, jak se zeptat, zda jste to skutečně mysleli vážně.

Problém #9859 byl nahlášen v roce 2015. Nyní byl uzavřen. Někde gopher, který od té doby dvakrát změnil kariéru, dostává oznámení na 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žijte přímo povýšené pole
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name není polem Line. Je to pole vnořeného Object. Dříve byste psali Line{Object: Object{name: "diagonal"}, ...}.

Nyní k drobnému písmu, které jsem ověřil tím, že jsem se pokusil vše zkompilovat.

(a) Klíčem je stále prostý identifikátor. Nemůžete napsat libovolnou cestu selektoru.

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

Funkce tedy neznamená „klíče nyní fungují jako přístup k poli“. Specificky to znamená „implicitně povýšené názvy polí jsou povoleny“. O něco užší význam, než naznačuje titulek.

(b) Nemůžete specifikovat vnořené pole a zároveň něco, co je z něj povýšeno.

1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object

Rozumné. Řekli jste mu dvě protichůdné věci o stejné paměti a on odmítl hádat.

(c) Vnořování ukazatelů není pozváno.

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

Abyste mohli následovat ukazatel, potřebujete ukazatel, který lze následovat, a v době vytváření literálu ještě žádný neexistuje. Také fér.

Dobrá zpráva: go fix za vás přepíše vaše staré literály. Více o tom později.

1.3. Odvození typu funkce je méně svévolné

Odvození typu pro generické funkce dříve fungovalo někde a jinde ne, bez zjevného principu, proč tomu tak je. Přiřazení do proměnné? V pořádku. Vložení téže funkce do pole struktury? Chyba kompilátoru, napište prosím double[int], jako by byl rok 2022.

Go 1.27 zajišťuje, že odvození funguje v každém kontextu, kde je cílový typ jednoznačně znám:

 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}

Jedno double, instancované jako func(int) int na třech místech a func(float64) float64 na čtvrtém, zcela z kontextu. Pokud píšete konfigurační struktury nebo tabulky obslužných rutin plné hodnot funkcí, z vaší kódové základny právě zmizí spousta šumu typu [T].

2. Runtime: Výkon zdarma a detektor úniků

2.1. Alokace specializovaná na velikost

Kompilátor nyní emituje volání rutin pro alokaci specializovanou na velikost pro malé objekty. Poznámky k vydání uvádějí až 30% úsporu u alokací pod 80 bajtů a přibližně 1% celkově u programů náročných na alokace.

Nevěřím poznámkám k vydání bez měření, 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

O 30–35 % rychlejší v mikrobenchmarku, který nedělá nic jiného než alokuje na Apple Silicon. Jinými slovy: toto je nejlepší možný případ, dosažený v laboratorních podmínkách benchmarkem navrženým tak, aby čísla vypadala dobře. Ve skutečném programu GC a skutečná práce většinu z toho pohltí. Tvrzení o ~1 % je to poctivé.

Cenou je velikost binárního souboru. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Asi 50 KB, fixně, bez ohledu na váš program. Můžete se odhlásit pomocí GOEXPERIMENT=nosizespecializedmalloc, ale tato úniková cesta je naplánována k odstranění v Go 1.28, takže ji berte jako workaround pro nahlášení chyby, nikoli jako životní styl.

2.2. Profil úniků Goroutine je nyní skutečný

Experimentální v Go 1.26, obecně dostupné v 1.27. GOEXPERIMENT goroutineleakprofile je pryč; prostě to funguje.

 1func leak() {
 2	ch := make(chan int) // nikdo nikdy nepošle. nikdo.
 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

Tři gorutiny, trvale zaseknuté, se souborem a číslem řádku ukazujícím přesně na místo, kde jste to udělali.

Trik za tím je skutečně chytrý: znovu využívá analýzu dosažitelnosti garbage collectoru. Pokud je gorutina G zablokována na primitivu P a P je nedosažitelné z jakékoli spustitelné gorutiny (nebo čehokoli, co by tyto gorutiny mohly probudit), pak se P již nikdy nikdo nedotkne, takže se G nikdy neprobudí. To není heuristika — to je důkaz.

Stejný design vám dává omezení zdarma: pokud je kanál nebo mutex dosažitelný přes globální proměnnou nebo přes lokální proměnnou nějaké gorutiny, která stále běží, GC jej stále vidí, takže runtime nemůže dojít k žádnému závěru. Všimněte si, že i výše uvedený příklad vyžaduje dvě volání runtime.GC() pro nahlášení. Nezachytí vše.

Zachytí však klasický „zapomněl jsem zrušit kontext, worker gorutina žije věčně“ — což je většina úniků po většinu času.

Pokud importujete net/http/pprof, je to také na /debug/pprof/goroutineleak. Připojte to do stagingu, kontrolujte to týdně a buďte potichu zděšeni.

Tuto práci přispěl Vlad Saioc z Uberu, který si zaslouží nápoj dle vlastního výběru.

2.3. Tracebacky vám nyní řeknou, který požadavek zemřel

Pro moduly deklarující Go 1.27 nebo novější obsahují řádky záhlaví tracebacku nyní popisky gorutin 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: popisky jsou děděny podřízenými gorutinami. Gorutina 3 nikdy nenastavila popisek. Získala jej od svého rodiče.

Takže až příště produkce uvízne v deadlocku a někdo pošle procesu SIGQUIT, místo 4 000 gorutin, které vypadají identicky, získáte 4 000 gorutin označených tím, ke kterému požadavku a ke kterému tenantovi patří. Tři řádky pprof.Do ve vašem vstupním bodě požadavku vám to zajistí.

Protože popisky mohou obsahovat věci, které byste raději nevypsali na stderr, GODEBUG=tracebacklabels=0 je vypne a toto odhlášení má explicitně zůstat navždy.

2.4. asynctimerchan je nadobro pryč

Go 1.23 udělalo kanály časovačů nebufferovanými (synchronními) a nabídlo asynctimerchan=1 jako cestu zpět ke starému chování. V 1.27 je toto nastavení trvale odstraněno. Kanály balíčku time jsou synchronní, tečka, žádné vyjednávání.

Zajímavou částí je politika zavedená spolu s ním. Odstraněný GODEBUG ponechaný ve vašem go.mod automaticky nerozbije build — rozbije se pouze v případě, že je nastaven na starou 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/godebug#go-127)
4
5# go.mod obsahuje: godebug asynctimerchan=0
6(builds fine)

Což znamená, že jediní lidé, na které se bude křičet, jsou ti, kteří se skutečně spoléhali na odstraněné chování. Všichni, kdo jej nastavili na finální výchozí hodnotu a zapomněli na to, na to mohou nadále nemyslet. To je promyšlený kus API archeologie.

3. Standardní knihovna

3.1. encoding/json/v2: Nahradili engine za letu

Tohle je ta velká věc. Více než dva roky diskuzí o návrhu konečně přinesly ovoce.

Nyní existují tři balíčky a pochopení tohoto rozdělení je polovina úspěchu:

BalíčekÚloha
encoding/jsonAPI v1, které znáte. Chování 100% nezměněno. Nyní implementováno nad v2
encoding/json/v2Sémantické zpracování. Hodnoty Go ↔ JSON
encoding/json/jsontextSyntaktické zpracování. JSON jako proud tokenů

Hlavní novinkou je, že encoding/json byl přebudován na zcela odlišné implementaci a chová se identicky. Zde je jak:

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ý v1 výstřelek byl zakódován jako volba, seskupen do DefaultOptionsV1() a API v1 vždy aplikuje tento balíček. Což znamená, že se to v 1.26 i 1.27 chová stejně:

1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)  // <nil>, map[a:2]

Ano, v1 stále potichu přijímá duplicitní klíče a bere ten poslední. Vždy to dělalo. Stále to dělá. Kompatibilita znamená kompatibilitu i s těmi špatnými částmi.

v2 má mezitím své 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í klíče a neplatné UTF-8 jsou odmítnuty. Obojí je skutečně nebezpečné, ne jen neúhledné — když se dva parsery neshodnou na tom, který duplicitní klíč vyhrává, dostanete bezpečnostní chyby. CVE-2017-12635 v CouchDB bylo přesně toto: JSON tělo se dvěma klíči roles, kde validátor přečetl jeden a úložná vrstva druhý. Odmítnutí je správná cesta.

Volby jsou 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// seřazené klíče mapy + odsazení
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// odmítnout neznámá pole — dříve vyžadovalo 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ětšina věcí, které jste dříve řešili knihovnou třetí strany nebo ručně psaným MarshalJSON, jsou nyní příznaky.

A příběh migrace je tou nejlepší částí. Poslední volby vyhrávají, takže můžete přijmout přísnost v2 po jednom chování najednou:

1// zachovat sémantiku v1, ale odmítnout duplicitní klíče jako v2
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Nemusíte portovat 200tisícovou kódovou základnu na v2, abyste přestali přijímat duplicitní klíče. Přepnete jednu volbu. Pro cokoli velkého je to realistická cesta.

Výkon: marshalling je zhruba na stejné úrovni, unmarshalling je výrazně rychlejší. Pokud se něco pokazí, GOEXPERIMENT=nojsonv2 obnoví starou implementaci — a i toto odhlášení je nakonec na odpis, takže raději podejte issue, než se s tím smíříte.

jsontext je nízkoúrovňová vrstva: Encoder/Decoder procházející JSON jako Tokeny a Valuey se stavovým automatem, který vás drží v mezích. Sáhněte po něm, když píšete streamovací transformátor nebo JSON filtr a nechcete vůbec materializovat hodnoty Go.

3.2. Standardní balíček uuid

Konečně. RFC 9562 ve standardní knihovně, bez nutnosti go get.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: horních 48 bitů je časové razítko, takže se vždy řadí v pořadí vytvoření
 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še. Můžete si přečíst celou dokumentaci balíčku, zatímco vám chladne káva.

Detaily, které stojí za to znát:

  • UUID je [16]byte, takže == funguje a je přímo použitelný jako klíč mapy. Stejný design jako google/uuid.
  • Nil a Max jsou funkce, nikoli proměnné. Protože var Nil UUID na úrovni balíčku je nabitá zbraň namířená na vaši nohu a někdo, někde, by ji nakonec přiřadil.
  • Náhodné bity pocházejí z kryptograficky bezpečného generátoru.
  • Implementuje encoding.TextMarshaler/TextUnmarshaler/TextAppender, takže zapadne přímo do JSON struktur.
  • NewV7 je to, co chcete pro primární klíče databáze. Časové řazení znamená, že váš B-tree index přestane fragmentovat, v čemž jsou UUID v4 notoricky špatné.

Nyní můžete smazat závislost. Pokud potřebujete v1/v3/v5 nebo luxusnější možnosti parsování, knihovny třetích stran stále mají své místo.

3.3. crypto/mldsa: Post-kvantové podpisy, a jsou obrovské

Go 1.24 nám dalo crypto/mlkem pro post-kvantovou výměnu klíčů. Go 1.27 přináší druhou polovinu: podpisy ML-DSA, standardizované jako 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

Podívejte se na ta čísla. Jeden podpis má 3 309 bajtů. Ed25519 má 64. To je 50násobný nárůst a veřejný klíč je navíc téměř 2 KB. Nacpěte pár takových do certifikačního řetězce a váš TLS handshake začne potřebovat vlastní strategii MTU.

Toto je skutečná cena kvantové odolnosti dnes a je to důvod, proč nikdo zítra všechno nepřepne. Ale je to nyní ve standardní knihovně, což je místo, kde chcete, aby to bylo předtím, než to budete potřebovat.

Options.Context je doménová separace: podepisujte stejným klíčem pro různé účely, pro každý použijte jiný kontext a podpis z jednoho kontextu nebude v jiném verifikován. Výše uvedený příklad ukazuje přesně to — stejný klíč, stejná zpráva, jiný kontext, odmítnuto.

PrivateKey implementuje crypto.Signer, takže zapadá do existujících rozhraní. crypto/x509 zpracovává klíče a podpisy ML-DSA a crypto/tls podporuje schémata podpisů MLDSA44/MLDSA65/MLDSA87 v TLS 1.3.

Existuje také SignDeterministic, který přeskakuje náhodnost — šikovné pro testy a reprodukovatelné buildy.

3.4. simd: Vektorové instrukce bez assembleru

Go 1.26 představilo architekturově specifické simd/archsimd jako experiment. Go 1.27 přidává simdpřenositelné a agnostické k šířce vektoru. Povolte pomocí 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	// zpracovat zbytek pomocí částečného načtení/uložení — žádná skalární vyčišťovací smyč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]

Designová volba, na které záleží: šířka vektoru není nikdy pevně zakódována. Ptáte se va.Len() za běhu. VectorBitSize() vám řekne skutečnou šířku, Emulated() vám řekne, zda jste získali skutečný hardware nebo zdvořilou softwarovou imitaci. Můj Mac nahlásil 128-bit NEON; stroj s AVX-512 nahlásí více; stroj bez ničeho nahlásí emulaci a kód stále běží.

LoadFloat32sPart/StorePart si zaslouží zvláštní zmínku. Nejotravnější částí ručně psaného SIMD je vždy ten roztřepený konec pole a toto to zvládá bez samostatné skalární smyčky.

Stále experimentální, API stále nestabilní, nedávejte to do produkce. Ale fakt, že je to kód Go a nikoli assembler nebo cgo, je skutečně velká věc.

3.5. hash/maphash.Hasher

Nové rozhraní popisující kontrakt mezi typem hodnoty a kontejnery založenými na hashování:

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

Proč? Vestavěná mapa Go přijímá pouze comparable klíče. Nemůžete klíčovat na slice. Nemůžete definovat „rovno bez ohledu na velikost písmen“. Hasher řeší obojí najednou:

 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)   // stejný seed, jinak to všechno nemá smysl
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

Pro běžnou sémantiku == existuje ComparableHasher[T]:

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

Háček: Hasher je rozhraní, nikoli datová struktura. Zatím neexistuje žádná standardní hash tabulka nebo Bloomův filtr, který by jej konzumoval. Toto je základ pro budoucí balíček kontejnerů. Dnes byste jej použili při vytváření vlastní struktury nebo při konzumaci existující implementace, jako je go/types.Hasher (který vám umožní použít types.Type jako klíč mapy, respektující Identical).

Správa seedu je na vás. Pokud by hashOf výše vytvořilo nový seed při každém volání, identické hodnoty by se hashovaly odlišně a příklad by vypsal false — což je přesně ta chyba, kterou jsem udělal při prvním pokusu. Jeden seed na kontejner. (Seed je randomizován, aby odolal DoS útokům typu hash-flooding, což je důvod, proč to není jen konstanta.)

3.6. httptest.NewTestServer + synctest.Sleep: Hit, který uspí

Pokud si z tohoto vydání osvojíte jen jednu věc, ať je to tato.

testing/synctest absolvoval v Go 1.25 a dal nám falešné hodiny pro testování souběžného kódu. Kromě momentu, kdy se váš test dotkl skutečné sítě, iluze se zhroutila. httptest.NewTestServer v Go 1.27 používá in-memory falešnou síť, takže bublina zůstává neporušená. A synctest.Sleep (= time.Sleep + synctest.Wait) to doplňuje.

Zde je test retry-s-exponenciálním-backoffem:

 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 měl být přesně 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)

Přečtěte si to dvakrát. Simulovalo to tři sekundy backoffu proti skutečnému HTTP serveru a skončilo to za 0,00 sekund. A tvrzení je elapsed == 3*time.Secondpřesně tři sekundy, ne „alespoň tři sekundy, plus mínus jitter plánovače“. Falešné hodiny nejitří.

synctest.Sleep existuje z konkrétního důvodu: pokud váš test spí po stejnou dobu jako kód pod testem, je jen hádankou, který se probudí jako první. synctest.Sleep spí a poté čeká, dokud není každá další gorutina v bublině trvale zablokována, takže systém pozorujete poté, co se usadil.

Timeouty, retries, jističe (circuit breakers), omezovače rychlosti — každý test HTTP klienta závislý na čase, který vlastníte, může být rychlý a deterministický. Pokud je vaše testovací sada v současnosti držena pohromadě pomocí time.Sleep(100 * time.Millisecond) a naděje, toto je váš východ.

3.7. Změny v net/http, které skutečně ovlivňují produkci

Tiché, ale objeví se ve vašich metrikách.

HTTP/1 těla odpovědí se automaticky vypouštějí při Close. Nepřečtený obsah je nyní vypuštěn (až do konzervativního limitu), když zavřete tělo, takže spojení může být znovu použito. Což znamená, že konečně můžete smazat toto zaříkávadlo, které všichni kopírují ze stejné odpovědi na Stack Overflow od roku 2016:

1// již není nutné
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

Pro většinu programů je to no-op nebo malá výhra. Pokud to věci zhoršuje, pravděpodobně jste v kategorii, kterou poznámky k vydání zdvořile popisují: Transport.MaxIdleConns nastaveno na 0, nebo nový Client pro každý požadavek, čímž se zcela obchází limit nečinných spojení. Transport.DisableKeepAlives = true to zakryje, ale skutečnou radou poznámek k vydání je, že „hlubší pohled by byl pravděpodobně prospěšný“, což je týmový jazyk Go pro máte větší problémy.

Priorita klienta HTTP/2 (RFC 9218). Server nyní ctí signály priority klienta. Pokud jste preferovali staré plánování round-robin, Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Omezuje, kolik hodnot hlaviček server přijme, s výchozí hodnotou DefaultMaxHeaderValueCount. Další dveře zavřené před „pošli deset tisíc hlaviček a sleduj, co se stane“.

ALPN u uživatelem poskytnutých spojení. Pokud váš net.Conn implementuje ConnectionState() tls.ConnectionState, Transport a Server na něm provedou TLS ALPN vyjednávání — takže vlastní dialery procházející proxy mohou stále vyjednat HTTP/2.

3.8. Drobnosti, které vyvolávají radost

strings.CutLast / bytes.CutLast. Cut dělí na prvním oddělovači. Žádná varianta „poslední“ neexistovala, takže každý ručně vyráběl LastIndex plus slicing a zhruba 30 % z nás se při prvním pokusu o jedno seklo.

1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true

Skvělé pro přípony souborů a pro parsování host:port — kde musíte najít poslední dvojtečku, protože adresy IPv6 jsou jimi plné.

math/big.Int.Divide. Dělení s explicitním režimem zaokrouhlování, nahrazující hod mincí „chci Quo nebo 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

Pokud pracujete v doméně, kde je pravidlo zaokrouhlování napsáno v předpisu, je to pro vás.

url.URL.Clone a url.Values.Clone. Hluboké kopie. Stará mělká kopie *u sdílela ukazatel Userinfo, což produkovalo přesně ten typ chyby, jejíž nalezení trvá den a oprava pět sekund.

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 metody čtení nyní vrací io.EOF přímo místo zabalení do net.OpError. err == io.EOF nyní funguje, jak vždy mělo.

database/sql.ConvertAssign a driver.RowsColumnScanner. Funkce pro autory ovladačů. První odhaluje konverze typů, které provádí Rows.Scan; druhá umožňuje ovladačům skenovat přímo do uživatelských cílů, přeskakujíc mezilehlou alokaci.

unicode 15 → 17. Dvě verze v jednom skoku. Klasifikace řetězců a chování normalizace se mohou jemně posunout. Pokud máte testy, které na tom závisí, zjistíte to.

compress/flate je rychlejší — a jeho výstupní bajty se mohou lišit od Go 1.26. To kaskáduje do archive/zip, compress/gzip, compress/zlib a image/png. Pokud máte testy zlatých souborů, které hashují komprimovaný výstup, rozbijí se a nebude to regrese. Zkontrolujte to před upgradem, ne během incidentu.

4. Toolchain

4.1. go fix získal více modernizátorů

go fix se potichu mění v nástroj pro modernizaci kódu. Go 1.27 přidává atomictypes, embedlit, slicesbackward a unsafefuncs. Použijte -diff pro náhled:

 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 }

Čtyři modernizátory spuštěny na jednom malém souboru:

  • embedlit — přepisuje vnořené literály pomocí nové syntaxe z §1.2
  • atomictypesatomic.AddInt64(&x, 1) se stane atomic.Int64.Add(1), což znemožňuje neatomický přístup a potichu opravuje chyby 32bitového zarovnání, o kterých jste nevěděli
  • slicesbackward — zpětné smyčky se stanou slices.Backward
  • waitgroupgo — rituál Add/go/Done se stane wg.Go (přejmenováno z waitgroup z 1.26, aby se předešlo nejednoznačnosti)

go tool fix help vypisuje všech 26; go tool fix help <name> vysvětluje jeden. Také fmtappendf bylo odstraněno „kvůli stylistickým obavám“, což je krásně diplomatický způsob, jak popsat cokoli, co se stalo v tom vlákně issue.

Spuštění go fix ./... na staré kódové základně je skutečně uspokojující odpoledne. Samozřejmě si nejdřív přečtěte diff.

4.2. go test spouští stdversion ve výchozím nastavení

Toto je změna, která vám nejpravděpodobněji přeruší den.

go test nyní ve výchozím nastavení spouští kontrolu vet stdversion, označující symboly standardní knihovny novější, než dovoluje direktiva go ve vašem go.mod:

1// go.mod říká: 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]

To zabíjí klasický režim selhání, kdy vše funguje na vašem stroji (nejnovější toolchain) a exploduje v CI nebo ve starším prostředí uživatele. Pokud publikujete knihovny s konzervativní direktivou go, toto je dar.

4.3. go test -json získal OutputType

Řádky "Action":"output" nyní nesou volitelné pole "OutputType": "error", "error-continue" nebo "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 (žádné pole), výstup t.Error (error) a řádky generované frameworkem (frame) jsou nyní rozlišitelné. Každý, kdo napsal regex, aby zjistil, které řádky testového výstupu jsou „skutečné“, jej nyní může smazat.

4.4. Vylepšení go doc

Syntaxe package@version. Přečtěte si dokumentaci pro konkrétní verzi, aniž byste ji přidali do svého modulu:

1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...

Šikovné pro kontrolu, co se změnilo před upgradem.

-ex a tisk zdrojového příkladu:

 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 příkladu a očekávaný výstup, v terminálu, aniž byste otevírali kartu prohlížeče, která bude otevřená ještě příští čtvrtek.

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

Pro moduly na go 1.27 nebo novější, go mod tidy sloučí roztroušené bloky require do maximálně dvou (přímé a nepřímé), zachovávajíc připojené komentáře.

 1// Před
 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šimněte si, že komentář // networking přežil. Toto většinou čistí po řešení konfliktů při Git merge, což je místo, kde se rodí zbloudilé bloky require. Pokud má váš tým více než tři lidi přidávající závislosti, vaše konflikty v go.mod budou znatelně tišší.

4.6. Různé

  • Podpora bzr odstraněna. Pokud se vás to týká, opravdu bych chtěl slyšet ten příběh.
  • go tool trace -http se váže na localhost, když dostane pouze port. -http=:6060 již neposlouchá na každém rozhraní. Použijte -http=0.0.0.0:6060, pokud jste to mysleli vážně. Toto nyní odpovídá go tool pprof a zabraňuje občasnému náhodnému veřejnému profileru.
  • Soubory odpovědí (@file) jsou podporovány compile, link, asm, cgo, cover a pack ve formátu kompatibilním s GCC. Pro build systémy, které překračují limity délky příkazového řádku — ahoj, Bazele.

5. Kompilátor, Linker, Porty

Kompilátor. Relativní názvy souborů v direktivách //line se nyní řeší vůči adresáři obsahujícího souboru, což odpovídá go/scanner. Relevantní, pokud píšete generátory kódu.

Názvy symbolů literálů funkcí (uzávěrů) jsou nyní také jednodušší — stejný název bez ohledu na inlining a více instancí stejného literálu může sdílet kód v binárním souboru. Žádná funkční změna, kromě: kód, který porovnává identitu funkcí přes reflect.Value.Pointer, uvidí „rovnost“ častěji než dříve. Toto porovnání nikdy nebylo platné, ale pokud jej máte, nyní je čas, kdy vám začne lhát hlasitěji.

Linker. Nové volby -macos a -macsdk nastavují verze OS a SDK v macOS příkazu pro načtení LC_BUILD_VERSION.

Porty.

  • Darwin nyní vyžaduje macOS 13 Ventura nebo novější, jak bylo oznámeno v 1.26. Zkontrolujte své CI běžce.
  • PowerPC (GOOS=linux GOARCH=ppc64) přepnulo na ELFv2 ABI. Vyžaduje Linux kernel 3.13+ (RHEL7 zpětně portováno na 3.10). Cgo, PIE a externí linkování jsou nyní podporovány. Pokud používáte cgo, ale potřebujete statickou binárku čistého Go, nastavte CGO_ENABLED=0.

6. Kontrolní seznam upgradu

Go 1.27 bere kompatibilitu vážně, ale před navýšením verze zkontrolujte tyto:

  • Testy zlatých souborů na komprimovaném výstupu. compress/flate změnil enkodéry; bajty gzip/zip/png se mohou lišit.
  • Testy porovnávající řetězce chybových zpráv JSON. Chování je identické; text chyby nikoli.
  • Odstraněné GODEBUGy v go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — tyto selžou při buildu pouze pokud jsou nastaveny na své staré hodnoty.
  • Narušení stdversion. go test je nyní zachytí. Spusťte jej brzy, pokud udržujete knihovnu s konzervativní direktivou go.
  • CI běžci s macOS 12 nebo starším. Podpora je pryč.
  • Testy assertující názvy symbolů literálů funkcí a porovnávání funkcí reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 nebo Client pro každý požadavek. V kombinaci s automatickým vypouštěním těl odpovědí to může být pomalejší.

Závěrečné myšlenky

Go 1.27 je vydání, kde se najednou sešla spousta odložené údržby.

Generické metody byly chybějícím dílkem práce na generikách z 1.18. Selektory polí ve strukturových literálech uzavřely jedenáct let starý problém. encoding/json/v2 byly dva roky diskuzí o návrhu. A uuid vyplnilo díru, kterou v podstatě každý projekt v Go záplatoval stejnou závislostí třetí strany po celou dekádu.

Pokud chcete prioritní pořadí, jak z toho skutečně získat hodnotu:

  1. httptest.NewTestServer + synctest.Sleep — HTTP testy závislé na čase se stávají rychlými a deterministickými. Nejlepší poměr úsilí a přínosu v celém vydání.
  2. Profil úniků gorutin — vystavte jeden endpoint ve stagingu a připravte se na pokoru.
  3. Popisky gorutin v tracebacku — tři řádky pprof.Do ve vašem vstupním bodě požadavku změní váš příští stack dump ve 3 ráno v něco čitelného.
  4. go fix ./... — nechte nástroj provést modernizaci.
  5. encoding/json/v2 — žádný spěch. Přijměte ji po jedné volbě.

Vezměte go1.27rc3 a spusťte proti němu svou testovací sadu. Všechno na tom kontrolním seznamu je výrazně příjemnější objevit v úterý odpoledne než během incidentu.

Reference