Go 1.27, sau Cum am învățat să nu mai îmi fac griji și să iubesc metodele generice


Go 1.27 apare în august 2026 și, pentru o dată, limbajul în sine s-a schimbat. Nu schimbarea de tip „am adăugat o nouă funcție în slices” — ci o schimbare efectivă. Metodele generice sunt incluse. O problemă veche de unsprezece ani a fost închisă. encoding/json a fost înlocuit în mod discret cu un nou motor în timpul zborului.
Fiecare exemplu de mai jos a fost rulat pe go1.27rc3 (darwin/arm64). Fiecare bloc de ieșire reprezintă rezultatul real, copiat și lipit, incluzând mesajele de eroare. Dacă ceva pare ciudat, este pentru că așa a spus efectiv compilatorul.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Limbajul s-a schimbat (Da, pe bune)
1.1. Metode generice
Începând cu Go 1.18 avem generice, iar din Go 1.18 avem Această Discuție. Ea sună cam așa:
„Ia să scriu eu o metodă
Mappe tipul meu de felie (slice)—”
metoda nu trebuie să aibă parametri de tip„...bine. Atunci va fi o funcție la nivel de pachet.”
Metodele puteau folosi doar parametrii de tip declarați de receptor. Proprija ta metodă nu putea introduce alții noi. Așa că fiecare transformare generică a fost exilată în domeniul de vizibilitate al pachetului (package scope), unde a stat alături de alte șaptesprezece funcții ajutătoare independente numite într-o oarecare variantă MapSlice.
Go 1.27 remediază acest aspect:
1type List[E any] []E
2
3// F este un parametru de tip declarat chiar de metodă
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}
Nici măcar nu este necesar ca receptorul să fie generic. O structură banală poate avea o metodă generică:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Acum, înainte de a vă refactoriza întreaga bază de cod în această după-amiază, există două restricții, iar ele contează mai mult decât par:
- Metodele de interfață nu pot declara parametri de tip.
- Metodele generice nu pot implementa metode de interfață.
Aceasta din urmă este cea care vă va crea probleme:
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
Așadar, metodele generice și dispecerizarea dinamică (dynamic dispatch) nu se combină. Nu este vorba că echipa Go ar fi zgârcită; motivul este că o metodă generică are un număr infinit de instanțieri, în timp ce o tabelă de metode de interfață trebuie să fie finită și cunoscută la momentul compilării. Nu puteți plasa un lucru infinit într-o tabelă finită. Universul a spus nu.
Interpretare practică: metodele generice sunt destinate tipurilor concrete cu API-uri orientate spre utilitate. Orice trebuie să se ascundă în spatele unei interfețe are în continuare nevoie de abordarea veche.
Biblioteca standard a profitat deja de acest lucru. Rand din math/rand/v2 a primit o metodă generică:
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
Înainte, doar funcția de la nivelul pachetului rand.N era generică, ceea ce însemna utilizarea sursei globale. Acum, propriul dumneavoastră *Rand inițializat cu o sămânță beneficiază de aceeași comoditate. Un lucru mărunt. Un lucru plăcut.
1.2. Selectori de câmp pentru literali de structură (Struct Literal Field Selectors), sau: Problema #9859 își găsește în sfârșit liniștea
Încorporarea (embedding) vă oferă u.ID. Încorporarea nu vă oferă User{ID: 1}. În schimb, vă oferă User{Base: Base{ID: 1}}, ceea ce reprezintă modul în care Go vă întreabă dacă ați vorbit serios.
Problema #9859 a fost înregistrată în 2015. Acum a fost închisă. Undeva, un gopher care și-a schimbat cariera de două ori între timp primește o notificare pe GitHub.
1type Object struct{ name, color string }
2type Point3D struct {
3 Object
4 x, y, z float64
5}
6type Line struct {
7 Object
8 p, q Point3D
9}
10
11// Go 1.27: folosiți direct câmpul promovat
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name nu este un câmp al lui Line. Este un câmp al lui Object încorporat. Anterior, ați fi scris Line{Object: Object{name: "diagonal"}, ...}.
Trecem acum la detaliile tehnice, pe care le-am verificat încercând efectiv să le compilez pe toate.
(a) Cheia rămâne un identificator simplu. Nu puteți scrie o cale de selectare arbitrară.
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
Prin urmare, caracteristica nu înseamnă „cheile funcționează acum ca accesul la câmpuri”. Este vorba în mod specific despre faptul că „sunt permise numele de câmpuri promovate implicit”. Puțin mai restrâns decât sugerează titlul.
(b) Nu puteți specifica simultan un câmp încorporat și ceva promovat din acesta.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Rezonabil. I-ați transmis două lucruri contradictorii despre aceeași zonă de memorie, iar el a refuzat să ghicească.
(c) Încorporarea de pointeri nu este invitată.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Pentru a urma un pointer, aveți nevoie de un pointer pe care să îl urmați, iar în momentul construirii literalului acesta nu există încă. De asemenea, corect.
Vestea bună: go fix va rescrie vechii dumneavoastră literali în mod automat. Mai multe despre asta mai târziu.
1.3. Inferența tipului de funcție a devenit mai puțin arbitrară
Inferența de tip pentru funcțiile generice obișnuia să funcționeze în anumite locuri și în altele nu, fără a exista un principiu discernabil la bază. Atribuirea unei variabile? În regulă. Plasarea exact aceleiași funcții în câmpul unei structuri? Eroare de compilare, vă rugăm să scrieți double[int] de parcă am fi în 2022.
Go 1.27 face ca inferența să funcționeze în orice context în care tipul țintă este cunoscut fără ambiguitate:
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: era necesar double[int]
8 a := A{double} // 1.26: era necesar double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: era necesar double[int]
12
13 var fn func(float64) float64 = double // acest lucru a funcționat întotdeauna
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
O singură funcție double, instanțiată ca func(int) int în trei locuri și ca func(float64) float64 într-al patrulea, exclusiv pe baza contextului. Dacă scrieți structuri de opțiuni sau tabele de handlere pline de valori de tip funcție, o mulțime de zgomot reprezentat de [T] este pe cale să dispară din baza dumneavoastră de cod.
2. Runtime: Performanță gratuită și un detector de scurgeri (leak detector)
2.1. Alocare specializată în funcție de dimensiune (Size-Specialized Allocation)
Compilatorul emite acum apeluri către rutine de alocare specializate în funcție de dimensiune pentru obiectele mici. Notele de lansare revendică reduceri de până la 30% pentru alocările sub 80 de octeți și de aproximativ 1% în total pentru programele care utilizează intensiv alocările.
Nu cred notele de lansare fără măsurători, așa că:
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# implicit (malloc specializat pe dimensiuni activat)
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
Cu 30–35% mai rapid într-un microbenchmark care nu face altceva decât să aloce pe Apple Silicon. Ceea ce înseamnă: acesta este scenariul optim, obținut în condiții de laborator printr-un benchmark conceput să facă numărul să arate bine. Într-un program real, colectorul de gunoi (GC) și activitatea efectivă vor estompa majoritatea acestor câștiguri. Afirmația de ~1% este cea onestă.
Costul îl reprezintă dimensiunea binarului. Hello World:
12413202 octeți (implicit)
22361826 octeți (nosizespecializedmalloc)
Aproximativ 50 KB, fixați, indiferent de programul dumneavoastră. Puteți renunța la această facilitate folosind GOEXPERIMENT=nosizespecializedmalloc, însă acea soluție de rezervă este programată pentru eliminare în Go 1.28, așa că tratați-o mai degrabă ca pe o ocolire pentru raportarea bug-urilor decât ca pe un stil de viață.
2.2. Profilul scurgerilor de goroutine este acum o realitate
Experimental în Go 1.26, disponibil în mod general în 1.27. Variabila goroutineleakprofile din GOEXPERIMENT a dispărut; totul funcționează direct.
1func leak() {
2 ch := make(chan int) // nimeni nu va trimite vreodată. nimeni.
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
Trei goroutine, blocate permanent, cu un nume de fișier și un număr de linie care indică exact locul unde ați comis eroarea.
Șiretenia din spatele acestui mecanism este genială: reutilizează analiza de accesibilitate a colectorului de gunoi. Dacă goroutina G este blocată pe elementul primitiv P, iar P este inaccesibil din orice goroutină executabilă (sau din orice element pe care acele goroutine l-ar putea trezi), atunci nimic nu va mai atinge vreodată P, așa că G nu se va mai trezi niciodată. Aceasta nu este o heuristică, ci o demonstrație matematică.
Același design vă oferă gratuit și limitarea: dacă canalul sau mutexul este accesibil printr-o variabilă globală sau printr-o variabilă locală a unei goroutine care încă rulează, colectorul de gunoi îl poate vedea în continuare, astfel încât runtime-ul nu poate trage nicio concluzie. Remarcați că până și exemplul simplist de mai sus necesită două apeluri runtime.GC() pentru a genera raportul. Nu va detecta absolut totul.
Totuși, va detecta clasica problemă „am uitat să anulez contextul, goroutina de lucru trăiește la nesfârșit”, care reprezintă majoritatea scurgerilor în cea mai mare parte a timpului.
Dacă importați net/http/pprof, această funcționalitate este disponibilă și la /debug/pprof/goroutineleak. Integrați acest lucru în mediul de staging, verificați-l săptămânal și fiți discret îngroziți.
Această îmbunătățire a fost adusă de Vlad Saioc de la Uber, căruia i se cuvine o băutură la alegere.
2.3. Jurnalele de depanare (Tracebacks) indică acum ce cerere a eșuat
Pentru modulele care declară Go 1.27 sau o versiune ulterioară, liniile antet ale jurnalului de depanare includ acum etichetele de goroutine din 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...
Detaliul important: etichetele sunt moștenite de goroutinele copil. Goroutina 3 nu a setat niciodată o etichetă. A primit-o de la părintele său.
Așadar, data viitoare când mediul de producție se blochează în deadlock și cineva trimite un semnal SIGQUIT procesului, în loc de 4.000 de goroutine care par toate identice, veți obține 4.000 de goroutine etichetate cu cererea și chiriașul (tenant) cărora le aparțin. Trei linii de pprof.Do la punctul de intrare al cererii vă oferă acest beneficiu.
Deoarece etichetele pot conține informații pe care ați prefera să nu le trimiteți în fluxul de erori standard (stderr), opțiunea GODEBUG=tracebacklabels=0 dezactivează această facilitate, iar această opțiune de dezactivare este concepută explicit să rămână valabilă pentru totdeauna.
2.4. asynctimerchan a dispărut definitiv
Go 1.23 a transformat canalele de tip cronometru (timer channels) în canale fără tampon (sincrone) și a oferit asynctimerchan=1 ca modalitate de revenire la comportamentul vechi. În 1.27, această setare este eliminată permanent. Canalele din pachetul time sunt sincrone, punct, fără negocieri.
Partea interesantă o reprezintă politica introdusă odată cu aceasta. O variabilă GODEBUG eliminată, lăsată în fișierul dumneavoastră go.mod, nu întrerupe automat compilarea; ea eșuează doar dacă este setată la valoarea veche:
1# go.mod conține: 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 conține: godebug asynctimerchan=0
6(compilarea reușește)
Ceea ce înseamnă că singurii oameni cărora li se reproșează ceva sunt cei care s-au bazat efectiv pe comportamentul eliminat. Toți ceilalți care au setat valoarea la cea devenită ulterior implicită și au uitat de ea își pot continua activitatea fără a mai fi nevoiți să se gândească la acest aspect. Aceasta este o piesă bine gândită de arheologie API.
3. Biblioteca Standard
3.1. encoding/json/v2: Au înlocuit motorul în timpul zborului
Aceasta este modificarea majoră. Peste doi ani de discuții pe marginea propunerii au fost în sfârșit finalizați.
Există acum trei pachete, iar înțelegerea acestei împărțiri reprezintă cea mai mare parte a provocării:
| Pachet | Rol |
|---|---|
encoding/json | API-ul v1 pe care îl cunoașteți. Comportament 100% neschimbat. Implementat acum deasupra v2 |
encoding/json/v2 | Procesare semantică. Valori Go ↔ JSON |
encoding/json/jsontext | Procesare sintactică. JSON ca flux de jetoane (token stream) |
Noutatea principală este că encoding/json a fost reconstruit pe o implementare complet diferită și se comportă în mod identic. Iată cum:
1// encoding/json.Unmarshal din Go 1.27, prescurtat
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Fiecare ciudățenie moștenită din v1 a fost codificată ca opțiune, inclusă în DefaultOptionsV1(), iar API-ul v1 aplică întotdeauna acest pachet de opțiuni. Ceea ce înseamnă că următorul cod se comportă la fel în 1.26 și 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Da, v1 acceptă în continuare în mod silențios chei duplicate și o reține pe ultima dintre ele. A făcut-o întotdeauna. O face în continuare. Compatibilitatea înseamnă compatibilitate și cu părțile mai puțin reușite.
Între timp, v2 are o serie de reguli stricte:
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
Cheile duplicate și codificările UTF-8 invalide sunt respinse. Ambele sunt cu adevărat periculoase, nu doar necurate; atunci când două compresoare sau parsere nu cad de acord asupra cărei chei duplicate i se acordă întâietate, apar vulnerabilități de securitate. CVE-2017-12635 al CouchDB a constat exact în acest lucru: un corp JSON cu două chei roles, unde validatorul a citit una, iar stratul de stocare a citit-o pe cealaltă. Respingerea reprezintă decizia corectă.
Opțiunile sunt variadice:
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// chei de mapă sortate + indentare
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// respingerea câmpurilor necunoscute – anterior necesita un Decoder și 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 – majoritatea aspectelor pe care le rezolvați anterior cu ajutorul unei biblioteci terțe sau al unei funcții MarshalJSON scrise de mână reprezintă acum simple fanioane (flags).
Iar povestea migrării este partea cea mai frumoasă. Ultimele opțiuni au prioritate, astfel încât puteți adopta nivelul de strictitudine din v2 treptat, câte un comportament pe rând:
1// păstrează semantica v1, dar respinge cheile duplicate la fel ca v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Nu trebuie să portați o bază de cod de 200.000 de linii la v2 pentru a înceta să mai acceptați chei duplicate. Modificați o singură opțiune. Pentru orice proiect de anvergură, aceasta este calea realistă.
Performanță: serializarea (marshaling) se află aproximativ la nivel de paritate, iar deserializarea (unmarshaling) este semnificativ mai rapidă. Dacă ceva nu funcționează conform așteptărilor, GOEXPERIMENT=nojsonv2 restabilește vechea implementare – iar această opțiune de revenire este și ea programată pentru eliminare în cele din urmă, așa că este preferabil să deschideți un tichet (issue) decât să vă bazați pe ea pe termen lung.
jsontext este stratul de nivel inferior: Encoder/Decoder parcurgând JSON-ul sub formă de Token-uri și Value-uri cu o mașină de stări care menține corectitudinea. Apelați la el atunci când scrieți un transformator în flux (streaming transformer) sau un filtru JSON și nu doriți deloc să materializați valori Go.
3.2. Un pachet standard pentru identificatori unici universali (uuid)
În sfârșit. RFC 9562, în biblioteca standard, fără a fi nevoie de go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: primii 48 de biți reprezintă o marcă temporală, așa că aceștia se sortează întotdeauna în ordinea creării
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}
Întregul API se rezumă la New, NewV4, NewV7, Nil, Max, Parse, MustParse și un singur tip: type UUID [16]byte. Asta este tot. Puteți citi întreaga documentație a pachetului în timp ce vi se răcește cafeaua.
Detalii care merită cunoscute:
UUIDeste de tip[16]byte, așa că operatorul==funcționează și poate fi utilizat direct ca cheie într-o mapă. Același design ca îngoogle/uuid.NilșiMaxsunt funcții, nu variabile. Deoarece o variabilăvar Nil UUIDla nivel de pachet reprezintă o armă încărcată îndreptată spre propriul picior, iar cineva, undeva, ar ajunge în cele din urmă să o suprascrie.- Bizele aleatoare provin dintr-un generator securizat criptografic.
- Implementează
encoding.TextMarshaler/TextUnmarshaler/TextAppender, așa că se integrează direct în structurile JSON. NewV7este cel pe care îl doriți pentru cheile primare din baze de date. Ordonarea temporală înseamnă că indexul de tip arbore B (B-tree index) nu se mai fragmentează, problemă de care UUID-urile v4 sunt notoriu afectate.
Acum puteți elimina o dependență. Dacă aveți nevoie de versiunile v1/v3/v5 sau de opțiuni de parsare mai avansate, bibliotecile terțe își păstrează utilitatea.
3.3. crypto/mldsa: Semnături post-cuantice, și sunt enorme
Go 1.24 ne-a oferit crypto/mlkem pentru schimbul de chei post-cuantic. Go 1.27 aduce cealaltă jumătate: semnăturile ML-DSA, standardizate ca 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
Priviți aceste numere. O singură semnătură are 3.309 octeți. Ed25519 are 64. Aceasta reprezintă o creștere de 50 de ori, iar cheia publică adaugă aproape 2 KB pe deasupra. Dacă includeți câteva astfel de elemente într-un lanț de certificate, negocierea TLS (TLS handshake) va începe să necesite propria sa strategie MTU.
Acesta este costul real al rezistenței la atacurile cuantice în prezent și motivul pentru care nimeni nu migrează totul peste noapte. Cu toate acestea, facilitatea este prezentă acum în biblioteca standard, adică exact acolo unde doriți să se afle înainte de a avea nevoie de ea.
Options.Context asigură separarea domeniilor: semnați cu aceeași cheie în scopuri diferite, utilizați un context diferit pentru fiecare, iar o semnătură dintr-un context nu va putea fi verificată în altul. Exemplul de mai sus demonstrează exact acest lucru – aceeași cheie, același mesaj, context diferit, respins.
PrivateKey implementează crypto.Signer, așa că se integrează în interfețele existente. crypto/x509 gestionează cheile și semnăturile ML-DSA, iar crypto/tls acceptă schemele de semnătură MLDSA44/MLDSA65/MLDSA87 în TLS 1.3.
De asemenea, există funcția SignDeterministic, care omite aleatorietatea – utilă pentru teste și compilări reproducerile (reproducible builds).
3.4. simd: Instrucțiuni vectoriale fără cod de asamblare
Go 1.26 a introdus pachetul specific arhitecturii simd/archsimd ca experiment. Go 1.27 adaugă simd – portabil și independent de lățimea vectorului. Se activează prin 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 // gestionează capătul rămas (tail) cu o încărcare/stocare parțială – fără buclă scalară de curățare
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]
Alegerea de design care contează: lățimea vectorului nu este niciodată hardcodată. Interogați va.Len() la momentul execuției. VectorBitSize() vă indică lățimea efectivă, Emulated() vă spune dacă beneficiați de hardware real sau de o imitație software politicoasă. Mac-ul meu a raportat NEON pe 128 de biți; o mașină AVX-512 raportează mai mult; o mașină fără suport raportează emulare, iar codul totrulează.
Funcțiile LoadFloat32sPart/StorePart merită o mențiune specială. Partea cea mai supărătoare a codului SIMD scris manual o reprezintă întotdeauna capătul neregulat de la sfârșitul matricei, iar acest mecanism îl gestionează fără a necesita o buclă scalară separată.
Facilitatea este încă experimentală, API-ul rămâne instabil, așa că nu o introduceți în mediul de producție. Totuși, faptul că acesta este cod Go și nu cod de asamblare (assembly) sau cgo reprezintă un aspect cu adevărat important.
3.5. hash/maphash.Hasher
O nouă interfață care descrie contractul dintre un tip de dată și containerele bazate pe tabele de dispersie (hash-based containers):
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
De ce? Tipul map încorporat din Go acceptă doar chei care sunt comparable (comparabile). Nu puteți utiliza o felie (slice) ca cheie. Nu puteți defini „egalitate ignorând majusculele/minusculele”. Hasher rezolvă ambele probleme simultan:
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) // aceeași sămânță, altfel nimic din toate acestea nu are sens
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Pentru semantica obișnuită a operatorului == există ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Capcana: Hasher este o interfață, nu o structură de date. Nu există încă nicio tabelă de dispersie standard sau filtru Bloom care să o consume. Aceasta este o fundație pentru un viitor pachet de containere. Astăzi o veți folosi atunci când vă construiți propria structură sau când consumați o implementare existentă, cum ar fi go/types.Hasher (care vă permite să folosiți types.Type ca cheie de mapă, respectând Identical).
Gestionarea semințelor (seeds) vă revine. Dacă funcția hashOf de mai sus ar fi generat o sămânță nouă la fiecare apel, valorile identice ar fi generat coduri hash diferite, iar exemplul ar fi afișat false – ceea ce reprezintă exact eroarea pe care am scris-o la prima mea încercare. O sămânță per container. (Sămânța este randomizată pentru a contracara atacurile de tip DoS bazate pe coliziuni hash, motiv pentru care nu este doar o constantă.)
3.6. httptest.NewTestServer + synctest.Sleep: Lovitura surpriză
Dacă adoptați un singur lucru din această lansare, asigurați-vă că acesta este.
testing/synctest a fost promovat în Go 1.25 și ne-a oferit un ceas simulat pentru testarea codului concurent. Cu toate acestea, în momentul în care testul dumneavoastră interacționa cu o rețea reală, iluzia se prăbușea. Funcția httptest.NewTestServer din Go 1.27 utilizează o rețea simulată în memorie, astfel încât bula rămâne intactă. Iar synctest.Sleep (= time.Sleep + synctest.Wait) completează acest tablou.
Iată un test pentru reîncercare cu backoff exponențial:
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-ul ar trebui să fie exact 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)
Citiți asta de două ori. A simulat trei secunde de backoff împotriva unui server HTTP real și s-a încheiat în 0,00 secunde. Iar aserțiunea este elapsed == 3*time.Second – exact trei secunde, nu „cel puțin trei secunde, cu o marjă de eroare cauzată de planificator”. Ceasurile simulate nu au variații de jitter.
synctest.Sleep există dintr-un motiv precis: dacă testul dumneavoastră se pune în așteptare (sleep) pentru aceeași durată ca și codul supus testării, cine se trezește primul este o chestiune de noroc. synctest.Sleep așteaptă și apoi blochează execuția până când fiecare altă goroutină din bulă este blocată în mod durabil, astfel încât să puteți observa sistemul după ce acesta s-a stabilizat.
Timp de expirare (timeouts), reîncercări, disjunctoare (circuit breakers), limitatoare de rată – fiecare test de client HTTP dependent de timp pe care îl dețineți poate deveni rapid și determinist. Dacă suita dumneavoastră de teste este menținută în prezent prin time.Sleep(100 * time.Millisecond) și speranță, aceasta este calea de ieșire.
3.7. Modificări net/http care afectează efectiv mediul de producție
Modificări discrete, dar care se vor reflecta în metricile dumneavoastră.
Corpurile răspunsurilor HTTP/1 se golesc automat la apelul Close. Conținutul necitit este acum drenat (până la o limită conservatoare) atunci când închideți corpul, astfel încât conexiunea să poată fi reutilizată. Ceea ce înseamnă că puteți în sfârșit să ștergeți acea incantație pe care toată lumea a copiat-o din același răspuns de pe Stack Overflow încă din 2016:
1// nu mai este necesar
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Pentru majoritatea programelor, aceasta este o operațiune neutră (no-op) sau un câștig minor. Dacă face ca lucrurile să fie mai rele, vă aflați probabil în categoria descrisă politicos în notele de lansare: Transport.MaxIdleConns setat la 0, sau un nou Client per cerere, ocolind complet limita conexiunilor inactive. Transport.DisableKeepAlives = true va ascunde problema, însă sfatul real din notele de lansare este că „o analiză aprofundată ar fi probabil benefică”, ceea ce în limbajul echipei Go înseamnă că aveți probleme mai mari.
Prioritizarea clienților HTTP/2 (RFC 9218). Serverul onorează acum semnalele de prioritate ale clientului. Dacă preferați vechea programare de tip round-robin, folosiți Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Limitează numărul de valori de antet pe care serverul le va accepta, având ca valoare implicită DefaultMaxHeaderValueCount. Încă o ușă închisă pentru scenariul „trimite zece mii de antete și vezi ce se întâmplă”.
Negociere ALPN pe conexiuni furnizate de utilizator. Dacă obiectul dumneavoastră net.Conn implementează ConnectionState() tls.ConnectionState, Transport și Server vor efectua negocierea TLS ALPN pe acesta – astfel încât dispozitivele de apelare personalizate (custom dialers) care trec printr-un proxy pot negocia în continuare HTTP/2.
3.8. Lucrurile mărunte care aduc satisfacție
strings.CutLast / bytes.CutLast. Cut efectuează împărțirea la primul separator. Nu exista nicio variantă „last” (ultimul), așa că toată lumea a implementat manual LastIndex combinat cu tăierea de felii (slicing), iar aproximativ 30% dintre noi am comis o eroare de tip off-by-one la prima încercare.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Grozav pentru extensii de fișiere și pentru parsarea șirurilor host:port – unde trebuie să găsiți ultimul două puncte, deoarece adresele IPv6 sunt pline de ele.
math/big.Int.Divide. Împărțire cu un mod de rotunjire explicit, înlocuind ghicitul de tip „oare Quo sau Div îmi trebuie?”:
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
Dacă lucrați într-un domeniu în care regula de rotunjire este stipulată într-o reglementare, această funcție vă este adresată.
url.URL.Clone și url.Values.Clone. Copii profunde (deep copies). Vechea copiere superficială prin *u partaja pointerul Userinfo, ceea ce producea exact genul de eroare care necesită o zi întreagă pentru a fi descoperită și cinci secunde pentru a fi remediată.
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
Metodele de citire pentru net.UnixConn returnează acum io.EOF în mod direct, în loc să îl ambaleze în net.OpError. Verificarea err == io.EOF funcționează acum așa cum ar fi trebuit să o facă dintotdeauna.
database/sql.ConvertAssign și driver.RowsColumnScanner. Funcționalități destinate autorilor de drivere. Prima expune conversiile de tip pe care le efectuează Rows.Scan; a doua le permite driverelor să scaneze direct în destinațiile utilizatorului, ocolind o alocare intermediară.
unicode 15 → 17. Două versiuni dintr-un singur salt. Clasificarea șirurilor și comportamentul de normalizare se pot modifica subtil. Dacă aveți teste care depind de acest aspect, veți afla în curând.
compress/flate a devenit mai rapid – iar octeții săi de ieșire pot diferi de cei din Go 1.26. Acest lucru se propagă către archive/zip, compress/gzip, compress/zlib și image/png. Dacă aveți teste bazate pe fișiere de referință (golden-file tests) care calculează hash-ul pentru ieșirea comprimată, acestea se vor defecta, iar acest lucru nu va reprezenta o regresie. Verificați acest aspect înainte de actualizare, nu în timpul analizei incidentului.
4. Lanțul de unelte (Toolchain)
4.1. go fix a integrat mai multe instrumente de modernizare
go fix se transformă în mod discret într-un instrument de modernizare a codului. Go 1.27 adaugă atomictypes, embedlit, slicesbackward și unsafefuncs. Utilizați opțiunea -diff pentru previzualizare:
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 }
Patru instrumente de modernizare au fost declanșate pe un singur fișier mic:
embedlit– rescrie literali încorporați utilizând noua sintaxă din secțiunea §1.2atomictypes–atomic.AddInt64(&x, 1)devineatomic.Int64.Add(1), ceea ce face imposibil accesul neatomic și remediază în mod silențios erorile de aliniere pe 32 de biți de care nu erați conștiențislicesbackward– buclele parcurse invers devinslices.Backwardwaitgroupgo– ritualulAdd/go/Donedevinewg.Go(redenumit din versiuneawaitgroupdin 1.26 pentru a evita ambiguitatea)
Comanda go tool fix help listează toate cele 26 de instrumente; go tool fix help <name> explică unul dintre ele. De asemenea, funcția fmtappendf a fost eliminată „din motive legate de stil”, ceea ce reprezintă o modalitate extrem de diplomatică de a descrie ceea ce s-a întâmplat în acel fir de discuție.
Rularea comenzii go fix ./... pe o bază de cod veche reprezintă o după-amiază extrem de satisfăcătoare. Citiți mai întâi diferențele (diff), evident.
4.2. go test rulează stdversion în mod implicit
Aceasta este modificarea cel mai puțin probabil să treacă neobservată în programul dumneavoastră.
Comanda go test rulează acum verificarea statică stdversion în mod implicit, semnalând simbolurile din biblioteca standard mai noi decât permite directiva go din fișierul dumneavoastră go.mod:
1// go.mod menționează: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast este un simbol din 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]
Acest comportament elimină modul clasic de eșec în care totul funcționează pe mașina dumneavoastră (cel mai recent lanț de unelte) și explodează în mediul CI sau în mediul mai vechi al unui utilizator. Dacă publicați biblioteci cu o directivă go conservatoare, aceasta reprezintă un cadou util.
4.3. go test -json a primit câmpul OutputType
Liniile "Action":"output" conțin acum un câmp opțional "OutputType": "error", "error-continue" sau "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'
Ieșirea generată de t.Log (fără câmp), ieșirea t.Error (error) și liniile generate de cadru (frame) pot fi acum distinse cu ușurință. Oricine a scris vreodată o expresie regulată (regex) pentru a deduce ce linii dintr-o ieșire de test sunt „reale” poate acum să o șteargă.
4.4. Îmbunătățiri aduse comenzii go doc
Sintaxa package@version. Puteți citi documentația pentru o versiune specifică fără a o adăuga la modulul dumneavoastră:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Util pentru a verifica ce s-a schimbat înainte de o actualizare.
Opțiunea -ex și afișarea codului sursă pentru exemple:
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
Codul sursă al exemplului și rezultatul preconizat, direct în terminal, fără a mai deschide o filă de browser care va rămâne probabil deschisă și joi.
4.5. go mod tidy curăță blocurile require
Pentru modulele care utilizează go 1.27 sau o versiune ulterioară, comanda go mod tidy comasează blocurile require dispersate în cel mult două blocuri (directe și indirecte), păstrând comentariile atașate.
1// Înainte
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// După: 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)
Remarcați faptul că comentariul // networking s-a păstrat. Această funcționalitate curăță în principal efectele rezolvării conflictelor de îmbinare Git (Git merge conflict resolution), care reprezintă momentul în care iau naștere blocurile require rătăcite. Dacă echipa dumneavoastră numără mai mult de trei persoane care adaugă dependențe, conflictele din fișierul go.mod sunt pe cale să devină considerabil mai reduse.
4.6. Diverse și detalii minore
- Suportul pentru
bzra fost eliminat. Dacă acest lucru vă afectează în mod direct, aș dori cu sinceritate să aud acea poveste. go tool trace -httpse leagă de localhost atunci când i se furnizează doar un port. Opțiunea-http=:6060nu mai ascultă pe fiecare interfață de rețea. Utilizați-http=0.0.0.0:6060dacă aceasta a fost intenția dumneavoastră. Acest comportament corespunde acum cugo tool pprofși previne activarea accidentală a unui profiler public.- Fișierele de răspuns (
@file) sunt acceptate de utilitarelecompile,link,asm,cgo,coverșipack, într-un format compatibil cu GCC. Utile pentru sistemele de compilare care depășesc limitele de lungime ale liniei de comandă – bun găsit, Bazel.
5. Compilator, Linker, Platforme suportate (Ports)
Compilatorul. Numele de fișiere relative din directivele //line se rezolvă acum în raport cu directorul fișierului care le conține, conformându-se comportamentului din go/scanner. Relevant dacă scrieți generatoare de cod.
Numele simbolurilor pentru literali de funcție (clojures) sunt de asemenea mai simple acum – același nume indiferent de inline-uire, iar instanțele multiple ale aceluiași literal pot partaja codul în interiorul binarului. Nicio schimbare funcțională, cu o excepție: codul care compară identitatea funcțiilor prin reflect.Value.Pointer va considera că acestea sunt „egale” mai des decât înainte. Această comparație nu a fost niciodată validă, însă dacă o folosiți, acum este momentul în care vă va induce în eroare și mai accentuat.
Linkerul. Noile opțiuni -macos și -macsdk setează versiunile de sistem de operare și SDK în comanda de încărcare LC_BUILD_VERSION pe macOS.
Platforme (Ports).
- Darwin necesită acum macOS 13 Ventura sau o versiune ulterioară, așa cum s-a anunțat în 1.26. Verificați-vă mașinile de rulare CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) a trecut la ABI-ul ELFv2. Necesită kernelul Linux 3.13+ (RHEL7 a primit un backport la 3.10). Cgo, PIE și legarea externă (external linking) sunt acum acceptate. Dacă utilizați cgo, dar aveți nevoie de un binar pur Go static, setațiCGO_ENABLED=0.
6. Lista de verificare pentru actualizare
Go 1.27 tratează compatibilitatea cu seriozitate, însă verificați următoarele aspecte înainte de a mări versiunea:
- Teste bazate pe fișiere de referință (golden-file tests) pe ieșirea comprimată.
compress/flatea schimbat codificatoarele; octeții gzip/zip/png pot diferi. - Teste care verifică șirurile mesajelor de eroare JSON. Comportamentul este identic; textul erorii nu este.
- Variabile GODEBUG eliminate în
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf– acestea determină eșecul compilării doar dacă sunt setate la valorile lor vechi. - Încălcări ale regulii
stdversion.go testdetectează acum aceste cazuri. Rulați testele din timp dacă mențineți o bibliotecă cu o directivăgoconservatoare. - Mașini de rulare CI cu macOS 12 sau mai vechi. Suportul a fost eliminat.
- Teste care verifică numele simbolurilor pentru literali de funcție, precum și comparații de funcții prin
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0sau un obiectClientper cerere. Combinat cu golirea automată a corpurilor de răspuns, acest lucru poate deveni mai lent.
Gânduri de încheiere
Go 1.27 este lansarea în care o mare parte din datoriile tehnice amânate au trebuit plătite simultan.
Metodele generice au reprezentat piesa lipsă din puzzle-ul genericului din versiunea 1.18. Selectorii de câmp pentru literali de structură au închis o problemă veche de unsprezece ani. encoding/json/v2 a reprezentat rezultatul a doi ani de discuții pe marginea propunerilor. Iar uuid a umplut un gol pe care practic fiecare proiect Go îl completa cu aceeași dependență terță parte timp de un deceniu.
Dacă doriți o ordine de priorități pentru a obține valoare reală din această versiune:
httptest.NewTestServer+synctest.Sleep– testele HTTP dependente de timp devin rapide și deterministe. Cel mai bun raport efort-beneficiu din întreaga lansare.- Profilul scurgerilor de goroutine – expuneți un punct final (endpoint) în mediul de staging și pregătiți-vă să fiți surprinși de rezultate.
- Etichetele de goroutine din jurnalele de depanare – trei linii de
pprof.Dola punctul de intrare al cererii transformă următorul jurnal de stivă generat la ora 3 dimineața într-un format lizibil. go fix ./...– lăsați instrumentul să se ocupe de modernizare.encoding/json/v2– fără grabă. Adoptați-l câte o opțiune pe rând.
Descărcați go1.27rc3 și rulați-vă suita de teste împotriva acesteia chiar acum. Orice element de pe acea listă de verificare este considerabil mai plăcut de descoperit într-o după-amiază de marți decât în timpul gestionării unui incident.
Referințe