GoSuda

Go 1.27, aneb Jak jsem se naučil přestat se strachovat a zamilovat si generické metody

By iwanhae views ...

Přehled Go 1.27

Go 1.27 vyjde v srpnu 2026 a tentokrát se změnil samotný jazyk. Nejde o to, že jsme do slices přidali novou funkci, ale o skutečnou změnu. Generické metody jsou zavedeny. Jeden jedenáctiletý issue byl uzavřen. encoding/json byl tiše nahrazen novým enginem, zatímco letadlo bylo ve vzduchu.

Každý příklad níže byl spuštěn na go1.27rc3 (darwin/arm64). Každý výstupní blok je skutečný výstup, zkopírovaný včetně chybových hlášení. Pokud se něco zdá podivné, je to proto, že to kompilátor skutečně vypsal.

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

1. Jazyk se změnil (ano, opravdu)

1.1. Generické metody

Od Go 1.18 máme generika a od Go 1.18 máme také Rozhovor. Vypadá takto:

„Jen napíšu metodu Map na svůj slice typ…“

method must have no type parameters

„…dobře. Tak to bude funkce na úrovni balíčku.“

Metody mohly používat pouze typové parametry deklarované v receiveru. Vlastní metoda nemohla zavádět nové. Každá generická transformace tedy skončila v package scope, kde ležela vedle dalších sedmnácti volně plovoucích pomocných funkcí s nějakou variantou názvu MapSlice.

Go 1.27 to 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}

Receiver ani nemusí být 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) }

Než ale dnes odpoledne začnete refaktorovat celou kódovou základnu, existují dvě omezení a jsou důležitější, než se na první pohled zdá:

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

Právě to druhé vás nejspíš potrápí:

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á dispatch se tedy nekombinují. Nejde o skoupost týmu Go: generická metoda má nekonečně mnoho instancí, zatímco tabulka metod rozhraní musí být konečná a známá v době kompilace. Nekonečnou věc nelze uložit do konečné tabulky. Vesmír řekl ne.

Praktické shrnutí: generické metody jsou určeny pro konkrétní typy s API ve tvaru užitných funkcí. Cokoli, co se musí skrývat za rozhraním, stále potřebuje starý přístup.

Standardní knihovna toho již využila. Rand z math/rand/v2 dostal 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 funkce na úrovni balíčku rand.N, která používala globální zdroj. Nyní má vlastní nastavený *Rand stejné pohodlí. Malá věc. Hezká věc.

1.2. Selektory polí ve struct literálech, neboli: issue #9859 konečně doputoval do cíle

Embedding vám dá u.ID. Embedding vám ale nedá User{ID: 1}. Místo toho dostanete User{Base: Base{ID: 1}}, což je způsob, jakým se Go ptá, zda jste to opravdu mysleli.

Issue #9859 bylo založeno v roce 2015. Nyní je uzavřeno. Někde gopher, který mezitím dvakrát změnil kariéru, právě dostává notifikaci 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žijte povýšené pole přímo
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name není polem Line. Je polem vloženého Object. Dříve jste museli psát Line{Object: Object{name: "diagonal"}, ...}.

Nyní drobné podrobnosti, které jsem ověřil tím, že jsem se skutečně pokusil zkompilovat všechno.

(a) Klíčem je stále prostý identifikátor. Nelze zapsat 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á, že „klíče nyní fungují jako přístup k polím“. Znamená konkrétně, že jsou povoleny implicitně povýšené názvy polí. Je to o něco užší, než naznačuje titulek.

(b) Nelze současně určit vložené pole a 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 kompilátoru dvě protichůdné věci o stejné paměti a on odmítl hádat.

(c) Vkládání ukazatelů není vítá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 sledovat ukazatel, potřebujete ukazatel, který můžete sledovat, a v okamžiku konstrukce literálu ještě žádný neexistuje. I to je férové.

Dobrá zpráva: go fix vaše staré literály přepíše za vás. K tomu se dostaneme později.

1.3. Odvozování typů funkcí je méně libovolné

Odvozování typů pro generické funkce fungovalo dříve na některých místech a na jiných ne, bez jakéhokoli rozpoznatelného principu, podle něhož se to rozhodovalo. Přiřazení do proměnné? V pořádku. Vložení stejné funkce do pole struktury? Chyba kompilátoru, prosím napište double[int], jako bychom psali rok 2022.

Go 1.27 zajišťuje, že odvozování 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}

Jedna funkce double se instancuje jako func(int) int na třech místech a jako func(float64) float64 na čtvrtém, a to čistě z kontextu. Pokud píšete option struktury nebo handler tabulky plné funkčních hodnot, spousta šumu v podobě [T] z vaší kódové základny brzy zmizí.

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

2.1. Alokace specializovaná podle velikosti

Kompilátor nyní pro malé objekty generuje volání alokačních rutin specializovaných podle velikosti. Poznámky k vydání slibují až 30% úsporu u alokací menších než 80 bajtů a zhruba 1% celkově u programů náročných na alokace.

Poznámkám k vydání nevěřím 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# výchozí (alokace specializovaná podle velikosti zapnuta)
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

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

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

12413202 bytes  (výchozí)
22361826 bytes  (nosizespecializedmalloc)

Přibližně 50 KB navíc, pevně, bez ohledu na váš program. Můžete se odhlásit pomocí GOEXPERIMENT=nosizespecializedmalloc, ale tento únikový východ je naplánován k odstranění v Go 1.28, takže ho berte jako obejití pro hlášení chyb, a ne jako životní styl.

2.2. Profil úniku goroutin je nyní skutečný

V Go 1.26 experimentální, v 1.27 obecně dostupný. GOEXPERIMENT goroutineleakprofile zmizel; prostě 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 goroutiny natrvalo uvízlé, se souborem a číslem řádku, které přesně ukazují, kde jste to udělali.

Trik za tím je opravdu chytrý: znovu využívá analýzu dosažitelnosti garbage collectoru. Pokud je goroutina G blokována na primitivu P a P není dosažitelný z žádné spustitelné goroutiny (ani z ničeho, co by tyto goroutiny mohly probudit), pak se k P už nikdo nemůže dostat, a G se tedy nikdy neprobudí. Nejde o heuristiku, ale o důkaz.

Stejný návrh vám zároveň přináší jeho omezení zadarmo: pokud je kanál nebo mutex dosažitelný přes globální proměnnou nebo přes lokální proměnnou nějaké stále běžící goroutiny, GC je stále vidí, a runtime tedy nemůže nic vyvodit. Všimněte si, že i tento hračkový příklad potřebuje dvě volání runtime.GC(), aby se něco nahlásilo. Nezachytí všechno.

Zachytí však klasické „zapomněl jsem zrušit context, worker goroutina žije věčně“, což je většina úniků ve většině případů.

Pokud importujete net/http/pprof, je profil dostupný také na /debug/pprof/goroutineleak. Zapojte to do stagingu, kontrolujte to každý týden a tiše se děste.

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

2.3. Tracebacky nyní říkají, který požadavek selhal

U modulů deklarujících Go 1.27 nebo novější obsahují řádky hlaviček tracebacku nyní štítky goroutin 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: štítky dědí podřízené goroutiny. Goroutina 3 nikdy žádný štítek nenastavila. Dostala ho od svého rodiče.

Až tedy příště v produkci dojde k deadlocku a někdo pošle procesu SIGQUIT, místo 4 000 goroutin, které vypadají úplně stejně, dostanete 4 000 goroutin označených tím, ke kterému požadavku a kterému tenantovi patří. Tři řádky pprof.Do na vstupním bodu vašich požadavků vám to zajistí.

Protože štítky mohou obsahovat věci, které nechcete vypisovat do stderr, GODEBUG=tracebacklabels=0 tuto funkci vypne a tato možnost odhlášení je výslovně zamýšlena jako trvale dostupná.

2.4. asynctimerchan je navždy pryč

Go 1.23 nastavilo kanály časovačů jako nebufferované (synchronní) 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í, bez výjimky a bez vyjednávání.

Zajímavá je politika, která byla zavedena spolu s tím. Odstraněný GODEBUG ponechaný ve vašem go.mod automaticky nerozbije build. Rozbije ho pouze tehdy, když 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/doc/godebug#go-127)
4
5# go.mod obsahuje: godebug asynctimerchan=0
6(builds fine)

To znamená, že na vás budou křičet pouze lidé, kteří se na odstraněné chování skutečně spoléhali. Všichni, kdo nastavili výslednou výchozí hodnotu a zapomněli na to, si mohou dál nemyslet na to. Je to promyšlený kus archeologie API.

3. Standardní knihovna

3.1. encoding/json/v2: motor byl vyměněn za jízdy

Toto je velká věc. Více než dva roky diskuse o návrhu konečně dospěly k výsledku.

Nyní existují tři balíčky a pochopení jejich rozdělení je většinou úspěchu:

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

Hlavní zpráva je, že encoding/json byl přestavěn na zcela jiné implementaci a chová se identicky. Jak to funguje:

1// encoding/json.Unmarshal v Go 1.27, zkráceně
2func Unmarshal(data []byte, v any) error {
3	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}

Každá legacy zvláštnost v1 byla zakódována jako option, sbalena do DefaultOptionsV1() a API v1 tento balík vždy použije. Díky tomu se toto chová stejně v 1.26 i 1.27:

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

Ano, v1 stále tiše přijímá duplicitní klíče a použije poslední z nich. Vždy to takto dělal. Dělá to i nadále. Kompatibilita znamená kompatibilitu i se špatnými částmi.

v2 mezitím 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é, nejen neuspořádané: když se dva parsery neshodnou na tom, který duplicitní klíč vyhrává, vznikají bezpečnostní chyby. Právě takový byl CVE-2017-12635 v CouchDB: JSON tělo se dvěma klíči roles, přičemž validátor přečetl jeden a úložná vrstva druhý. Odmítnutí je správné rozhodnutí.

Options 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 externí knihovnou nebo ručně psaným MarshalJSON, je nyní přepínačů.

Nejlepší část je ale příběh migrace. Pozdější options mají přednost, takže můžete přijímat přísnost v2 po jednom chování:

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

Nemusíte portovat kódovou základnu o 200 tisících řádcích na v2, abyste přestali přijímat duplicitní klíče. Přepnete jednu option. U čehokoli velkého je to realistická cesta.

Výkon: marshalling je zhruba na úrovni původního, unmarshalling je výrazně rychlejší. Pokud se něco pokazí, GOEXPERIMENT=nojsonv2 obnoví starou implementaci. I tato možnost odhlášení bude časem odstraněna, takže raději založte issue, než abyste se v ní usadili.

jsontext je nízkoúrovňová vrstva: Encoder/Decoder, které procházejí JSON jako Token a Value se stavovým automatem, který vás udržuje v poctivosti. 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á značka, takže tyto hodnoty se vždy řadí podle času 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 tvoří New, NewV4, NewV7, Nil, Max, Parse, MustParse a jeden typ: type UUID [16]byte. To je vše. Celou dokumentaci balíčku si můžete přečíst, zatímco vám vychladne káva.

Podrobnosti, které stojí za zmínku:

  • UUID je [16]byte, takže == funguje a lze jej přímo použít jako klíč mapy. Stejný návrh jako google/uuid.
  • Nil a Max jsou funkce, nikoli proměnné. Protože package-level var Nil UUID je nabitá zbraň namířená na vaši nohu a někdo by do ní nakonec něco přiřadil.
  • Náhodné bity pocházejí z kryptograficky bezpečného generátoru.
  • Implementuje encoding.TextMarshaler/TextUnmarshaler/TextAppender, takže se bez problémů vejde do JSON struktur.
  • NewV7 je ten, který chcete pro primární klíče databáze. Časové uspořádání znamená, že se váš B-strom index přestane fragmentovat, což je u UUID v4 známý problém.

Nyní můžete odebrat jednu závislost. Pokud potřebujete v1/v3/v5 nebo pokročilejší možnosti parsování, externí knihovny mají stále své místo.

3.3. crypto/mldsa: postkvantové podpisy, a jsou obrovské

Go 1.24 nám dalo crypto/mlkem pro postkvantovou 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. Jediný podpis má 3 309 bajtů. Ed25519 má 64. Je to padesátinásobný nárůst a veřejný klíč má k tomu téměř 2 KB. Vložte několik takových podpisů do řetězce certifikátů a váš TLS handshake začne potřebovat vlastní strategii MTU.

To je skutečná cena kvantové odolnosti dnes a proto zatím nikdo nepřechází na všechno hned zítra. Ale je to již ve standardní knihovně, což je přesně to, co chcete, dříve než to budete potřebovat.

Options.Context slouží k oddělení domén: podepisujte stejným klíčem pro různé účely, pro každý použijte jiný kontext, a podpis z jednoho kontextu neprojde ověřením v jiném. Příklad výše to přesně ukazuje: stejný klíč, stejná zpráva, jiný kontext, odmítnuto.

PrivateKey implementuje crypto.Signer, takže zapadne 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, což se hodí pro testy a reprodukovatelné buildy.

3.4. simd: vektorové instrukce bez assembleru

Go 1.26 zavedlo experimentálně architekturně specifický simd/archsimd. Go 1.27 přidává simd, které je přenositelné a nezávislé na šířce vektoru. Zapíná se 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í — bez skalární úklidové smyčky
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]

Rozhodnutí o návrhu, na kterém záleží: šířka vektoru není nikdy pevně zakódována. Zavoláte va.Len() za běhu. VectorBitSize() vám řekne skutečnou šířku, Emulated() vám řekne, zda máte skutečný hardware, nebo zdvořilou softwarovou imitaci. Můj Mac hlásil 128bitové NEON; stroj s AVX-512 hlásí více; stroj bez podpory hlásí emulaci a kód přesto běží.

Zvláštní zmínku zaslouží LoadFloat32sPart/StorePart. Nejotravnější část ručně psaného SIMD je vždy nepravidelný konec pole a toto ho zvládne bez samostatné skalární smyčky.

Stále experimentální, API stále nestabilní, nenasazujte do produkce. Ale skutečnost, že jde o kód Go, a ne o assembler nebo cgo, je opravdu velká věc.

3.5. hash/maphash.Hasher

Nové rozhraní popisující kontrakt mezi hodnotovým typem 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 v Go přijímá pouze klíče comparable. Nemůžete použít slice jako klíč. Nemůžete definovat „rovnost 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 celé toto nedává 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 standardní hash tabulka ani Bloomův filtr, který by jej používal. Jde o základ pro budoucí balíček kontejnerů. Dnes byste jej použili při stavbě vlastní struktury, nebo byste využili existující implementaci, například go/types.Hasher (která umožňuje použít types.Type jako klíč mapy s respektováním Identical).

Správa seedu je na vás. Kdyby hashOf výše vytvářela při každém volání nový seed, stejné hodnoty by se hashovaly odlišně a příklad by vypsal false. To je přesně chyba, kterou jsem udělal při prvním pokusu. Jeden seed na jeden kontejner. (Seed je náhodný, aby se zabránilo útokům DoS záplavou hashů, a proto to není pouhá konstanta.)

3.6. httptest.NewTestServer + synctest.Sleep: úder pro spáče

Pokud z tohoto vydání přijmete jen jednu věc, ať je to tato.

testing/synctest vyšlo z experimentu v Go 1.25 a dalo nám falešné hodiny pro testování souběžného kódu. Jakmile však váš test sáhl na skutečnou síť, iluze se zhroutila. httptest.NewTestServer v Go 1.27 používá paměťovou falešnou síť, takže bublina zůstává neporušená. A synctest.Sleep (= time.Sleep + synctest.Wait) to doplňuje.

Zde je test opakování 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 tři sekundy backoffu proti skutečnému HTTP serveru a skončilo za 0,00 sekundy. A tvrzení zní elapsed == 3*time.Second, tedy přesně tři sekundy, nikoli „alespoň tři sekundy, s ohledem na jitter plánovače“. Falešné hodiny žádný jitter nemají.

synctest.Sleep existuje z konkrétního důvodu: pokud váš test spí stejně dlouho jako testovaný kód, je to otázka štěstí, který se probudí první. synctest.Sleep spí a potom čeká, dokud každá další goroutina v bublině není trvale blokována, takže pozorujete systém poté, co se ustálil.

Timeouty, opakování, circuit breakery, rate limitery: každý časově závislý test HTTP klienta, který vlastníte, se může stát rychlým a deterministickým. Pokud vaše testovací sada dnes drží pohromadě díky time.Sleep(100 * time.Millisecond) a naději, toto je vaše cesta ven.

3.7. Změny net/http, které skutečně ovlivní produkci

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

Tělo HTTP/1 odpovědí se při Close automaticky vyprázdní. Nepřečtený obsah je nyní při zavření těla vyprázdněn (až do konzervativního limitu), takže spojení lze znovu použít. To znamená, že konečně můžete smazat tuto formulku, kterou si od roku 2016 všichni kopírují z jedné odpovědi na Stack Overflow:

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

Pro většinu programů jde o nic nedělající operaci nebo malý zisk. Pokud to věci zhorší, pravděpodobně jste v kategorii, kterou poznámky k vydání zdvořile popisují: Transport.MaxIdleConns nastaveno na 0, nebo nový Client na každý požadavek, což zcela obchází limit nečinných spojení. Transport.DisableKeepAlives = true to přikryje, ale skutečná rada z poznámek k vydání zní, že „hlubší pohled by pravděpodobně byl přínosný“, což v Go-týmovém jazyce znamená máte větší problémy.

Priorita klienta HTTP/2 (RFC 9218). Server nyní respektuje signály priority klienta. Pokud dáváte přednost staršímu plánování round-robin, nastavte Server.DisableClientPriority = true.

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

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

3.8. Drobnosti, které přinášejí radost

strings.CutLast / bytes.CutLast. Cut dělí na prvním oddělovači. Varianta „poslední“ neexistovala, takže každý ručně psal LastIndex a řezání, a zhruba 30 % z nás se napoprvé spletlo o jedna.

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 IPv6 adresy jich obsahují plno.

math/big.Int.Divide. Dělení s explicitním režimem zaokrouhlování, které nahrazuje hod mincí otázkou, zda chcete 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 oboru, kde je pravidlo zaokrouhlování zapsáno v předpisu, je toto pro vás.

url.URL.Clone a url.Values.Clone. Hluboké kopie. Staré mělké kopírování *u sdílelo ukazatel Userinfo, což vedlo k přesně tomu typu chyby, jejíž nalezení zabere 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

Metody čtení net.UnixConn nyní vracejí io.EOF přímo, místo aby jej zabalily do net.OpError. err == io.EOF nyní funguje, jak to vždy mělo.

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

unicode 15 → 17. Dvě verze najednou. Chování klasifikace a normalizace řetězců se může jemně změnit. 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. Projeví se to i v archive/zip, compress/gzip, compress/zlib a image/png. Pokud máte golden-file testy, které hashují komprimovaný výstup, rozbijí se, a nebude to regrese. Zkontrolujte to před upgradem, ne během vyšetřování incidentu.

4. Toolchain

4.1. go fix získal další modernizátory

go fix se tiše 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 }

Na jednom malém souboru se spustili čtyři modernizátory:

  • embedlit: přepisuje vložené literály na novou syntaxi z §1.2
  • atomictypes: atomic.AddInt64(&x, 1) se mění na atomic.Int64.Add(1), což znemožňuje nesynchronizovaný přístup a tiše opravuje chyby zarovnání na 32bitových platformách, o kterých jste nevěděli
  • slicesbackward: zpětné smyčky se mění na slices.Backward
  • waitgroupgo: rituál Add/go/Done se mění na wg.Go (přejmenováno z waitgroup z 1.26, aby nedocházelo k nejednoznačnosti)

go tool fix help vypíše všech 26; go tool fix help <name> vysvětlí jeden z nich. Také fmtappendf byl odstraněn „z důvodu stylistických obav“, což je nádherně diplomatický způsob, jak popsat, co se stalo v tom vlákně issue.

Spuštění go fix ./... na starší kódové základně je opravdu uspokojivé odpoledne. Nejprve si samozřejmě přečtěte diff.

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

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

go test nyní ve výchozím stavu spouští vet kontrolu stdversion, která označuje symboly standardní knihovny novější, než povoluje 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 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]

Tím se odstraňuje klasický scénář, kdy vše funguje na vašem stroji (nejnovější toolchain) a v CI nebo na starším prostředí uživatele to exploduje. Pokud publikujete knihovny s konzervativní direktivou go, je to dárek.

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 (bez pole), výstup t.Error (error) a řádky generované frameworkem (frame) jsou nyní rozlišitelné. Kdokoli, kdo napsal regulární výraz, aby zjistil, které řádky výstupu testu jsou „skutečné“, si jej nyní může smazat.

4.4. Vylepšení go doc

Syntaxe package@version. Přečtěte si dokumentaci konkrétní verze, 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...

Užitečné pro kontrolu změn před upgradem.

-ex a výpis zdrojového kódu příkladů:

 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 příkladu i očekávaný výstup, přímo v terminálu, bez otevírání záložky prohlížeče, která bude stále otevřená příští čtvrtek.

4.5. go mod tidy uklízí vaše bloky require

U modulů s go 1.27 nebo novějším go mod tidy slučuje roztroušené bloky require nejvýše do dvou (přímé a nepřímé) a zachovává 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	// sítě
 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 především uklízí po řešení konfliktů při slučování v Gitu, kde se roztroušené bloky require rodí. Pokud má váš tým více než tři lidi přidávající závislosti, konflikty ve vašem go.mod budou výrazně tišší.

4.6. Různé drobnosti

  • Podpora bzr byla odstraněna. Pokud se vás to týká, rád bych si poslechl váš příběh.
  • go tool trace -http se při zadání pouze portu váže na localhost. -http=:6060 již neposlouchá na všech rozhraních. Použijte -http=0.0.0.0:6060, pokud to myslíte vážně. Nyní to odpovídá go tool pprof a brání občasnému náhodnému veřejnému profileru.
  • Soubory s odpověďmi (@file) jsou podporovány v compile, link, asm, cgo, cover a pack ve formátu kompatibilním s GCC. Pro buildovací systémy, které překračují limity délky příkazového řádku, zdar, Bazel.

5. Kompilátor, linker, porty

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

Názvy symbolů funkčních literálů (closures) 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, až na jednu: kód, který porovnává identitu funkcí pomocí reflect.Value.Pointer, uvidí „rovno“ častěji než dříve. Toto porovnání nikdy nebylo platné, ale pokud ho máte, nyní začne lhát hlasitěji.

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

Porty.

  • Darwin nyní vyžaduje macOS 13 Ventura nebo novější, jak bylo oznámeno v 1.26. Zkontrolujte své CI runnery.
  • PowerPC (GOOS=linux GOARCH=ppc64) přešlo na ABI ELFv2. Vyžaduje jádro Linuxu 3.13+ (RHEL7 má zpětný port do 3.10). Cgo, PIE a externí linkování jsou nyní podporovány. Pokud používáte cgo, ale potřebujete statický čistě Go binární soubor, nastavte CGO_ENABLED=0.

6. Kontrolní seznam pro upgrade

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

  • Golden-file testy 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 hlášení JSON. Chování je identické; text chyby identický není.
  • Odstraněné GODEBUGy v go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf: tyto selžou při buildu pouze tehdy, když jsou nastaveny na své staré hodnoty.
  • Porušení stdversion. go test je nyní zachytí. Pokud udržujete knihovnu s konzervativní direktivou go, spusťte to včas.
  • CI runnery s macOS 12 nebo starším. Podpora skončila.
  • Testy ověřující názvy symbolů funkčních literálů a porovnání funkcí přes reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 nebo Client na každý požadavek. V kombinaci s automatickým vyprazdňováním těl odpovědí se to může zpomalit.

Závěrečné myšlenky

Go 1.27 je vydání, kde se najednou splatilo mnoho odložené údržby.

Generické metody byly chybějícím kouskem práce na generikách z 1.18. Selektory polí ve struct literálech uzavřely jedenáctiletý issue. encoding/json/v2 představuje dva roky diskuse o návrhu. A uuid zaplnilo díru, kterou téměř každý Go projekt celou dekádu латal stejnou externí závislostí.

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

  1. httptest.NewTestServer + synctest.Sleep: časově závislé HTTP testy se stanou rychlými a deterministickými. Nejlepší poměr úsilí a přínosu v celém vydání.
  2. Profil úniku goroutin: vystavte jeden endpoint ve stagingu a připravte se na pokoru.
  3. Štítky goroutin v tracebackách: tři řádky pprof.Do na vstupním bodu požadavků promění vaši další stack dump ve 3 ráno v něco čitelného.
  4. go fix ./...: nechte nástroj, ať provede modernizaci.
  5. encoding/json/v2: nespěchejte. Přijměte jednu option po druhé.

Stáhněte si go1.27rc3 a spusťte na něm svou testovací sadu hned teď. Vše na tomto kontrolním seznamu je výrazně příjemnější objevit v úterý odpoledne než během incidentu.

Reference