GoSuda

Go 1.27, o come ho imparato a smettere di preoccuparmi e ad amare i Generic Methods

By iwanhae
views ...

Panoramica di Go 1.27

Go 1.27 arriverà nell'agosto 2026 e, per una volta, il linguaggio stesso ha subito dei cambiamenti. Non semplici modifiche come "abbiamo aggiunto una nuova funzione a slices", ma cambiamenti sostanziali. Sono stati introdotti i generic methods. Un problema aperto da undici anni è stato risolto. encoding/json è stato silenziosamente sostituito con un nuovo motore mentre il sistema era in funzione.

Ogni esempio riportato di seguito è stato eseguito su go1.27rc3 (darwin/arm64). Ogni blocco di output è reale, copiato e incollato, inclusi i messaggi di errore. Se qualcosa appare insolito, è perché corrisponde esattamente a quanto restituito dal compilatore.

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

1. Il linguaggio è cambiato (sì, davvero)

1.1. Generic Methods

Dal rilascio di Go 1.18 disponiamo dei generics e, da allora, è in corso "La Conversazione". Funziona così:

"Lasciatemi solo scrivere un metodo Map sul mio tipo slice—"

method must have no type parameters

"...va bene. Sarà una funzione a livello di package."

I metodi potevano utilizzare solo parametri di tipo dichiarati dal receiver. Il proprio metodo non poteva introdurne di nuovi. Di conseguenza, ogni trasformazione generica veniva relegata all'ambito del package, dove risiedeva accanto ad altri diciassette helper a struttura libera chiamati con variazioni di MapSlice.

Go 1.27 risolve questo problema:

 1type List[E any] []E
 2
 3// F è un parametro di tipo dichiarato dal metodo stesso
 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}

Il receiver non deve necessariamente essere generico. Una semplice struct può possedere un metodo generico:

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

Ora, prima di rifattorizzare l'intera codebase questo pomeriggio, occorre considerare due restrizioni, che hanno un peso maggiore di quanto sembri:

  1. I metodi di interfaccia non possono dichiarare parametri di tipo.
  2. I metodi generici non possono implementare metodi di interfaccia.

Il secondo punto è quello che creerà problemi:

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

Pertanto, i metodi generici e il dynamic dispatch non sono compatibili. Non si tratta di una scelta restrittiva del team di Go: un metodo generico possiede infinite istanziazioni, mentre la tabella dei metodi di un'interfaccia deve essere finita e nota al momento della compilazione. Non è possibile inserire un elemento infinito in una tabella finita. L'universo ha posto un limite.

In sintesi: i metodi generici sono destinati a tipi concreti con API di utilità. Qualsiasi elemento che debba essere nascosto dietro un'interfaccia richiede ancora l'approccio tradizionale.

La libreria standard ne ha già tratto vantaggio. Rand di math/rand/v2 ha ora un metodo generico:

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

In precedenza, solo la funzione rand.N a livello di package era generica, il che implicava l'utilizzo della sorgente globale. Ora, il proprio *Rand con seed personalizzato offre la stessa comodità. Un dettaglio minimo, ma significativo.

1.2. Selettori di campo nei letterali di struct, ovvero: il problema #9859 finalmente risolto

L'embedding permette di utilizzare u.ID. Tuttavia, non consente di scrivere User{ID: 1}. Al contrario, richiede User{Base: Base{ID: 1}}, che è il modo in cui Go verifica se l'intento è corretto.

Il problema #9859 è stato aperto nel 2015 ed è stato finalmente chiuso. Da qualche parte, un gopher che nel frattempo ha cambiato carriera due volte sta ricevendo una notifica su 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: utilizza direttamente il campo promosso
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name non è un campo di Line, bensì dell'oggetto incorporato Object. In precedenza, si scriveva Line{Object: Object{name: "diagonal"}, ...}.

Ecco le note tecniche, verificate personalmente tramite compilazione:

(a) La chiave rimane un identificatore semplice. Non è possibile scrivere un percorso di selezione arbitrario.

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

La funzionalità non consiste nel permettere che le chiavi agiscano come l'accesso ai campi, ma specificamente nel consentire l'uso di nomi di campi implicitamente promossi. Un ambito leggermente più ristretto di quanto suggerito dal titolo.

(b) Non è possibile specificare simultaneamente un campo incorporato e un elemento promosso da esso.

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

Ragionevole. Avendo fornito istruzioni contrastanti sulla stessa memoria, il compilatore ha rifiutato di tentare una supposizione.

(c) L'incorporamento di puntatori non è supportato.

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

Per seguire un puntatore è necessario un puntatore, e al momento della costruzione del letterale non ne esiste ancora uno. Anche questo è corretto.

Buona notizia: go fix riscriverà automaticamente i vecchi letterali. Maggiori dettagli a seguire.

1.3. L'inferenza del tipo di funzione è meno arbitraria

L'inferenza di tipo per le funzioni generiche funzionava in alcuni contesti e non in altri, senza un principio discernibile. L'assegnazione a una variabile? Funzionava. Inserire la stessa funzione in un campo di una struct? Errore del compilatore, richiedendo esplicitamente double[int] come nel 2022.

Go 1.27 rende l'inferenza operativa in ogni contesto in cui il tipo di destinazione è chiaramente noto:

 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: richiedeva double[int]
 8	a := A{double}             // 1.26: richiedeva double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: richiedeva double[int]
12
13	var fn func(float64) float64 = double  // ha sempre funzionato
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

Un'unica funzione double, istanziata come func(int) int in tre posizioni e func(float64) float64 in una quarta, interamente basata sul contesto. Se si utilizzano struct di opzioni o tabelle di handler composte da valori di funzione, gran parte della verbosità di [T] scomparirà dalla codebase.

2. Runtime: prestazioni gratuite e un rilevatore di leak

2.1. Allocazione specializzata per dimensione

Il compilatore ora emette chiamate a routine di allocazione specializzate per dimensione per gli oggetti piccoli. Le note di rilascio indicano fino al 30% di risparmio sulle allocazioni inferiori a 80 byte e circa l'1% complessivo per i programmi ad alta intensità di allocazione.

Non credendo alle note di rilascio senza misurazioni, ecco i dati:

 1type small struct{ a, b, c, d int }
 2
 3var sink any
 4
 5func BenchmarkAlloc(b *testing.B) {
 6	for b.Loop() {
 7		sink = &small{}
 8	}
 9}
10
11func BenchmarkSlice(b *testing.B) {
12	for b.Loop() {
13		sink = make([]byte, 48)
14	}
15}
1# default (size-specialized malloc on)
2BenchmarkAlloc-12     3000000     5.358 ns/op
3BenchmarkSlice-12     3000000    13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12     3000000     8.722 ns/op
7BenchmarkSlice-12     3000000    20.81 ns/op

Un incremento di velocità del 30–35% in un microbenchmark che esegue esclusivamente allocazioni su Apple Silicon. Si tratta del caso migliore, ottenuto in condizioni di laboratorio da un benchmark progettato per ottimizzare i risultati. In un programma reale, il garbage collector e il carico di lavoro effettivo ridurranno tale vantaggio. L'affermazione dell'1% risulta quindi quella più onesta.

Il costo è rappresentato dalle dimensioni del binario. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Circa 50KB, fissi, indipendentemente dal programma. È possibile disattivare la funzione con GOEXPERIMENT=nosizespecializedmalloc, ma questa opzione è programmata per la rimozione in Go 1.28, pertanto va considerata come una soluzione temporanea per la segnalazione di bug, non come una pratica standard.

2.2. Il profilo di goroutine leak è ora effettivo

Sperimentale in Go 1.26, disponibile in generale in 1.27. Il GOEXPERIMENT goroutineleakprofile è stato rimosso; la funzionalità è integrata.

 1func leak() {
 2	ch := make(chan int) // nessuno invierà mai nulla. nessuno.
 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

Tre goroutine, permanentemente bloccate, con indicazione di file e riga esatta del problema.

Il meccanismo alla base è ingegnoso: riutilizza l'analisi di raggiungibilità del garbage collector. Se la goroutine G è bloccata su una primitiva P, e P è irraggiungibile da qualsiasi goroutine eseguibile (o da qualsiasi cosa tali goroutine possano attivare), allora nulla potrà mai più accedere a P, quindi G non verrà mai riattivata. Non è un'euristica, è una prova.

Lo stesso design comporta una limitazione naturale: se il canale o il mutex è raggiungibile tramite una variabile globale o tramite una variabile locale di una goroutine ancora in esecuzione, il GC può ancora rilevarlo, impedendo al runtime di trarre conclusioni. Si noti che persino l'esempio semplificato sopra richiede due chiamate a runtime.GC() per essere riportato. Non cattura tutto.

Tuttavia, individuerà il classico problema del "dimenticato di cancellare il contesto, la worker goroutine vive per sempre", che costituisce la maggior parte dei leak.

Importando net/http/pprof, la funzionalità è disponibile anche su /debug/pprof/goroutineleak. Integratela negli ambienti di staging, controllatela settimanalmente e preparatevi a rimanere sorpresi.

Questo lavoro è stato contribuito da Vlad Saioc di Uber, a cui spetta un brindisi.

2.3. I traceback ora indicano quale richiesta è fallita

Per i moduli che dichiarano Go 1.27 o versioni successive, le righe di intestazione del traceback includono ora le etichette delle goroutine di 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...

Il dettaglio cruciale: le etichette sono ereditate dalle goroutine figlio. La goroutine 3 non ha mai impostato un'etichetta; l'ha ricevuta dal genitore.

Quindi, la prossima volta che il sistema va in deadlock in produzione e qualcuno invia un SIGQUIT al processo, invece di 4.000 goroutine identiche, si otterranno 4.000 goroutine etichettate con la richiesta e il tenant di appartenenza. Tre righe di pprof.Do nel punto di ingresso della richiesta offrono questo vantaggio.

Poiché le etichette possono contenere informazioni sensibili, GODEBUG=tracebacklabels=0 consente di disabilitare la funzionalità, mantenendo questa opzione in modo permanente.

2.4. asynctimerchan è definitivamente rimosso

Go 1.23 ha reso i canali dei timer non bufferizzati (sincroni) e ha offerto asynctimerchan=1 per ripristinare il comportamento precedente. In 1.27, tale impostazione è permanentemente rimossa. I canali del package time sono sincroni, punto, senza eccezioni.

L'aspetto interessante è la politica introdotta. Un GODEBUG rimosso lasciato nel proprio go.mod non interrompe automaticamente la compilazione; lo fa solo se impostato sul vecchio valore:

1# go.mod contiene: 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 contiene: godebug asynctimerchan=0
6(compilazione corretta)

Ciò significa che solo chi si affidava effettivamente al comportamento rimosso riceverà un avviso. Chiunque lo avesse impostato sul valore predefinito finale e se ne fosse dimenticato continuerà a non doversene preoccupare. Un esempio di archeologia delle API ben gestita.

3. Libreria standard

3.1. encoding/json/v2: il motore è stato sostituito durante il volo

Questa è la novità principale. Oltre due anni di discussioni sulle proposte sono giunti a compimento.

Esistono ora tre package, e comprendere la suddivisione è fondamentale:

PackageFunzione
encoding/jsonL'API v1 nota. Comportamento invariato al 100%. Ora implementata sopra la v2
encoding/json/v2Elaborazione semantica. Valori Go ↔ JSON
encoding/json/jsontextElaborazione sintattica. JSON come flusso di token

Il punto principale è che encoding/json è stato ricostruito su un'implementazione completamente diversa mantenendo un comportamento identico. Ecco come:

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

Ogni particolarità della v1 è stata codificata come un'opzione, raggruppata in DefaultOptionsV1(), e l'API v1 applica sempre tale bundle. Di conseguenza, il comportamento tra 1.26 e 1.27 resta invariato:

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

Sì, la v1 accetta ancora silenziosamente chiavi duplicate mantenendo l'ultima. Lo ha sempre fatto e continuerà a farlo. Compatibilità significa anche mantenere le parti meno corrette.

La v2, invece, ha un approccio più rigoroso:

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

Chiavi duplicate e UTF-8 non valido vengono rifiutati. Entrambi rappresentano rischi di sicurezza concreti; quando due parser non concordano su quale chiave duplicata sia valida, si creano vulnerabilità. La CVE-2017-12635 di CouchDB era esattamente questo: un corpo JSON con due chiavi roles, dove il validatore ne leggeva una e il layer di archiviazione l'altra. Il rifiuto è la scelta corretta.

Le opzioni sono variadiche:

 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// chiavi di mappa ordinate + indentazione
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// rifiuta campi sconosciuti — in precedenza richiedeva un Decoder e 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: gran parte delle esigenze precedentemente risolte con librerie di terze parti o MarshalJSON personalizzati sono ora semplici flag.

La strategia di migrazione è il punto di forza. Le opzioni successive prevalgono, permettendo di adottare il rigore della v2 un comportamento alla volta:

1// mantieni la semantica v1, ma rifiuta chiavi duplicate come nella v2
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Non è necessario convertire un'intera codebase alla v2 per smettere di accettare chiavi duplicate. Basta attivare un'opzione. Per i progetti di grandi dimensioni, questa è la via più realistica.

Prestazioni: il marshaling è sostanzialmente alla pari, l'unmarshaling è significativamente più veloce. In caso di problemi, GOEXPERIMENT=nojsonv2 ripristina la vecchia implementazione, ma si consiglia di segnalare il problema anziché fare affidamento su tale opzione, anch'essa destinata a essere rimossa.

jsontext è il layer di basso livello: Encoder/Decoder che analizzano il JSON come Token e Value con una macchina a stati. È ideale per trasformatori di streaming o filtri JSON in cui non si desidera materializzare i valori Go.

3.2. Un package uuid standard

Finalmente. RFC 9562, nella libreria standard, senza bisogno di go get.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: i primi 48 bit sono un timestamp, quindi ordinati cronologicamente
 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}

L'intera API comprende New, NewV4, NewV7, Nil, Max, Parse, MustParse e un tipo: type UUID [16]byte. Tutto qui. Si può leggere l'intera documentazione del package mentre il caffè si raffredda.

Dettagli utili:

  • UUID è [16]byte, quindi == funziona ed è utilizzabile come chiave di mappa. Lo stesso design di google/uuid.
  • Nil e Max sono funzioni, non variabili. Una var Nil UUID a livello di package sarebbe pericolosa, poiché qualcuno potrebbe assegnarvi un valore.
  • I bit casuali provengono da un generatore crittograficamente sicuro.
  • Implementa encoding.TextMarshaler/TextUnmarshaler/TextAppender, quindi si integra perfettamente nelle struct JSON.
  • NewV7 è la scelta ideale per le chiavi primarie dei database. L'ordinamento temporale evita la frammentazione dell'indice B-tree, problema noto degli UUID v4.

È possibile eliminare una dipendenza. Se servono opzioni v1/v3/v5 o parsing avanzato, le librerie di terze parti restano valide.

3.3. crypto/mldsa: firme post-quantum, ed enormi

Go 1.24 ha introdotto crypto/mlkem per lo scambio di chiavi post-quantum. Go 1.27 aggiunge l'altra metà: firme ML-DSA, standardizzate come 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

Osservate le cifre. Una singola firma è di 3.309 byte. Ed25519 ne richiede 64. Si tratta di un aumento di 50 volte, con una chiave pubblica di quasi 2KB. Inserire questi dati in una catena di certificati richiede una strategia MTU dedicata per l'handshake TLS.

Questo è il costo reale della resistenza quantistica oggi, motivo per cui la transizione non sarà immediata. Ma la presenza nella libreria standard è fondamentale prima che diventi una necessità.

Options.Context permette la separazione dei domini: firmare con la stessa chiave per scopi diversi, utilizzando un contesto differente per ciascuno, impedisce che una firma di un contesto sia valida in un altro. L'esempio mostra proprio questo: stessa chiave, stesso messaggio, contesto diverso, rifiuto.

PrivateKey implementa crypto.Signer, integrandosi nelle interfacce esistenti. crypto/x509 gestisce chiavi e firme ML-DSA, e crypto/tls supporta gli schemi MLDSA44/MLDSA65/MLDSA87 in TLS 1.3.

È presente anche SignDeterministic, che evita la casualità, utile per test e build riproducibili.

3.4. simd: istruzioni vettoriali senza assembly

Go 1.26 ha introdotto simd/archsimd come esperimento. Go 1.27 aggiunge simd, portabile e indipendente dalla larghezza del vettore. Abilitabile con 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	// gestisce la coda con un load/store parziale — senza loop di cleanup scalare
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]

La scelta di design decisiva: la larghezza del vettore non è mai hardcoded. Si interroga va.Len() a runtime. VectorBitSize() indica la larghezza effettiva, Emulated() indica se si sta usando hardware reale o un'emulazione software. Il mio Mac ha riportato 128-bit NEON; una macchina AVX-512 ne riporta di più; una macchina senza supporto riporta l'emulazione e il codice viene eseguito comunque.

LoadFloat32sPart/StorePart meritano una menzione speciale. La parte più frustrante del SIMD scritto a mano è sempre la coda finale dell'array; questa funzione la gestisce senza loop scalari separati.

Ancora sperimentale, API instabile, non utilizzabile in produzione. Ma il fatto che si tratti di codice Go e non di assembly o cgo è un passo avanti notevole.

3.5. hash/maphash.Hasher

Una nuova interfaccia che descrive il contratto tra un tipo di valore e i contenitori basati su hash:

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

Perché? La mappa integrata di Go accetta solo chiavi comparable. Non è possibile usare una slice come chiave. Non si può definire "uguale ignorando le maiuscole". Hasher risolve entrambi i problemi:

 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)   // stesso seed, altrimenti il risultato non ha senso
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

Per la semantica == ordinaria esiste ComparableHasher[T]:

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

Il limite: Hasher è un'interfaccia, non una struttura dati. Non esiste ancora una tabella hash standard o un filtro di Bloom che la utilizzi. È la base per un futuro package di contenitori. Oggi si utilizza per costruire la propria struttura o per consumare implementazioni esistenti come go/types.Hasher (che permette di usare types.Type come chiave di mappa, rispettando Identical).

La gestione del seed è a carico dell'utente. Se hashOf generasse un nuovo seed a ogni chiamata, valori identici produrrebbero hash diversi, rendendo l'esempio false. Un seed per contenitore. (Il seed è randomizzato per contrastare attacchi DoS di hash-flooding, motivo per cui non è una costante).

3.6. httptest.NewTestServer + synctest.Sleep: il successo inaspettato

Se dovete adottare una sola novità, che sia questa.

testing/synctest è stato introdotto in Go 1.25, offrendo un clock fittizio per testare codice concorrente. Tuttavia, non appena il test toccava una rete reale, l'illusione svaniva. httptest.NewTestServer di Go 1.27 utilizza una rete fittizia in-memory, mantenendo l'isolamento. synctest.Sleep (= time.Sleep + synctest.Wait) completa il quadro.

Ecco un test di retry con backoff esponenziale:

 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		// il backoff dovrebbe essere esattamente 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)

Rileggetelo. Ha simulato tre secondi di backoff contro un server HTTP reale, terminando in 0,00 secondi. E l'asserzione è elapsed == 3*time.Secondesattamente tre secondi, non "almeno tre secondi, a meno di jitter dello scheduler". I clock fittizi non hanno jitter.

synctest.Sleep esiste per una ragione precisa: se il test dorme per la stessa durata del codice sotto test, non è possibile sapere chi si sveglierà per primo. synctest.Sleep dorme e poi attende che ogni altra goroutine nel bubble sia stabilmente bloccata, permettendo di osservare il sistema una volta assestato.

Timeout, retry, circuit breaker, rate limiter: ogni test di client HTTP dipendente dal tempo può diventare veloce e deterministico. Se la vostra suite di test si basa su time.Sleep(100 * time.Millisecond) e speranza, questa è la vostra via d'uscita.

3.7. Modifiche a net/http che influenzano la produzione

Silenziose, ma visibili nelle metriche.

I corpi delle risposte HTTP/1 si svuotano automaticamente alla Close. Il contenuto non letto viene ora svuotato (fino a un limite conservativo) quando si chiude il corpo, permettendo il riutilizzo della connessione. Potete finalmente eliminare questo incantesimo copiato da Stack Overflow dal 2016:

1// non più necessario
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

Per la maggior parte dei programmi è un'operazione neutra o un piccolo vantaggio. Se peggiora le cose, probabilmente rientrate nei casi descritti nelle note di rilascio: Transport.MaxIdleConns impostato a 0 o un nuovo Client per ogni richiesta, bypassando il limite di connessioni inattive. Transport.DisableKeepAlives = true può risolvere il sintomo, ma il consiglio reale è che "un'analisi più approfondita sarebbe vantaggiosa".

Priorità del client HTTP/2 (RFC 9218). Il server ora rispetta i segnali di priorità del client. Se preferite il vecchio scheduling round-robin, impostate Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Limita il numero di valori di intestazione accettati dal server, con valore predefinito DefaultMaxHeaderValueCount. Un'altra porta chiusa agli attacchi basati sull'invio di migliaia di header.

ALPN su connessioni fornite dall'utente. Se la vostra net.Conn implementa ConnectionState() tls.ConnectionState, Transport e Server eseguiranno la negoziazione ALPN TLS; questo permette ai dialer personalizzati tramite proxy di negoziare HTTP/2.

3.8. Piccole cose che portano gioia

strings.CutLast / bytes.CutLast. Cut divide al primo separatore. Non esisteva una variante "last", spingendo molti a usare LastIndex con slicing, spesso causando errori di off-by-one.

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

Ottimo per estensioni di file e parsing di host:port, dove è necessario trovare l'ultimo due punti, dato che gli indirizzi IPv6 ne sono pieni.

math/big.Int.Divide. Divisione con modalità di arrotondamento esplicita, eliminando il dubbio tra Quo e 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

Se lavorate in un ambito in cui la regola di arrotondamento è regolamentata, questa funzione fa per voi.

url.URL.Clone e url.Values.Clone. Copie profonde (deep copy). Il vecchio *u (shallow copy) condivideva il puntatore Userinfo, causando bug difficili da individuare.

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: i metodi di lettura ora restituiscono io.EOF direttamente invece di wrapparlo in net.OpError. err == io.EOF funziona come previsto.

database/sql.ConvertAssign e driver.RowsColumnScanner. Funzionalità per autori di driver. La prima espone le conversioni di tipo eseguite da Rows.Scan; la seconda permette ai driver di scansionare direttamente nelle destinazioni dell'utente, evitando un'allocazione intermedia.

unicode 15 → 17. Due versioni in un unico salto. La classificazione delle stringhe e il comportamento di normalizzazione potrebbero variare leggermente. Se avete test dipendenti da questo, lo scoprirete.

compress/flate è più veloce — e i suoi byte di output potrebbero differire da Go 1.26. Questo si riflette su archive/zip, compress/gzip, compress/zlib e image/png. Se avete test basati su hash di output compressi, falliranno; non si tratta di una regressione. Verificate questo aspetto prima dell'aggiornamento.

4. Toolchain

4.1. go fix ha aggiunto più modernizzatori

go fix si sta trasformando in uno strumento di modernizzazione del codice. Go 1.27 aggiunge atomictypes, embedlit, slicesbackward e unsafefuncs. Usate -diff per l'anteprima:

 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 }

Quattro modernizzatori applicati a un piccolo file:

  • embedlit — riscrive letterali incorporati usando la nuova sintassi di §1.2
  • atomictypesatomic.AddInt64(&x, 1) diventa atomic.Int64.Add(1), rendendo l'accesso non atomico impossibile e risolvendo bug di allineamento a 32-bit di cui non eravate a conoscenza.
  • slicesbackward — i loop all'indietro diventano slices.Backward
  • waitgroupgo — il rituale Add/go/Done diventa wg.Go

go tool fix help elenca tutti i 26 modernizzatori; go tool fix help <name> ne spiega uno specifico. Inoltre, fmtappendf è stato rimosso "per ragioni stilistiche".

Eseguire go fix ./... su una vecchia codebase è un'attività davvero soddisfacente. Leggete il diff prima, ovviamente.

4.2. go test esegue stdversion per impostazione predefinita

Questo è il cambiamento che più probabilmente interromperà la vostra giornata.

go test ora esegue il controllo stdversion di default, segnalando simboli della libreria standard più recenti di quanto permesso dalla direttiva go nel vostro go.mod:

1// go.mod dice: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLast è un simbolo 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]

Questo elimina il classico fallimento in cui tutto funziona sulla vostra macchina (toolchain aggiornata) ma esplode in CI o in ambienti utente datati. Se pubblicate librerie con una direttiva go conservativa, questo è un dono.

4.3. go test -json ha guadagnato OutputType

Le righe "Action":"output" ora contengono un campo opzionale "OutputType": "error", "error-continue" o "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'

L'output di t.Log (nessun campo), l'output di t.Error (error) e le righe generate dal framework (frame) sono ora distinguibili. Chiunque abbia scritto una regex per capire quali righe di output fossero "reali" può ora eliminarla.

4.4. Miglioramenti a go doc

Sintassi package@version. Leggete i documenti per una versione specifica senza aggiungerla al modulo:

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

Utile per verificare cosa è cambiato prima di un upgrade.

-ex e stampa del codice sorgente di esempio:

 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

Sorgente dell'esempio e output atteso, nel terminale, senza aprire un tab del browser.

4.5. go mod tidy pulisce i blocchi require

Per i moduli su go 1.27 o successivi, go mod tidy unisce i blocchi require sparsi in un massimo di due (diretti e indiretti), preservando i commenti.

 1// Prima
 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// Dopo: 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)

Il commento // networking è sopravvissuto. Questo pulisce principalmente i residui della risoluzione dei conflitti di merge Git. Se il vostro team ha più di tre persone che aggiungono dipendenze, i conflitti nel go.mod diminuiranno sensibilmente.

4.6. Varie

  • Supporto bzr rimosso.
  • go tool trace -http si lega a localhost se fornita solo una porta. -http=:6060 non ascolta più su ogni interfaccia. Usate -http=0.0.0.0:6060 se necessario. Questo ora corrisponde a go tool pprof e previene profiler pubblici accidentali.
  • File di risposta (@file) sono supportati da compile, link, asm, cgo, cover e pack, in un formato compatibile con GCC. Per sistemi di build che superano i limiti di lunghezza della riga di comando.

5. Compilatore, Linker, Port

Compilatore. I nomi di file relativi nelle direttive //line ora vengono risolti rispetto alla directory del file contenitore. Rilevante se scrivete generatori di codice.

I nomi dei simboli dei letterali di funzione (closure) sono ora più semplici: lo stesso nome indipendentemente dall'inlining, e istanze multiple dello stesso letterale possono condividere codice nel binario. Nessun cambiamento funzionale, tranne: il codice che confronta l'identità di funzione tramite reflect.Value.Pointer vedrà "uguale" più spesso. Tale confronto non è mai stato valido, ma se lo utilizzate, ora inizierà a mentire più apertamente.

Linker. Nuove opzioni -macos e -macsdk impostano le versioni OS e SDK nel comando di caricamento LC_BUILD_VERSION di macOS.

Port.

  • Darwin ora richiede macOS 13 Ventura o successivi. Controllate i vostri runner CI.
  • PowerPC (GOOS=linux GOARCH=ppc64) è passato all'ABI ELFv2. Richiede kernel Linux 3.13+. Cgo, PIE e linking esterno sono ora supportati. Se usate cgo ma necessitate di un binario statico pure-Go, impostate CGO_ENABLED=0.

6. Checklist di aggiornamento

Go 1.27 prende sul serio la compatibilità, ma verificate questi punti:

  • Test golden-file su output compressi. compress/flate ha cambiato encoder; i byte gzip/zip/png potrebbero differire.
  • Test che corrispondono alle stringhe dei messaggi di errore JSON. Il comportamento è identico; il testo dell'errore no.
  • GODEBUG rimossi nel go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf — falliscono solo se impostati sui vecchi valori.
  • Violazioni stdversion. go test le rileva ora.
  • Runner CI macOS 12 o precedenti. Il supporto è terminato.
  • Test che asseriscono sui nomi dei simboli dei letterali di funzione, e confronti di funzione reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 o un Client per richiesta. Insieme allo svuotamento automatico dei corpi di risposta, potrebbe rallentare.

Riflessioni conclusive

Go 1.27 è la release in cui molta manutenzione differita è stata completata.

I metodi generici erano il pezzo mancante del lavoro iniziato con la 1.18. I selettori di campo per i letterali di struct hanno chiuso un problema di undici anni. encoding/json/v2 è frutto di due anni di discussioni. E uuid ha colmato una lacuna che quasi ogni progetto Go patchava con dipendenze di terze parti da un decennio.

Priorità per ottenere valore:

  1. httptest.NewTestServer + synctest.Sleep — i test HTTP diventano veloci e deterministici.
  2. Il profilo di goroutine leak — esponete un endpoint in staging e preparatevi a sorprese.
  3. Etichette traceback goroutine — tre righe di pprof.Do rendono leggibile ogni dump di stack alle 3 del mattino.
  4. go fix ./... — lasciate che lo strumento modernizzi il codice.
  5. encoding/json/v2 — nessuna fretta. Adottatelo un'opzione alla volta.

Scaricate go1.27rc3 ed eseguite la vostra suite di test. È molto meglio scoprire questi cambiamenti in un martedì pomeriggio che durante un incidente.

Riferimenti