GoSuda

Go 1.27, ovvero come ho imparato a smettere di preoccuparmi e ad amare i metodi generici

By iwanhae views ...

Panoramica di Go 1.27

Go 1.27 arriva nell'agosto 2026, e questa volta il linguaggio stesso è cambiato. Non il tipo di cambiamento "abbiamo aggiunto una nuova funzione a slices": un cambiamento vero. I metodi generici sono finalmente disponibili. Una issue aperta da undici anni è stata chiusa. encoding/json è stato sostituito silenziosamente con un nuovo motore mentre l'aereo era in volo.

Ogni esempio riportato di seguito è stato eseguito con go1.27rc3 (darwin/arm64). Ogni blocco di output è reale, copiato e incollato, inclusi i messaggi di errore. Se qualcosa sembra strano, è perché è esattamente ciò che il compilatore ha effettivamente restituito.

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

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

1.1. Metodi generici

Dalla versione 1.18 disponiamo dei generics e, dalla versione 1.18, disponiamo anche della celebre conversazione. Procede così:

"Scriviamo semplicemente un metodo Map sul mio tipo slice..."

method must have no type parameters

"...va bene. Allora sarà una funzione a livello di package."

I metodi potevano usare soltanto i parametri di tipo dichiarati dal receiver. Un metodo non poteva introdurne di nuovi. Così ogni trasformazione generica finiva confinata nello scope del package, accanto ad altre diciassette funzioni libere con nomi che erano variazioni di MapSlice.

Go 1.27 risolve il 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 nemmeno essere generico. Una struct semplice può avere 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, ci sono due restrizioni, e contano più di quanto sembri:

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

È la seconda quella che vi morderà:

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

Quindi metodi generici e dispatch dinamico non si combinano. Non si tratta di avarizia da parte del team Go: un metodo generico ha infinite istanziazioni, mentre una tabella dei metodi di interfaccia deve essere finita e nota a tempo di compilazione. Non si può inserire una cosa infinita in una tabella finita. L'universo ha detto no.

Lettura pratica: i metodi generici servono per tipi concreti con API di tipo utility. Tutto ciò che deve nascondersi dietro un'interfaccia richiede ancora l'approccio tradizionale.

La libreria standard ne ha già approfittato. Il tipo Rand di math/rand/v2 ha ottenuto 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 a livello di package rand.N era generica, il che significava usare la sorgente globale. Ora il proprio *Rand con seme esplicito riceve la stessa comodità. Piccola cosa. Cosa bella.

1.2. Selettori di campo nei literal di struct, ovvero: la issue #9859 finalmente viene chiusa

L'embedding ti dà u.ID. L'embedding non ti dà User{ID: 1}. Ti dà invece User{Base: Base{ID: 1}}, che è il modo in cui Go ti chiede se intendevi davvero farlo.

La issue #9859 fu aperta nel 2015. Ora è stata chiusa. 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: si usa direttamente il campo promosso
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name non è un campo di Line. È un campo dell'Object incorporato. In precedenza si doveva scrivere Line{Object: Object{name: "diagonal"}, ...}.

Ora veniamo alle note a piè di pagina, che ho verificato provando effettivamente a compilare tutto.

(a) La chiave è ancora un identificatore semplice. Non si può 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

Quindi la funzionalità non è "le chiavi ora funzionano come l'accesso ai campi". È specificamente "i nomi dei campi promossi implicitamente sono consentiti". Un po' più ristretta di quanto suggerisca il titolo.

(b) Non si possono specificare contemporaneamente un campo incorporato e un campo 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. Gli si sono fornite due indicazioni contrastanti sulla stessa memoria, e il compilatore ha rifiutato di tirare a indovinare.

(c) L'embedding di puntatori non è gradito.

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 serve un puntatore da seguire, e al momento della costruzione del literal non ne esiste ancora uno. Anche questo è corretto.

La buona notizia: go fix riscriverà per voi i vecchi literal. Ne parliamo più avanti.

1.3. L'inferenza dei tipi di funzione è diventata meno arbitraria

L'inferenza dei tipi per le funzioni generiche funzionava in alcuni contesti e in altri no, senza un principio riconoscibile che spiegasse la differenza. Assegnare a una variabile? Va bene. Inserire esattamente la stessa funzione in un campo di struct? Errore del compilatore: scrivete double[int] come nel 2022.

Go 1.27 fa funzionare l'inferenza in ogni contesto in cui il tipo di destinazione è noto in modo non ambiguo:

 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: serviva double[int]
 8	a := A{double}             // 1.26: serviva double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: serviva double[int]
12
13	var fn func(float64) float64 = double  // questo 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 solo double, istanziato come func(int) int in tre punti e come func(float64) float64 in un quarto, interamente dal contesto. Se scrivete struct di opzioni o tabelle di handler piene di valori funzione, una buona quantità di rumore [T] sta per scomparire dalla vostra 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 dichiarano fino al 30% in meno sulle allocazioni inferiori a 80 byte, e circa l'1% complessivo per i programmi con molte allocazioni.

Non credo alle note di rilascio senza misurare, quindi:

 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 attivo)
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

Il 30–35% in più di velocità in un microbenchmark che non fa altro che allocare su Apple Silicon. Il che significa: questo è il caso migliore, ottenuto in condizioni di laboratorio da un benchmark progettato per far sembrare buono il numero. In un programma reale, il GC e il lavoro effettivo seppelliranno gran parte di questo guadagno. L'affermazione dell'~1% è quella onesta.

Il costo è nella dimensione del binario. Hello World:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Circa 50KB, fissi, indipendentemente dal programma. Si può disattivare con GOEXPERIMENT=nosizespecializedmalloc, ma quella via di fuga è prevista per la rimozione in Go 1.28, quindi consideratela un workaround per le segnalazioni di bug, non uno stile di vita.

2.2. Il profilo dei goroutine leak ora è realtà

Sperimentale in Go 1.26, generalmente disponibile nella 1.27. Il GOEXPERIMENT goroutineleakprofile è scomparso: funziona e basta.

 1func leak() {
 2	ch := make(chan int) // nessuno invierà mai nulla. mai.
 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, bloccate per sempre, con un nome di file e un numero di riga che indicano esattamente dove le avete create.

Il trucco alla base di questo è davvero ingegnoso: riutilizza l'analisi di raggiungibilità del garbage collector. Se la goroutine G è bloccata sulla primitiva P, e P non è raggiungibile da nessuna goroutine eseguibile (né da ciò che tali goroutine potrebbero risvegliare), allora nulla potrà mai più toccare P, quindi G non si risveglierà mai. Non è un'euristica: è una dimostrazione.

Lo stesso design comporta gratuitamente il limite: se il channel o il mutex sono raggiungibili tramite una variabile globale, o tramite una variabile locale di una goroutine ancora in esecuzione, il GC può comunque vederli, quindi il runtime non può concludere nulla. Si noti che anche il semplice esempio sopra richiede due chiamate a runtime.GC() per essere segnalato. Non catturerà tutto.

Tuttavia catturerà il classico "ho dimenticato di cancellare il context, la goroutine worker vive per sempre", che è il leak più comune nella maggior parte dei casi.

Se importate net/http/pprof, il profilo è disponibile anche su /debug/pprof/goroutineleak. Collegatelo allo staging, controllatelo ogni settimana e siate silenziosamente inorriditi.

Questo lavoro è stato contribuito da Vlad Saioc di Uber, che merita una bevanda a sua scelta.

2.3. I traceback ora dicono quale richiesta è morta

Per i moduli che dichiarano Go 1.27 o successivo, le righe di intestazione dei traceback ora includono 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 importante: le etichette vengono ereditate dalle goroutine figlie. La goroutine 3 non ha mai impostato un'etichetta. Ne ha ricevuta una dal proprio genitore.

Quindi la prossima volta che la produzione va in deadlock e qualcuno invia un SIGQUIT al processo, invece di 4.000 goroutine che sembrano tutte identiche, ottenete 4.000 goroutine etichettate con la richiesta e il tenant di appartenenza. Tre righe di pprof.Do nel punto di ingresso delle richieste vi fanno ottenere tutto questo.

Poiché le etichette possono contenere cose che preferireste non riversare su stderr, GODEBUG=tracebacklabels=0 disattiva la funzionalità, e quella possibilità di disattivazione è esplicitamente destinata a restare per sempre.

2.4. asynctimerchan è scomparso per sempre

Go 1.23 ha reso i channel dei timer non bufferizzati (sincroni) e ha offerto asynctimerchan=1 come modo per tornare al vecchio comportamento. Nella 1.27 questa impostazione è rimossa in modo permanente. I channel del package time sono sincroni, punto e basta, senza negoziazione.

La parte interessante è la politica introdotta insieme a essa. Un GODEBUG rimosso lasciato nel vostro go.mod non interrompe automaticamente la build: la interrompe solo se è impostato al 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(la build funziona)

Il che significa che le uniche persone che vengono rimproverate sono quelle che si affidavano davvero al comportamento rimosso. Tutti coloro che l'avevano impostata al valore predefinito finale e l'avevano dimenticata possono continuare a non pensarci. È un pezzo di archeologia delle API ben ponderato.

3. Libreria standard

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

Questo è il grande cambiamento. Oltre due anni di discussione sulla proposta sono finalmente approdati.

Ora ci sono tre package, e capire la suddivisione è gran parte del lavoro:

PackageCompito
encoding/jsonL'API v1 che conoscete. 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 e si comporta in modo 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 peculiarità legacy della v1 è stata codificata come opzione, raggruppata in DefaultOptionsV1(), e l'API v1 applica sempre questo pacchetto. Il che significa che questo si comporta allo stesso modo nella 1.26 e nella 1.27:

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

Sì, la v1 accetta ancora in silenzio le chiavi duplicate e prende l'ultima. L'ha sempre fatto. Lo fa ancora. La compatibilità significa compatibilità anche con le parti sbagliate.

La v2, invece, ha delle opinioni:

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 rifiutate. Entrambi sono davvero pericolosi, non solo disordinati: quando due parser non concordano su quale chiave duplicata vincere, nascono bug di sicurezza. Il CVE-2017-12635 di CouchDB fu esattamente questo: un corpo JSON con due chiavi roles, dove il validatore ne leggeva una e il livello di storage l'altra. Rifiutare è la scelta giusta.

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 delle mappe 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 i campi sconosciuti: prima 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: la maggior parte delle cose che in precedenza si risolvevano con una libreria di terze parti o con un MarshalJSON scritto a mano sono ora dei flag.

E la storia della migrazione è la parte migliore. Le opzioni successive hanno la precedenza, quindi potete adottare la rigidità della v2 un comportamento alla volta:

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

Non dovete portare una codebase da 200k righe sulla v2 per smettere di accettare chiavi duplicate. Cambiate un'opzione. Per qualsiasi progetto di grandi dimensioni, questo è il percorso realistico.

Prestazioni: il marshaling è più o meno alla pari, l'unmarshaling è significativamente più veloce. Se qualcosa va storto, GOEXPERIMENT=nojsonv2 ripristina la vecchia implementazione, e anche quella via di uscita è prima o poi destinata alla rimozione, quindi aprite una issue invece di accomodarvi.

jsontext è il livello basso: Encoder/Decoder che attraversano il JSON come Token e Value, con una macchina a stati che vi mantiene onesti. Usatelo quando scrivete un trasformatore in streaming o un filtro JSON e non volete materializzare affatto valori Go.

3.2. Un package uuid nella libreria 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 48 bit più significativi sono un timestamp, quindi questi si ordinano sempre in ordine di creazione
 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 è New, NewV4, NewV7, Nil, Max, Parse, MustParse, e un solo tipo: type UUID [16]byte. Tutto qui. Potete leggere l'intera documentazione del package mentre il caffè si raffredda.

Dettagli degni di nota:

  • UUID è [16]byte, quindi == funziona ed è utilizzabile direttamente come chiave di mappa. Stesso design di google/uuid.
  • Nil e Max sono funzioni, non variabili. Perché una var Nil UUID a livello di package è una pistola carica puntata al vostro piede, e qualcuno, da qualche parte, finirebbe prima o poi per assegnarle un valore.
  • I bit casuali provengono da un generatore crittograficamente sicuro.
  • Implementa encoding.TextMarshaler/TextUnmarshaler/TextAppender, quindi si inserisce direttamente nelle struct JSON.
  • NewV7 è quello che volete per le chiavi primarie dei database. L'ordinamento temporale fa sì che il vostro indice B-tree smetta di frammentarsi, cosa in cui gli UUID v4 sono notoriamente pessimi.

Ora potete eliminare una dipendenza. Se vi servono v1/v3/v5 o opzioni di parsing più sofisticate, le librerie di terze parti hanno ancora un ruolo.

3.3. crypto/mldsa: firme post-quantistiche, e sono enormi

Go 1.24 ci ha dato crypto/mlkem per lo scambio di chiavi post-quantistico. Go 1.27 porta l'altra metà: le 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

Guardate quei numeri. Una singola firma occupa 3.309 byte. Ed25519 ne occupa 64. È un aumento di 50 volte, e la chiave pubblica supera i 2KB in aggiunta. Se inserite alcune di queste firme in una catena di certificati, il vostro handshake TLS inizierà ad avere bisogno di una strategia propria per la MTU.

Questo è il costo reale della resistenza quantistica oggi, ed è il motivo per cui nessuno sta passando tutto subito. Ma ora è nella libreria standard, che è dove la si vuole trovare prima di averne bisogno.

Options.Context è la separazione di dominio: firmate con la stessa chiave per scopi diversi, usate un contesto diverso per ciascuno, e una firma prodotta in un contesto non verrà verificata in un altro. L'esempio sopra mostra esattamente questo: stessa chiave, stesso messaggio, contesto diverso, rifiutata.

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

C'è anche SignDeterministic, che salta la casualità, utile per i test e le build riproducibili.

3.4. simd: istruzioni vettoriali senza assembly

Go 1.26 ha introdotto come esperimento il package specifico per architettura simd/archsimd. Go 1.27 aggiunge simd, portabile e indipendente dalla larghezza del vettore. Si abilita 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 caricamento/salvataggio parziale: nessun ciclo scalare di pulizia
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 progettuale che conta: la larghezza del vettore non è mai fissata nel codice. Chiedete va.Len() a runtime. VectorBitSize() indica la larghezza effettiva, Emulated() indica se avete ottenuto hardware reale o una cauta imitazione software. Il mio Mac ha riportato NEON a 128 bit; una macchina AVX-512 ne riporta di più; una macchina senza nulla riporta emulazione e il codice funziona comunque.

LoadFloat32sPart/StorePart meritano una menzione speciale. La parte più fastidiosa del SIMD scritto a mano è sempre la coda irregolare alla fine dell'array, e questo la gestisce senza un ciclo scalare separato.

Ancora sperimentale, API ancora instabile, non mettetelo in produzione. Ma il fatto che si tratti di codice Go e non di assembly o cgo è una novità davvero importante.

3.5. hash/maphash.Hasher

Una nuova interfaccia che descrive il contratto tra un tipo 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 potete usare una slice come chiave. Non potete definire "uguale ignorando le maiuscole". Hasher risolve entrambi i problemi contemporaneamente:

 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 seme, altrimenti tutto questo 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 di == esiste ComparableHasher[T]:

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

Il tranello: Hasher è un'interfaccia, non una struttura dati. Non esiste ancora una tabella hash standard o un filtro di Bloom che la utilizzi. È una base per un futuro package di contenitori. Oggi lo usereste quando costruite una struttura personalizzata, oppure consumereste un'implementazione esistente come go/types.Hasher (che permette di usare types.Type come chiave di mappa, rispettando Identical).

La gestione del seme è affidata a voi. Se hashOf qui sopra generasse un seme nuovo a ogni chiamata, valori identici produrrebbero hash diversi e l'esempio stamperebbe false, esattamente il bug che ho scritto nel mio primo tentativo. Un seme per contenitore. (Il seme è randomizzato per neutralizzare gli attacchi DoS basati sul flooding delle hash, ed è per questo che non è semplicemente una costante.)

3.6. httptest.NewTestServer + synctest.Sleep: il colpo dello sleeper

Se adottate una sola cosa di questa release, scegliete questa.

testing/synctest è passato allo stato stabile in Go 1.25 e ci ha dato un orologio finto per testare il codice concorrente. Ma nel momento in cui il test toccava una rete reale, l'illusione crollava. httptest.NewTestServer di Go 1.27 usa una rete finta in memoria, quindi la bolla resta intatta. E 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 deve 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)

Leggetelo due volte. Ha simulato tre secondi di backoff contro un vero server HTTP e ha terminato in 0,00 secondi. E l'asserzione è elapsed == 3*time.Second: esattamente tre secondi, non "almeno tre secondi, più o meno il jitter dello scheduler". Gli orologi finti non hanno jitter.

synctest.Sleep esiste per un motivo preciso: se il vostro test dorme per la stessa durata del codice sotto test, quale dei due si risveglia per primo è una questione di fortuna. synctest.Sleep dorme e poi attende che tutte le altre goroutine della bolla siano bloccate in modo durevole, così osservate il sistema dopo che si è assestato.

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

3.7. Modifiche a net/http che influiscono davvero sulla produzione

Silenziose, ma si vedranno nelle vostre metriche.

I corpi delle risposte HTTP/1 vengono svuotati automaticamente alla chiusura. Il contenuto non letto viene ora scaricato (fino a un limite prudente) quando chiudete il body, così la connessione può essere riutilizzata. Il che significa che finalmente potete eliminare questa formula che tutti hanno copiato dalla stessa risposta di Stack Overflow fin 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 questo è un no-op o un piccolo vantaggio. Se invece peggiora le cose, probabilmente rientrate nel caso che le note di rilascio descrivono con garbo: Transport.MaxIdleConns impostato a 0, oppure un nuovo Client per ogni richiesta, che aggira del tutto il limite di connessioni inattive. Transport.DisableKeepAlives = true nasconderà il problema, ma il consiglio effettivo delle note di rilascio è che "un'analisi più approfondita sarebbe probabilmente utile", che nel gergo del team Go significa avete problemi più grossi.

Priorità HTTP/2 lato client (RFC 9218). Il server ora rispetta i segnali di priorità del client. Se preferivate la vecchia schedulazione round-robin, impostate Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Limita quanti valori di header il server accetterà, con valore predefinito DefaultMaxHeaderValueCount. Un'altra porta chiusa su "invia diecimila header e guarda cosa succede".

ALPN su connessioni fornite dall'utente. Se il vostro net.Conn implementa ConnectionState() tls.ConnectionState, Transport e Server eseguiranno la negoziazione TLS ALPN su di essa, quindi i dialer personalizzati che passano per un proxy possono comunque negoziare HTTP/2.

3.8. Le piccole novità che fanno piacere

strings.CutLast / bytes.CutLast. Cut divide al primo separatore. Non esisteva una variante "last", quindi tutti scrivevano a mano LastIndex più slicing, e circa il 30% di noi ha sbagliato di uno al primo tentativo.

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

Ottimo per le estensioni dei file e per il parsing di host:port, dove bisogna trovare l'ultimo due punti, perché gli indirizzi IPv6 ne sono pieni.

math/big.Int.Divide. Divisione con una modalità di arrotondamento esplicita, che sostituisce il lancio della moneta "mi serve Quo o 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 dominio in cui la regola di arrotondamento è scritta in un regolamento, questo fa per voi.

url.URL.Clone e url.Values.Clone. Copie profonde. La vecchia copia superficiale *u condivideva il puntatore Userinfo, producendo esattamente il tipo di bug che richiede un giorno per essere trovato e cinque secondi per essere corretto.

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

I metodi di lettura di net.UnixConn ora restituiscono direttamente io.EOF invece di incapsularlo in net.OpError. err == io.EOF funziona ora, come avrebbe sempre dovuto.

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

unicode dalla 15 alla 17. Due versioni in un solo salto. Il comportamento della classificazione e della normalizzazione delle stringhe potrebbe cambiare in modo sottile. Se avete test che dipendono da questo, lo scoprirete.

compress/flate è diventato più veloce, e i suoi byte di output potrebbero differire da quelli di Go 1.26. Questo si propaga a archive/zip, compress/gzip, compress/zlib e image/png. Se avete test con file di riferimento che calcolano l'hash dell'output compresso, si romperanno, e non sarà una regressione. Verificatelo prima di aggiornare, non durante la revisione dell'incidente.

4. Toolchain

4.1. go fix ha ricevuto più modernizzatori

go fix si sta silenziosamente 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 sono intervenuti su un piccolo file:

  • embedlit: riscrive i literal incorporati usando la nuova sintassi del §1.2
  • atomictypes: atomic.AddInt64(&x, 1) diventa atomic.Int64.Add(1), che rende impossibile l'accesso non atomico e corregge silenziosamente bug di allineamento a 32 bit che non sapevate di avere
  • slicesbackward: i cicli all'indietro diventano slices.Backward
  • waitgroupgo: il rituale Add/go/Done diventa wg.Go (rinominato da waitgroup della 1.26 per evitare ambiguità)

go tool fix help elenca tutti i 26; go tool fix help <name> ne spiega uno. Inoltre, fmtappendf è stato rimosso "per motivi stilistici", che è un modo splendidamente diplomatico per descrivere quanto accaduto in quel thread della issue.

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

4.2. go test esegue stdversion per impostazione predefinita

Questo è il cambiamento con più probabilità di interrompere la vostra giornata.

go test ora esegue per impostazione predefinita il controllo vet stdversion, segnalando i simboli della libreria standard più recenti di quanto consentito dalla direttiva go del 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 della 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 la classica modalità di fallimento in cui tutto funziona sulla vostra macchina (toolchain più recente) ed esplode in CI o nell'ambiente più vecchio di un utente. Se pubblicate librerie con una direttiva go conservativa, questo è un regalo.

4.3. go test -json ha ottenuto OutputType

Le righe "Action":"output" ora contengono un campo opzionale "OutputType": "error", "error-continue" oppure "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 (senza campo), l'output di t.Error (error) e le righe generate dal framework (frame) ora sono distinguibili. Chiunque abbia scritto una regex per capire quali righe dell'output dei test siano "reali" può ora eliminarla.

4.4. Miglioramenti di go doc

Sintassi package@version. Leggete la documentazione di una versione specifica senza aggiungerla al vostro modulo:

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

Utile per controllare cosa è cambiato prima di un aggiornamento.

-ex e stampa del sorgente degli esempi:

 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 una scheda del browser che sarà ancora aperta il prossimo giovedì.

4.5. go mod tidy ripulisce i vostri blocchi require

Per i moduli con go 1.27 o successivo, go mod tidy unisce i blocchi require sparsi in al massimo due (diretti e indiretti), preservando i commenti associati.

 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)

Notate che il commento // networking è sopravvissuto. Questo serve soprattutto a ripulire dopo la risoluzione dei conflitti di merge di Git, che è il luogo in cui nascono i blocchi require vaganti. Se il vostro team conta più di tre persone che aggiungono dipendenze, i conflitti nel vostro go.mod stanno per diventare sensibilmente più silenziosi.

4.6. Varie

  • Supporto a bzr rimosso. Se questo vi riguarda, mi piacerebbe davvero sentire la vostra storia.
  • go tool trace -http si associa a localhost quando viene specificata solo una porta. -http=:6060 non si mette più in ascolto su tutte le interfacce. Usate -http=0.0.0.0:6060 se lo intendete davvero. Questo ora è allineato a go tool pprof e previene il occasionale profiler pubblico accidentale.
  • I file di risposta (@file) sono supportati da compile, link, asm, cgo, cover e pack, in un formato compatibile con GCC. Per i sistemi di build che superano i limiti di lunghezza della riga di comando: salve, Bazel.

5. Compilatore, linker, port

Compilatore. I nomi di file relativi nelle direttive //line ora vengono risolti rispetto alla directory del file che le contiene, in accordo con go/scanner. Rilevante se scrivete generatori di codice.

Anche i nomi dei simboli dei letterali di funzione (closure) sono ora più semplici: lo stesso nome indipendentemente dall'inlining, e più istanze dello stesso letterale possono condividere il codice nel binario. Nessuna modifica funzionale, tranne questa: il codice che confronta l'identità delle funzioni tramite reflect.Value.Pointer vedrà "uguale" più spesso di prima. Quel confronto non è mai stato valido, ma se lo usate, ora è il momento in cui inizierà a mentirvi più apertamente.

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

Port.

  • Darwin ora richiede macOS 13 Ventura o successivo, come annunciato nella 1.26. Controllate i vostri runner CI.
  • PowerPC (GOOS=linux GOARCH=ppc64) è passato all'ABI ELFv2. Richiede il kernel Linux 3.13+ (RHEL7 ha fatto il backport alla 3.10). Cgo, PIE e il linking esterno sono ora supportati. Se usate cgo ma vi serve un binario statico puro Go, impostate CGO_ENABLED=0.

6. Checklist per l'aggiornamento

Go 1.27 prende sul serio la compatibilità, ma verificate questi punti prima di cambiare versione:

  • Test con file di riferimento sull'output compresso. compress/flate ha cambiato gli encoder; i byte di gzip/zip/png potrebbero differire.
  • Test che confrontano stringhe di messaggi di errore JSON. Il comportamento è identico; il testo dell'errore no.
  • GODEBUG rimossi in go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf: questi fanno fallire la build solo se impostati ai loro valori vecchi.
  • Violazioni di stdversion. go test le rileva ora. Eseguitelo presto se mantenete una libreria con una direttiva go conservativa.
  • Runner CI macOS 12 o precedenti. Il supporto è terminato.
  • Test che fanno asserzioni sui nomi dei simboli dei letterali di funzione e confronti di funzioni con reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 oppure un Client per richiesta. Insieme allo svuotamento automatico dei body delle risposte, questo potrebbe rallentare.

Considerazioni conclusive

Go 1.27 è la release in cui è arrivata insieme una grande quantità di manutenzione rimandata.

I metodi generici erano il pezzo mancante del lavoro sui generics del 1.18. I selettori di campo nei literal di struct hanno chiuso una issue aperta da undici anni. encoding/json/v2 è stato il frutto di due anni di discussione sulla proposta. E uuid ha colmato un vuoto che praticamente ogni progetto Go stava riempiendo con la stessa dipendenza di terze parti da un decennio.

Se volete un ordine di priorità per ottenere davvero valore da tutto questo:

  1. httptest.NewTestServer + synctest.Sleep: i test HTTP dipendenti dal tempo diventano veloci e deterministici. Il miglior rapporto sforzo-resa dell'intera release.
  2. Il profilo dei goroutine leak: esponete un endpoint in staging e preparatevi a sentirvi umiliati.
  3. Etichette delle goroutine nei traceback: tre righe di pprof.Do nel punto di ingresso delle richieste trasformano il vostro prossimo dump dello stack alle 3 di notte in qualcosa di leggibile.
  4. go fix ./...: lasciate che lo strumento faccia la modernizzazione.
  5. encoding/json/v2: senza fretta. Adottatelo un'opzione alla volta.

Prendete go1.27rc3 ed eseguite ora la vostra suite di test. Tutto ciò che è in quella checklist è molto più piacevole da scoprire di martedì pomeriggio che durante un incidente.

Riferimenti