Menu

Generics in Golang: parametri di tipo, vincoli ed esempi

I generics (Go 1.18+) permettono a una sola funzione o a un solo tipo di lavorare con molti tipi restando type-safe. Scopri i parametri di tipo, i vincoli any, comparable e cmp.Ordered, i vincoli personalizzati con ~, i tipi generici e quando non usarli.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una funzione generica

I parametri di tipo vanno tra parentesi quadre prima dei parametri normali. Ognuno ha un vincolo che dice quali tipi sono ammessi.

T e U vengono dedotti dagli argomenti, quindi raramente scrivi Map[int, string](...). L'inferenza funziona a partire dagli argomenti della funzione; non può dedurre un tipo che compare solo nel risultato. Per func Zero[T any]() T, devi scrivere Zero[int]().

Prima di Go 1.18 questa funzione andava scritta una volta per ogni tipo, oppure doveva accettare []interface{} e rinunciare alla sicurezza dei tipi.

Vincoli

Un vincolo è un'interfaccia. Limita i tipi che si possono usare e, in cambio, dice al compilatore quali operazioni sono consentite sui valori di quel tipo.

VincoloAmmetteTi permette di usare
anyogni tipoassegnazione, passaggio, memorizzazione
comparablei tipi che supportano ====, !=, chiavi di una mappa
cmp.Ordered (Go 1.21)interi, float, stringhe<, <=, >, >=, più ==
un'interfaccia con metodii tipi con quei metodichiamare i metodi
un'unione di tipi come ~int | ~float64esattamente quei tipigli operatori supportati da tutti

Con any al posto di comparable, v == target non compila: invalid operation: v == target (incomparable types in type set). È il vincolo a rendere lecito l'operatore.

Queste due funzioni esistono già: slices.Index e slices.Max. I package standard slices, maps e cmp coprono la maggior parte delle esigenze generiche quotidiane, quindi controllali prima di scriverne di tue.

Vincoli personalizzati con unioni di tipi e ~

Un vincolo può elencare tipi con |. Gli operatori consentiti sono quelli che tutti i tipi elencati supportano.

La tilde conta. ~int64 significa "qualsiasi tipo il cui tipo sottostante è int64", quindi il tipo con nome Cents viene accettato. Scritto come semplice int64, Sum([]Cents{...}) fallisce con Cents does not satisfy Number (possibly missing ~ for int64 in Number).

Le interfacce che contengono unioni di tipi si possono usare solo come vincoli, mai come tipi di variabili normali. var n Number non compila.

Il package golang.org/x/exp/constraints ha vincoli pronti come Integer, Float e Signed. È fuori dalla libreria standard, quindi l'ambiente di esecuzione di questa pagina non può importarlo; cmp.Ordered è quello standard.

Tipi generici

Anche struct, slice, mappe e altri tipi possono avere parametri di tipo. I metodi di un tipo generico usano i parametri del receiver.

Due dettagli di questo esempio ricorrono di continuo:

  • Il valore zero di T. var zero T è il modo per restituire "niente" dal codice generico. Non esiste un T{} o un nil che funzioni per ogni tipo.
  • L'istanziazione. Un tipo generico va istanziato prima dell'uso: Stack[string], non Stack. Dentro i suoi metodi, il receiver si scrive Stack[T].

Go 1.24 ha aggiunto gli alias di tipo generici: type Set[T comparable] = map[T]struct{} ora compila.

Limiti in cui ti imbatterai

  • Niente parametri di tipo sui metodi. func (s *Stack[T]) Map[U any](...) non è consentito. Usa una funzione di primo livello.
  • Niente specializzazione. Non puoi scrivere un'implementazione separata per T = string. Un type switch su any(v) dentro la funzione è la soluzione alternativa, e di solito è un indizio che i generics sono lo strumento sbagliato.
  • Niente vincoli sugli operatori per nome. Non puoi dire "qualsiasi tipo con un metodo +"; elenchi i tipi in un'unione.
  • L'accesso ai campi tramite un vincolo non funziona. Anche se ogni tipo di un'unione ha un campo ID, v.ID non è consentito. Metti invece un metodo nel vincolo.

Quando non usare i generics

I generics sono adatti ai tipi contenitore (stack, set, cache, alberi) e agli algoritmi su slice e mappe che fanno la stessa cosa per ogni tipo di elemento. Non vanno bene ovunque.

  • Un solo tipo concreto. Se la funzione viene chiamata sempre e solo con []User, una versione generica aggiunge fatica nella lettura e nient'altro.
  • Comportamento che cambia in base al tipo. Per questo esistono le interfacce. Una funzione che accetta un io.Writer è più semplice di una che accetta [T io.Writer], e funziona allo stesso modo.
  • Sostituire i parametri interfaccia. func Print[T fmt.Stringer](v T) non è meglio di func Print(v fmt.Stringer).

La linea guida del team di Go: scrivi prima il codice per un tipo specifico, e ricorri ai parametri di tipo quando ti accorgi di scrivere lo stesso codice una seconda volta cambiando solo i tipi.

Errori comuni

  • Usare any dove serve comparable o cmp.Ordered. Il compilatore rifiuta == o < su un parametro di tipo any.
  • Dimenticare ~. I tipi definiti dall'utente come type ID int non soddisfano un vincolo int semplice.
  • Restituire nil per un parametro di tipo. Non è consentito, a meno che il vincolo non limiti T a tipi simili ai puntatori. Restituisci un valore zero.
  • Scrivere una funzione generica di supporto che esiste già. Controlla prima slices, maps e cmp.

Domande frequenti

Go ha i generics?

Sì, da Go 1.18 (marzo 2022). Funzioni e tipi possono dichiarare parametri di tipo tra parentesi quadre: func Map[T, U any](s []T, f func(T) U) []U. Ogni parametro di tipo ha un vincolo, cioè un'interfaccia che descrive cosa deve supportare il tipo.

Qual è la differenza tra any e comparable nei generics di Go?

any ammette ogni tipo ma con i valori non ti lascia fare quasi nulla oltre ad assegnarli, passarli e memorizzarli. comparable ammette solo i tipi che supportano == e !=, ed è ciò che ti serve per le chiavi di una mappa o per i controlli di uguaglianza. Per < e >, usa cmp.Ordered.

Cosa significa la tilde (~) in un vincolo Go?

~T significa "qualsiasi tipo il cui tipo sottostante è T". ~int corrisponde a int e anche a type Celsius int. Senza la tilde, int in un vincolo corrisponde solo a int stesso, quindi i tipi definiti dall'utente verrebbero rifiutati.

I metodi Go possono avere parametri di tipo?

No. Un metodo può usare i parametri di tipo del suo tipo receiver (func (s *Stack[T]) Push(v T)), ma non può dichiararne di nuovi. Scrivi invece una funzione generica di primo livello: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA