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.
| Vincolo | Ammette | Ti permette di usare |
|---|---|---|
any | ogni tipo | assegnazione, passaggio, memorizzazione |
comparable | i tipi che supportano == | ==, !=, chiavi di una mappa |
cmp.Ordered (Go 1.21) | interi, float, stringhe | <, <=, >, >=, più == |
| un'interfaccia con metodi | i tipi con quei metodi | chiamare i metodi |
un'unione di tipi come ~int | ~float64 | esattamente quei tipi | gli 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 unT{}o unnilche funzioni per ogni tipo. - L'istanziazione. Un tipo generico va istanziato prima dell'uso:
Stack[string], nonStack. Dentro i suoi metodi, il receiver si scriveStack[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 suany(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.IDnon è 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 difunc 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
anydove servecomparableocmp.Ordered. Il compilatore rifiuta==o<su un parametro di tipoany. - Dimenticare
~. I tipi definiti dall'utente cometype ID intnon soddisfano un vincolointsemplice. - Restituire
nilper 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,mapsecmp.
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].