Generics in Go
I generics (tipi generici) sono stati introdotti in Go 1.18 e rappresentano una delle aggiunte piu significative al linguaggio. Permettono di scrivere funzioni e tipi che funzionano con diversi tipi di dato senza sacrificare la sicurezza dei tipi a tempo di compilazione.
Parametri di Tipo
Un parametro di tipo e un segnaposto per un tipo concreto che viene specificato al momento dell’uso. Si dichiara tra parentesi quadre []:
func Stampa[T any](valore T) {
fmt.Println(valore)
}
func main() {
Stampa[int](42) // specifica il tipo esplicitamente
Stampa[string]("ciao") // specifica il tipo esplicitamente
Stampa(3.14) // Go inferisce il tipo float64
}
Il parametro di tipo T puo rappresentare qualsiasi tipo che soddisfa il constraint (vincolo) specificato.
Type Constraints (Vincoli di Tipo)
Un constraint definisce quali tipi sono ammessi come argomento per un parametro di tipo. I constraint sono definiti tramite interfacce:
Il Constraint any
any e un alias per interface{} e accetta qualsiasi tipo:
func Primo[T any](slice []T) T {
return slice[0]
}
Il Constraint comparable
comparable accetta solo tipi che supportano gli operatori == e !=:
func Contiene[T comparable](slice []T, elemento T) bool {
for _, v := range slice {
if v == elemento {
return true
}
}
return false
}
func main() {
numeri := []int{1, 2, 3, 4, 5}
fmt.Println(Contiene(numeri, 3)) // true
parole := []string{"go", "rust", "python"}
fmt.Println(Contiene(parole, "go")) // true
}
Constraint Personalizzati
E possibile definire constraint personalizzati usando interfacce:
type Numero interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Somma[T Numero](numeri []T) T {
var totale T
for _, n := range numeri {
totale += n
}
return totale
}
func main() {
interi := []int{1, 2, 3, 4, 5}
fmt.Println(Somma(interi)) // 15
decimali := []float64{1.1, 2.2, 3.3}
fmt.Println(Somma(decimali)) // 6.6
}
L’operatore ~ (tilde) indica che il constraint accetta anche tipi definiti dall’utente basati su quel tipo sottostante.
Type Sets (Insiemi di Tipi)
I constraint possono specificare un insieme di tipi usando l’operatore |:
type Ordinabile interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64 | ~string
}
func Minimo[T Ordinabile](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Minimo(3, 7)) // 3
fmt.Println(Minimo(3.14, 2.71)) // 2.71
fmt.Println(Minimo("abc", "def")) // abc
}
Constraint con Metodi
I constraint possono anche richiedere che il tipo implementi determinati metodi:
type Stringer interface {
String() string
}
func StampaFormattato[T Stringer](elementi []T) {
for _, e := range elementi {
fmt.Println(e.String())
}
}
E possibile combinare metodi e insiemi di tipi:
type NumeroConStringa interface {
~int | ~float64
String() string
}
Funzioni Generiche
Le funzioni generiche sono il caso d’uso piu comune dei generics:
package main
import "fmt"
// Filtra restituisce un nuovo slice con gli elementi che soddisfano il predicato
func Filtra[T any](slice []T, predicato func(T) bool) []T {
var risultato []T
for _, v := range slice {
if predicato(v) {
risultato = append(risultato, v)
}
}
return risultato
}
// Mappa applica una funzione a ogni elemento e restituisce un nuovo slice
func Mappa[T any, U any](slice []T, fn func(T) U) []U {
risultato := make([]U, len(slice))
for i, v := range slice {
risultato[i] = fn(v)
}
return risultato
}
func main() {
numeri := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
pari := Filtra(numeri, func(n int) bool {
return n%2 == 0
})
fmt.Println("Pari:", pari) // [2 4 6 8 10]
doppi := Mappa(numeri, func(n int) int {
return n * 2
})
fmt.Println("Doppi:", doppi) // [2 4 6 8 10 12 14 16 18 20]
// Mappa con cambio di tipo
stringhe := Mappa(numeri, func(n int) string {
return fmt.Sprintf("n=%d", n)
})
fmt.Println("Stringhe:", stringhe)
}
Tipi e Struct Generiche
E possibile definire tipi e struct generiche:
package main
import "fmt"
// Stack generico
type Stack[T any] struct {
elementi []T
}
func (s *Stack[T]) Push(valore T) {
s.elementi = append(s.elementi, valore)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elementi) == 0 {
var zero T
return zero, false
}
ultimo := s.elementi[len(s.elementi)-1]
s.elementi = s.elementi[:len(s.elementi)-1]
return ultimo, true
}
func (s *Stack[T]) Dimensione() int {
return len(s.elementi)
}
func main() {
// Stack di interi
stackInt := &Stack[int]{}
stackInt.Push(10)
stackInt.Push(20)
stackInt.Push(30)
if val, ok := stackInt.Pop(); ok {
fmt.Println("Pop:", val) // 30
}
// Stack di stringhe
stackStr := &Stack[string]{}
stackStr.Push("ciao")
stackStr.Push("mondo")
if val, ok := stackStr.Pop(); ok {
fmt.Println("Pop:", val) // "mondo"
}
}
Mappa Generica Ordinata
Un esempio piu avanzato con piu parametri di tipo:
type Coppia[K comparable, V any] struct {
Chiave K
Valore V
}
func ChiaviDiMappa[K comparable, V any](m map[K]V) []K {
chiavi := make([]K, 0, len(m))
for k := range m {
chiavi = append(chiavi, k)
}
return chiavi
}
func main() {
m := map[string]int{"go": 1, "rust": 2, "python": 3}
chiavi := ChiaviDiMappa(m)
fmt.Println(chiavi) // [go rust python] (ordine non garantito)
}
Il Pacchetto cmp (Go 1.21+)
A partire da Go 1.21, il pacchetto cmp della libreria standard fornisce constraint e funzioni utili per i generics:
package main
import (
"cmp"
"fmt"
"slices"
)
func main() {
numeri := []int{5, 3, 8, 1, 9, 2}
// Ordinamento generico con slices.SortFunc
slices.SortFunc(numeri, cmp.Compare[int])
fmt.Println(numeri) // [1 2 3 5 8 9]
// cmp.Or restituisce il primo valore non-zero (Go 1.22+)
risultato := cmp.Or(0, 0, 42, 100)
fmt.Println(risultato) // 42
}
Conclusione
I generics in Go offrono un modo potente per scrivere codice riutilizzabile e type-safe. Con i parametri di tipo, i constraint personalizzati e i type set, e possibile creare funzioni e strutture dati generiche mantenendo la semplicita e la leggibilita tipiche di Go. I pacchetti cmp e slices della libreria standard sfruttano ampiamente i generics, dimostrando come questa funzionalita si integri perfettamente nel linguaggio.