Go 1.27, avagy hogyan tanultam meg nem aggódni, és megszeretni a Generic Methods használatát

A Go 1.27 2026 augusztusában érkezik, és ezúttal maga a nyelv is változott. Nem úgy, hogy „hozzáadtunk egy új függvényt a slices-hoz”, hanem ténylegesen. Megérkeztek a generikus metódusok. Egy tizenegy éves probléma megoldódott. Az encoding/json csendben lecserélésre került egy új motorra, miközben a rendszer „repülés közben” volt.
Az alábbi példák mindegyike go1.27rc3 (darwin/arm64) környezetben futott. Minden kimeneti blokk valódi kimenet, másolva és beillesztve, beleértve a hibaüzeneteket is. Ha valami furcsának tűnik, az azért van, mert a fordító pontosan ezt mondta.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. A nyelv megváltozott (igen, tényleg)
1.1. Generikus metódusok
A Go 1.18 óta rendelkezünk generikusokkal, és a Go 1.18 óta zajlik a „beszélgetés”. Ez így hangzik:
„Hadd írjak már egy
Mapmetódust a szelet típusomhoz—”
a metódusnak nem lehetnek típusparaméterei„...rendben. Akkor marad a csomag szintű függvény.”
A metódusok csak a reciever (fogadó) által deklarált típusparamétereket használhatták. Saját metódus nem vezethetett be újakat. Így minden generikus transzformáció a csomag hatókörébe száműzetett, ahol tizenhét másik, MapSlice névváltozatú, szabadon lebegő segédfüggvény mellett várakozott.
A Go 1.27 ezt javítja:
1type List[E any] []E
2
3// Az F egy, a metódus által deklarált típusparaméter
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}
A fogadónak nem is kell generikusnak lennie. Egy egyszerű struct is rendelkezhet generikus metódussal:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Mielőtt azonban ma délután refaktorálná a teljes kódbázisát, két korlátozást érdemes figyelembe venni, mivel ezek fontosabbak, mint amilyennek látszanak:
- Az interfész metódusok nem deklarálhatnak típusparamétereket.
- A generikus metódusok nem implementálhatnak interfész metódusokat.
A második az, ami problémát okozhat:
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
Tehát a generikus metódusok és a dinamikus diszpécs nem férnek meg egymással. Ez nem azért van, mert a Go csapata szigorú lenne — hanem azért, mert egy generikus metódusnak végtelen számú példányosítása lehet, az interfész metódustáblázatának viszont végesnek és fordítási időben ismertnek kell lennie. Nem tehetünk végtelen dolgot egy véges táblázatba. Az univerzum nemet mondott.
Gyakorlati tanács: a generikus metódusok beton típusokhoz és segédprogram jellegű API-khoz valók. Mindennek, ami interfész mögé bújik, továbbra is a régi megközelítést kell használnia.
A standard könyvtár már élt a lehetőséggel. A math/rand/v2 Rand típusa kapott egy generikus metódust:
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
Korábban csak a csomag szintű rand.N volt generikus, ami azt jelentette, hogy a globális forrást kellett használni. Most a saját, seedelt *Rand példánya is megkapja ugyanezt a kényelmet. Apróság, de hasznos.
1.2. Struct literál mezőválasztók, avagy: a 9859-es számú probléma végre nyugvópontra jutott
A beágyazás (embedding) lehetővé teszi az u.ID használatát. Az beágyazás azonban nem teszi lehetővé a User{ID: 1} használatát. Helyette a User{Base: Base{ID: 1}} formát kell írni, ami a Go módja arra, hogy megkérdezze, tényleg így gondoltuk-e.
A 9859-es számú problémát 2015-ben nyitották meg. Most lezárták. Valahol egy gopher, aki azóta kétszer is karriert váltott, épp most kap egy GitHub-értesítést.
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: használja közvetlenül a promótált mezőt
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
A name nem a Line mezője. Ez a beágyazott Object mezője. Korábban a Line{Object: Object{name: "diagonal"}, ...} formát kellett írni.
Most jöjjenek a finom részletek, amelyeket úgy ellenőriztem, hogy megpróbáltam az egészet lefordítani.
(a) A kulcs továbbra is egy egyszerű azonosító. Nem írhat tetszőleges választóútvonalat (selector path).
1_ = Line{Object.name: "x"} // érvénytelen mezőnév: Object.name a struct literálban
2_ = Line{p.x: 1} // érvénytelen mezőnév: p.x a struct literálban
Tehát a funkció nem az, hogy „a kulcsok most úgy működnek, mint a mezőelérés”. Kifejezetten arról van szó, hogy „az implicit módon promótált mezőnevek engedélyezettek”. Ez valamivel szűkebb kör, mint amit a cím sugall.
(b) Nem adhat meg egyszerre egy beágyazott mezőt és egy abból promótált mezőt.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// nem adható meg egyszerre a promótált mezőnév és a befoglaló beágyazott Object mező
Ésszerű. Két egymásnak ellentmondó dolgot mondott ugyanarról a memóriaterületről, és a fordító nem volt hajlandó tippelni.
(c) A mutatóalapú beágyazás (pointer embedding) nem támogatott.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// érvénytelen implicit mutató indirekció a név eléréséhez
Egy mutató követéséhez mutatóra van szükség, a literál konstrukció pillanatában viszont még nincs ilyen. Ez is méltányos.
Jó hír: a go fix átírja helyettünk a régi literálokat. Erről később még lesz szó.
1.3. A függvénytípus-következtetés kevésbé önkényessé vált
A generikus függvények típus-következtetése korábban bizonyos helyeken működött, másokon nem, anélkül, hogy bármilyen felismerhető elv állt volna a háttérben. Változóhoz rendelés? Rendben. Ugyanazon függvény struct mezőbe helyezése? Fordítási hiba, kérjük, írjon double[int]-et, mintha 2022 lenne.
A Go 1.27 a következtetést minden olyan kontextusban működőképessé teszi, ahol a céltípus egyértelműen ismert:
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: double[int] szükséges volt
8 a := A{double} // 1.26: double[int] szükséges volt
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: double[int] szükséges volt
12
13 var fn func(float64) float64 = double // ez mindig is működött
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Egy double függvény, amely func(int) int típusként példányosul három helyen, és func(float64) float64 típusként egy negyedik helyen, teljes egészében a kontextusból. Ha opciós structokat vagy függvényértékekkel teli kezelőtáblákat használ, a [T] zaj jelentős része el fog tűnni a kódbázisából.
2. Runtime: Ingyenes teljesítmény és szivárgásdetektor
2.1. Méret-specifikus allokáció
A fordító mostantól méret-specifikus allokációs rutinokat hív meg kis objektumok esetén. A kiadási megjegyzések szerint az allokációk akár 30%-kal gyorsabbak 80 bájt alatt, és körülbelül 1%-os általános javulás érhető el az allokáció-intenzív programoknál.
Nem hiszek a kiadási megjegyzéseknek mérés nélkül, tehát:
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# alapértelmezett (méret-specifikus malloc bekapcsolva)
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
30–35%-kal gyorsabb egy olyan mikrobencsmarkban, amely Apple Siliconon csak allokál. Vagyis: ez a legjobb eset, amelyet laboratóriumi körülmények között értünk el egy olyan teszttel, amit kifejezetten arra terveztek, hogy jól mutasson. Valós programban a GC és a tényleges munka nagy részét elnyeli majd. Az ~1%-os állítás az őszinte.
Az ára a bináris mérete. Hello World:
12413202 bájt (alapértelmezett)
22361826 bájt (nosizespecializedmalloc)
Körülbelül 50 KB, fixen, a programtól függetlenül. A GOEXPERIMENT=nosizespecializedmalloc paraméterrel kikapcsolható, de ez a menekülőút a tervek szerint a Go 1.28-ban el lesz távolítva, ezért inkább hibajelentési megoldásként kezelje, ne életmódként.
2.2. A goroutine szivárgási profil most már valódi
Kísérleti volt a Go 1.26-ban, általánosan elérhető a 1.27-ben. A goroutineleakprofile GOEXPERIMENT megszűnt; egyszerűen működik.
1func leak() {
2 ch := make(chan int) // senki sem fog küldeni. senki.
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
Három goroutine, véglegesen elakadva, fájlnévvel és sorszámmal, amely pontosan arra mutat, hol követte el a hibát.
Az ennek alapjául szolgáló trükk valóban okos: újrahasznosítja a szemétgyűjtő elérhetőségi analízisét. Ha a G goroutine a P primitíven blokkolódik, és a P elérhetetlen bármely futtatható goroutine számára (vagy bármi számára, amit azok felébreszthetnének), akkor semmi sem érheti el többé a P-t, így a G sosem fog felébredni. Ez nem heurisztika — ez bizonyíték.
Ugyanez a kialakítás ingyen adja a korlátot is: ha a csatorna vagy mutex globálisan vagy egy még futó goroutine lokális változóján keresztül elérhető, a GC még mindig látja, így a runtime nem tud következtetni. Figyelje meg, hogy még a fenti játékpéldának is két runtime.GC() hívásra van szüksége a jelentéshez. Nem fog mindent elkapni.
Azonban elkapja a klasszikus „elfelejtettem törölni a contextet, a worker goroutine örökké él” esetet — ami a szivárgások többsége.
Ha importálja a net/http/pprof csomagot, ez a /debug/pprof/goroutineleak alatt is elérhető. Kösse be a staging környezetbe, ellenőrizze hetente, és maradjon csendben elborzadva.
Ezt a munkát Vlad Saioc jegyzi az Ubernél, akinek jár egy választott ital.
2.3. A traceback most már megmondja, melyik kérés halt meg
A Go 1.27-es vagy újabb verziót deklaráló modulok esetén a traceback fejléc sorai mostantól tartalmazzák a runtime/pprof goroutine címkéket.
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...
A fontos részlet: a címkéket a gyermek goroutine-ok öröklik. A 3-as goroutine sosem állított be címkét. A szülőjétől kapta.
Tehát amikor legközelebb a production környezetben holtpont alakul ki és valaki SIGQUIT-ot küld a folyamatnak, a 4000 egyforma kinézetű goroutine helyett 4000 olyan goroutine-t kap, amelyek meg vannak címkézve, hogy melyik kéréshez és melyik bérlőhöz tartoznak. A kérés belépési pontjánál három sornyi pprof.Do ezt megvásárolja Önnek.
Mivel a címkék tartalmazhatnak olyan dolgokat, amelyeket nem szeretne stderr-re írni, a GODEBUG=tracebacklabels=0 kikapcsolja azt, és ez a kikapcsolási lehetőség szándékosan örökre megmarad.
2.4. Az asynctimerchan végleg megszűnt
A Go 1.23 a timer csatornákat pufferelés nélkülivé (szinkronná) tette, és az asynctimerchan=1 beállítást kínálta a régi viselkedés visszaállítására. A 1.27-ben ez a beállítás véglegesen eltávolításra került. A time csomag csatornái szinkronok, pont, nincs tárgyalás.
Az érdekes rész az ezzel együtt bevezetett szabályzat. Egy eltávolított GODEBUG, amely a go.mod fájlban maradt, nem szakítja meg automatikusan a buildet — csak akkor szakítja meg, ha a régi értékre van állítva:
1# a go.mod tartalma: 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# a go.mod tartalma: godebug asynctimerchan=0
6(a build sikeres)
Ez azt jelenti, hogy csak azokat érinti a probléma, akik ténylegesen az eltávolított viselkedésre támaszkodtak. Mindenki más, aki az eredeti alapértelmezettre állította és elfelejtette, továbbra is gondtalan maradhat. Ez az API-archeológia egy átgondolt darabja.
3. Standard könyvtár
3.1. encoding/json/v2: Repülés közben cserélték le a motort
Ez a nagy horderejű változás. Több mint két évnyi javaslat-megbeszélés végre révbe ért.
Mostantól három csomag létezik, és a megértésük a csata nagy részét jelenti:
| Csomag | Feladat |
|---|---|
encoding/json | Az ismert v1 API. A viselkedés 100%-ban változatlan. Mostantól a v2-re épül |
encoding/json/v2 | Szemantikus feldolgozás. Go értékek ↔ JSON |
encoding/json/jsontext | Szintaktikus feldolgozás. JSON mint token-folyam |
A lényeg, hogy az encoding/json egy teljesen más implementációra épült át, és azonos módon viselkedik. Így:
1// A Go 1.27 encoding/json.Unmarshal rövidített változata
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Minden régi v1-es furcsaság opcióként lett kódolva, a DefaultOptionsV1() csomagba gyűjtve, és a v1 API mindig ezt a csomagot alkalmazza. Ez azt jelenti, hogy ez ugyanúgy viselkedik 1.26-ban és 1.27-ben:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Igen, a v1 továbbra is csendben elfogadja az ismétlődő kulcsokat, és az utolsót veszi figyelembe. Mindig is így tette. Még mindig így teszi. A kompatibilitás a rossz részekkel való kompatibilitást is jelenti.
A v2 viszont határozott véleménnyel bír:
1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: ismétlődő objektumtag-név "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: érvénytelen UTF-8 az 1. eltolás után
Az ismétlődő kulcsokat és az érvénytelen UTF-8 karaktereket visszautasítja. Mindkettő valóban veszélyes, nemcsak rendetlen — amikor két elemző nem ért egyet abban, hogy melyik ismétlődő kulcs nyer, biztonsági hibák keletkeznek. A CouchDB CVE-2017-12635 pont ilyen volt: egy JSON törzs két roles kulccsal, ahol az érvényesítő az egyiket, a tárolási réteg a másikat olvasta. A visszautasítás a helyes döntés.
Az opciók variadikusak:
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// rendezett map-kulcsok + behúzás
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// ismeretlen mezők visszautasítása — korábban Decoder és DisallowUnknownFields() kellett
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 — a legtöbb dolog, amit korábban külső könyvtárral vagy kézzel írt MarshalJSON függvénnyel oldott meg, mostantól flag.
A migrációs történet pedig a legjobb rész. A későbbi opciók nyernek, így a v2 szigorát viselkedésenként lehet bevezetni:
1// v1 szemantika megtartása, de az ismétlődő kulcsok visszautasítása, mint a v2-ben
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// ismétlődő objektumtag-név
Nem kell egy 200 ezer soros kódbázist átportolnia a v2-re ahhoz, hogy ne fogadjon el ismétlődő kulcsokat. Csak egy opciót kell átállítania. Bármilyen nagy projektnél ez a valósághű út.
Teljesítmény: a marshaling nagyjából pariban van, az unmarshaling jelentősen gyorsabb. Ha valami elromlik, a GOEXPERIMENT=nojsonv2 visszaállítja a régi implementációt — és ez a kikapcsolási lehetőség is előbb-utóbb megszűnik, szóval inkább jelentsen hibát, mintsem belenyugodjon.
A jsontext az alacsony szintű réteg: Encoder/Decoder, amely Token-ként és Value-ként járja be a JSON-t egy állapotgéppel, ami biztosítja a helyességet. Akkor nyúljon hozzá, ha streaming transzformátort vagy JSON szűrőt ír, és nem akarja materializálni a Go értékeket.
3.2. Egy standard uuid csomag
Végre. RFC 9562, a standard könyvtárban, nincs szükség go get parancsra.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: az első 48 bit egy időbélyeg, így ezek mindig létrehozási sorrendben rendeződnek
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}
Az egész API: New, NewV4, NewV7, Nil, Max, Parse, MustParse, és egy típus: type UUID [16]byte. Ez minden. Elolvashatja az egész csomag dokumentációját, amíg a kávéja kihűl.
Érdemes tudni:
- A
UUIDegy[16]byte, így a==működik, és közvetlenül használható map-kulcsként. Ugyanaz a kialakítás, mint agoogle/uuidesetében. - A
NilésMaxfüggvények, nem változók. Mivel egy csomag szintűvar Nil UUIDegy töltött fegyver, amit a saját lábára irányít, és valaki, valahol, végül hozzárendelne valamit. - A véletlenszerű bitek kriptográfiailag biztonságos generátorból származnak.
- Implementálja az
encoding.TextMarshaler/TextUnmarshaler/TextAppenderinterfészeket, így közvetlenül használható JSON structokban. - A
NewV7a megfelelő választás adatbázis elsődleges kulcsokhoz. Az időrendiség azt jelenti, hogy a B-fa indexe nem töredezik, amiben a v4 UUID-k híresen rosszak.
Mostantól törölhet egy függőséget. Ha v1/v3/v5 verziókra vagy különlegesebb elemzési opciókra van szüksége, a külső könyvtárakra továbbra is szükség lehet.
3.3. crypto/mldsa: Posztkvantum aláírások, és hatalmasak
A Go 1.24 megadta a crypto/mlkem csomagot a posztkvantum kulcscseréhez. A Go 1.27 elhozza a másik felét: ML-DSA aláírásokat, FIPS 204 szabvány szerint.
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
Nézze azokat a számokat. Egyetlen aláírás 3309 bájt. Az Ed25519 64 bájt. Ez 50-szeres növekedés, és a publikus kulcs ehhez képest majdnem 2 KB. Ha ezekből párat beletesz egy tanúsítványláncba, a TLS kézfogása saját MTU-stratégiát igényel majd.
Ez a kvantumellenállás tényleges ára manapság, és ezért nem vált át mindenki holnap. De benne van a standard könyvtárban, ami pontosan az a hely, ahol látni szeretné, mielőtt szüksége lenne rá.
Az Options.Context a tartomány-szétválasztást (domain separation) szolgálja: írjon alá ugyanazzal a kulccsal különböző célokra, használjon különböző kontextust mindegyikhez, és egy kontextusból származó aláírás nem lesz érvényes egy másikban. A fenti példa pontosan ezt mutatja — ugyanaz a kulcs, ugyanaz az üzenet, különböző kontextus, elutasítva.
A PrivateKey implementálja a crypto.Signer interfészt, így illeszkedik a meglévő interfészekhez. A crypto/x509 kezeli az ML-DSA kulcsokat és aláírásokat, a crypto/tls pedig támogatja az MLDSA44/MLDSA65/MLDSA87 aláírási sémákat TLS 1.3-ban.
Létezik SignDeterministic is, amely kihagyja a véletlenszerűséget — praktikus tesztekhez és reprodukálható buildekhez.
3.4. simd: Vektorutasítások assembly nélkül
A Go 1.26 bevezette az architektúra-specifikus simd/archsimd csomagot kísérleti jelleggel. A Go 1.27 hozzáadja a simd csomagot — hordozható és vektor-szélességtől független. Engedélyezze a GOEXPERIMENT=simd paraméterrel.
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 // kezelje a maradékot részleges betöltéssel/tárolással — nincs skalár tisztító ciklus
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]
A tervezési döntés, ami számít: a vektor szélessége sosem hardkódolt. Futásidőben kérdezi le a va.Len() függvénnyel. A VectorBitSize() megmondja a tényleges szélességet, az Emulated() megmondja, hogy valódi hardvert kapott-e, vagy egy udvarias szoftveres utánzást. Az én Macem 128 bites NEON-t jelentett; egy AVX-512 gép többet jelent; egy gép, amiben nincs semmi, emulációt jelent, és a kód továbbra is fut.
A LoadFloat32sPart/StorePart külön említést érdemel. A kézzel írt SIMD legidegesítőbb része mindig a tömb végén lévő maradék, és ez ezt külön skalár ciklus nélkül kezeli.
Még kísérleti, az API még instabil, ne tegye production környezetbe. De az a tény, hogy ez Go kód, nem pedig assembly vagy cgo, valóban nagy dolog.
3.5. hash/maphash.Hasher
Egy új interfész, amely leírja a típust és a hash-alapú konténerek közötti szerződést:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Miért? A Go beépített map-je csak comparable kulcsokat fogad el. Nem használhat szeletet (slice) kulcsként. Nem definiálhat „egyenlő kis- és nagybetű figyelmen kívül hagyásával” szabályt. A Hasher mindkettőt egyszerre oldja meg:
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) // ugyanaz a seed, különben az egész értelmetlen
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
A hagyományos == szemantikához ott a ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
A csapda: a Hasher egy interfész, nem adatstruktúra. Még nincs standard hash tábla vagy Bloom-szűrő, ami ezt fogyasztaná. Ez egy jövőbeli konténer csomag alapozása. Ma akkor használja, ha saját struktúrát épít, vagy egy meglévő implementációt fogyaszt, mint például a go/types.Hasher (amely lehetővé teszi a types.Type használatát map-kulcsként, tisztelve az Identical szabályt).
A seed kezelése az Ön dolga. Ha a fenti hashOf minden hívásnál friss seedet készítene, az azonos értékek különböző hash-értéket kapnának, és a példa false értéket írna ki — ez pontosan az a hiba, amit az első próbálkozásomkor elkövettem. Konténerenként egy seed. (A seed randomizált, hogy megakadályozza a hash-áradásos DoS támadásokat, ezért nem konstans.)
3.6. httptest.NewTestServer + synctest.Sleep: A meglepetés
Ha csak egy dolgot vesz át ebből a kiadásból, legyen ez az.
A testing/synctest a Go 1.25-ben végzett, és egy hamis órát adott a párhuzamos kód teszteléséhez. Csakhogy abban a pillanatban, amikor a tesztje valódi hálózatot érintett, az illúzió összeomlott. A Go 1.27 httptest.NewTestServer függvénye egy memóriabeli hamis hálózatot használ, így a buborék ép marad. A synctest.Sleep (= time.Sleep + synctest.Wait) pedig teljessé teszi.
Íme egy újrapróbálkozás exponenciális visszaeséssel (retry-with-exponential-backoff) teszt:
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 // a backoff pontosan 1s + 2s = 3s kell legyen
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)
Olvassa el ezt kétszer. Szimulált három másodpercnyi visszaesést egy valódi HTTP szerver ellen, és 0,00 másodperc alatt végzett. És az állítás elapsed == 3*time.Second — pontosan három másodperc, nem „legalább három másodperc, némi ütemező jitterrel”. A hamis órák nem jitterelnek.
A synctest.Sleep egy konkrét okból létezik: ha a tesztje ugyanannyi ideig alszik, mint a tesztelt kód, akkor találgatás, hogy melyik ébred fel először. A synctest.Sleep alszik, majd megvárja, amíg a buborékban lévő összes többi goroutine tartósan blokkolva van, így a rendszert akkor figyeli meg, amikor az megnyugodott.
Időtúllépések, újrapróbálkozások, áramkör-megszakítók, sebességkorlátozók — minden időfüggő HTTP kliens tesztje, amivel rendelkezik, gyors és determinisztikus lehet. Ha a tesztcsomagját jelenleg time.Sleep(100 * time.Millisecond) és remény tartja össze, ez az Ön kiútja.
3.7. net/http változások, amelyek ténylegesen érintik a productiont
Csendesek, de meg fognak jelenni a metrikáiban.
A HTTP/1 választestek automatikusan ürülnek a Close híváskor. Az olvasatlan tartalom mostantól kiürítésre kerül (egy konzervatív korlátig), amikor bezárja a testet, így a kapcsolat újrafelhasználható. Ami azt jelenti, hogy végre törölheti ezt az igét, amit mindenki lemásolt ugyanabból a Stack Overflow válaszból 2016 óta:
1// már nem szükséges
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
A legtöbb program számára ez semmit sem változtat vagy kis nyereség. Ha a dolgok rosszabbul mennek, valószínűleg abban a kategóriában van, amit a kiadási megjegyzések udvariasan leírnak: Transport.MaxIdleConns 0-ra állítva, vagy új Client minden kérésnél, teljesen megkerülve az üresjárati kapcsolat korlátját. A Transport.DisableKeepAlives = true elfedi a problémát, de a kiadási megjegyzések tényleges tanácsa az, hogy „egy mélyebb vizsgálat valószínűleg előnyös lenne”, ami a Go-csapat nyelvén azt jelenti: Önnek nagyobb problémái vannak.
HTTP/2 kliens prioritás (RFC 9218). A szerver mostantól figyelembe veszi a kliens prioritási jelzéseit. Ha a régi round-robin ütemezést részesítette előnyben, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Korlátozza, hány fejlécértéket fogad el a szerver, az alapértelmezett a DefaultMaxHeaderValueCount. Még egy ajtó bezárult a „küldj tízezer fejlécet és nézd meg mi történik” típusú támadások előtt.
ALPN a felhasználó által biztosított kapcsolatokon. Ha a net.Conn implementálja a ConnectionState() tls.ConnectionState metódust, a Transport és a Server elvégzi rajta a TLS ALPN egyeztetést — így a proxy-n keresztül menő egyedi dialerek továbbra is egyeztethetik a HTTP/2-t.
3.8. Apróságok, amelyek örömet okoznak
strings.CutLast / bytes.CutLast. A Cut az első elválasztónál vág. Nem volt „utolsó” változat, így mindenki kézzel írt LastIndex és szeletelés kombinációt, és körülbelül 30%-unk elsőre elvétette eggyel.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Nagyszerű fájlkiterjesztésekhez és host:port elemzéséhez — ahol az utolsó kettőspontot kell megtalálni, mert az IPv6 címek tele vannak velük.
math/big.Int.Divide. Osztás explicit kerekítési móddal, a „vajon Quo vagy Div kell nekem?” érmefeldobás helyett:
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
Ha olyan területen dolgozik, ahol a kerekítési szabályt rendelet írja elő, ez Önnek szól.
url.URL.Clone és url.Values.Clone. Mély másolatok (deep copy). A régi *u sekély másolat megosztotta a Userinfo mutatót, ami pontosan olyan hibát okozott, aminek a megtalálása egy nap, a kijavítása öt másodperc.
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 olvasási metódusok mostantól közvetlenül io.EOF-ot adnak vissza, ahelyett, hogy net.OpError-ba csomagolnák. Az err == io.EOF mostantól működik, ahogy mindig is kellett volna.
database/sql.ConvertAssign és driver.RowsColumnScanner. Driver-szerzőknek szóló funkciók. Az első felfedi a Rows.Scan által végzett típuskonverziókat; a második lehetővé teszi a drivereknek, hogy közvetlenül a felhasználói célhelyekre olvassanak, kihagyva egy köztes allokációt.
unicode 15 → 17. Két verzió egy ugrással. A karakterosztályozás és normalizáció viselkedése finoman változhat. Ha vannak tesztjei, amelyek ettől függenek, meg fogja tudni.
A compress/flate gyorsabb lett — és a kimeneti bájtjai eltérhetnek a Go 1.26-tól. Ez továbbgyűrűzik az archive/zip, compress/gzip, compress/zlib és image/png csomagokra. Ha vannak golden-file tesztjei, amelyek hashelik a tömörített kimenetet, azok el fognak törni, és ez nem regresszió. Ellenőrizze ezt mielőtt frissít, ne az incidens vizsgálata közben.
4. Toolchain
4.1. A go fix több modernizálót kapott
A go fix csendben kódmodernizáló eszközzé alakul. A Go 1.27 hozzáadta az atomictypes, embedlit, slicesbackward és unsafefuncs funkciókat. Használja a -diff opciót az előnézethez:
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 }
Négy modernizáló lépett életbe egy kis fájlon:
embedlit— átírja a beágyazott literálokat az új szintaxissal (§1.2)atomictypes— azatomic.AddInt64(&x, 1)helyettatomic.Int64.Add(1)lesz, ami lehetetlenné teszi a nem atomi hozzáférést, és csendben kijavítja a 32 bites illesztési hibákat, amelyekről nem is tudottslicesbackward— a visszafelé irányuló ciklusokslices.Backwardlesznekwaitgroupgo— azAdd/go/Donerituáléwg.Golesz (átnevezve az 1.26-oswaitgroupnévről a kétértelműség elkerülése érdekében)
A go tool fix help felsorolja mind a 26-ot; a go tool fix help <name> elmagyarázza az egyiket. Továbbá a fmtappendf el lett távolítva „stilisztikai aggályok miatt”, ami egy gyönyörűen diplomáciai módja annak, hogy leírják, mi történt abban a threadben.
A go fix ./... futtatása egy régi kódbázison valóban kielégítő délutáni program. Először olvassa el a diffet, nyilvánvalóan.
4.2. A go test alapértelmezetten futtatja a stdversiont
Ez az a változás, amely a legvalószínűbben szakítja meg a napját.
A go test mostantól alapértelmezetten futtatja a stdversion vet ellenőrzést, megjelölve a standard könyvtár olyan szimbólumait, amelyek újabbak, mint amit a go.mod fájl go direktívája megenged:
1// a go.mod azt mondja: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // A CutLast egy 1.27-es szimbólum
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]
Ez megszünteti a klasszikus hibaforrást, ahol minden működik az Ön gépén (legújabb toolchain), és felrobban a CI-ban vagy egy felhasználó régebbi környezetében. Ha konzervatív go direktívával rendelkező könyvtárakat publikál, ez egy ajándék.
4.3. A go test -json kapott OutputType mezőt
Az "Action":"output" sorok mostantól tartalmaznak egy opcionális "OutputType" mezőt: "error", "error-continue" vagy "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'
A t.Log kimenet (nincs mező), a t.Error kimenet (error) és a keretrendszer által generált sorok (frame) mostantól megkülönböztethetők. Aki valaha írt regexet annak kiderítésére, hogy a tesztkimenet mely sorai „valódiak”, most törölheti azt.
4.4. go doc fejlesztések
package@version szintaxis. Dokumentáció olvasása egy adott verzióhoz anélkül, hogy hozzáadná a moduljához:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Praktikus annak ellenőrzésére, mi változott frissítés előtt.
-ex és példa forráskód nyomtatás:
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
Példa forráskód és elvárt kimenet, a terminálban, anélkül, hogy megnyitna egy böngészőfület, ami jövő csütörtökig nyitva maradna.
4.5. A go mod tidy kitakarítja a require blokkokat
A go 1.27-es vagy újabb moduloknál a go mod tidy egyesíti a szétszórt require blokkokat legfeljebb kettőre (közvetlen és közvetett), megőrizve a csatolt megjegyzéseket.
1// Előtte
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// Utána: 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)
Figyelje meg, hogy a // networking megjegyzés túlélte. Ez főleg a Git merge konfliktusfeloldás utáni takarítás, ahol a kóbor require blokkok születnek. Ha a csapatában háromnál több ember ad hozzá függőségeket, a go.mod konfliktusai jelentősen csendesebbek lesznek.
4.6. Egyéb érdekességek
bzrtámogatás eltávolítva. Ha ez érinti Önt, igazán kíváncsi lennék a történetre.- A
go tool trace -httpcsak lokális hostra kötődik, ha csak portot ad meg. A-http=:6060már nem figyel minden interfészen. Használja a-http=0.0.0.0:6060parancsot, ha komolyan gondolta. Ez most már megegyezik ago tool pprofviselkedésével, és megelőzi az alkalmi véletlenszerű nyilvános profiler-megnyitást. - Válasz fájlok (
@file) támogatottak acompile,link,asm,cgo,coveréspackparancsoknál, GCC-kompatibilis formátumban. Azoknak a build rendszereknek, amelyek túllépik a parancssori hosszkorlátokat — üdvözlet, Bazel.
5. Fordító, linker, portok
Fordító. A relatív fájlnevek a //line direktívákban mostantól a tartalmazó fájl könyvtárához képest oldódnak fel, megegyezve a go/scanner viselkedésével. Fontos, ha kódgenerátorokat ír.
A függvény literál (closure) szimbólumnevek mostantól egyszerűbbek — ugyanaz a név az inliningtól függetlenül, és ugyanazon literál több példánya osztozhat a kódon a binárisban. Nincs funkcionális változás, kivéve: a függvényazonosságot reflect.Value.Pointer segítségével összehasonlító kód gyakrabban lát „egyenlőt”, mint korábban. Ez az összehasonlítás sosem volt érvényes, de ha használja, mostantól hangosabban fog hazudni.
Linker. Új -macos és -macsdk opciók állítják be az OS és SDK verziókat a macOS LC_BUILD_VERSION betöltési parancsban.
Portok.
- A Darwin mostantól macOS 13 Ventura vagy újabb verziót igényel, ahogy azt az 1.26-ban bejelentették. Ellenőrizze a CI-futtatóit.
- A PowerPC (
GOOS=linux GOARCH=ppc64) átállt az ELFv2 ABI-ra. Linux kernel 3.13+ szükséges (RHEL7 visszaportolva 3.10-re). A Cgo, PIE és külső linkelés mostantól támogatott. Ha cgo-t használ, de statikus, tiszta Go binárisra van szüksége, állítsa aCGO_ENABLED=0értéket.
6. Frissítési ellenőrzőlista
A Go 1.27 komolyan veszi a kompatibilitást, de ellenőrizze ezeket, mielőtt frissít:
- Golden-file tesztek tömörített kimeneten. A
compress/flatemegváltoztatta a kódolókat; a gzip/zip/png bájtok eltérhetnek. - JSON hibaüzenet szövegeire illesztő tesztek. A viselkedés azonos; a hibaüzenet szövege nem.
- Eltávolított GODEBUG-ok a
go.modfájlban.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— ezek csak akkor szakítják meg a buildet, ha a régi értékükre vannak állítva. -
stdversionszabályszegések. Ago testmostantól elkapja ezeket. Futtassa korán, ha olyan könyvtárat tart fenn, amely konzervatívgodirektívát használ. - macOS 12 vagy régebbi CI-futtatók. A támogatás megszűnt.
- Függvényliterál szimbólumnevekre építő tesztek, és
reflect.Value.Pointerfüggvény összehasonlítások. -
Transport.MaxIdleConns = 0vagyClientkérésenként. Az automatikusan ürülő választestekkel kombinálva ez lassabbá válhat.
Záró gondolatok
A Go 1.27 az a kiadás, ahol rengeteg halasztott karbantartási munka egyszerre vált esedékessé.
A generikus metódusok voltak az 1.18-as generikus munka hiányzó darabjai. A struct literál mezőválasztók egy tizenegy éves problémát zártak le. Az encoding/json/v2 két évnyi javaslat-megbeszélés eredménye. A uuid pedig betöltött egy űrt, amit alapvetően minden Go projekt ugyanazzal a külső függőséggel foltozott egy évtizede.
Ha prioritási sorrendet szeretne az érték eléréséhez:
httptest.NewTestServer+synctest.Sleep— az időfüggő HTTP tesztek gyorsak és determinisztikusak lesznek. A legjobb erőfeszítés-haszon arány az egész kiadásban.- A goroutine szivárgási profil — tegyen elérhetővé egy endpointot stagingben, és készüljön fel az alázatra.
- Traceback goroutine címkék — három sornyi
pprof.Doa kérés belépési pontjánál olvashatóvá teszi a következő hajnali stack dumpot. go fix ./...— hagyja, hogy az eszköz elvégezze a modernizálást.encoding/json/v2— semmi sietség. Vezesse be opcióról opcióra.
Töltse le a go1.27rc3 verziót, és futtassa rajta a tesztcsomagját most. Az ellenőrzőlistán szereplő dolgokat sokkal kellemesebb kedd délután felfedezni, mint egy incidens közben.
Referenciák