Generics in Go: casi d'uso e pattern
Codice riutilizzabile e sicuro dal punto di vista tipologico con i generics di Go
Generics in Go rappresentano una delle caratteristiche più significative aggiunte al linguaggio da Go 1.0. Introdotti in Go 1.18, i generics permettono di scrivere codice tipo-sicuro e riutilizzabile che funziona con diversi tipi senza sacrificare prestazioni o chiarezza del codice.
Questo articolo esplora casi d’uso pratici, pattern comuni e best practice per sfruttare i generics nei tuoi programmi Go. Se sei nuovo al Go o hai bisogno di un ripasso sui fondamenti, consulta il nostro Go Cheatsheet per costrutti linguistici essenziali e sintassi.

Comprendere i Generics in Go
I generics in Go ti permettono di scrivere funzioni e tipi parametrizzati da parametri di tipo. Questo elimina la necessità di duplicare il codice quando si ha la stessa logica che deve funzionare con diversi tipi, mantenendo la sicurezza del tipo a tempo di compilazione.
Sintassi di Base
La sintassi per i generics utilizza le parentesi quadre per dichiarare i parametri di tipo:
// Funzione generica
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
// Utilizzo
maxInt := Max(10, 20)
maxString := Max("apple", "banana")
Vincoli di Tipo
I vincoli di tipo specificano quali tipi possono essere utilizzati con il tuo codice generico:
any: Qualsiasi tipo (equivalente ainterface{})comparable: Tipi che supportano gli operatori==e!=- Vincoli di interfaccia personalizzati: Definisci i tuoi requisiti
// Utilizzo di un vincolo personalizzato
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Sum[T Numeric](numbers []T) T {
var sum T
for _, n := range numbers {
sum += n
}
return sum
}
Casi d’Uso Comuni
1. Strutture Dati Generiche
Uno dei casi d’uso più convincenti per i generics è la creazione di strutture dati riutilizzabili:
// Pila generica
type Stack[T any] struct {
items []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{items: make([]T, 0)}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Utilizzo
intStack := NewStack[int]()
intStack.Push(42)
intStack.Push(100)
stringStack := NewStack[string]()
stringStack.Push("hello")
2. Utilità per Slice
I generics rendono facile la scrittura di funzioni per la manipolazione delle slice riutilizzabili:
// Funzione Map
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Funzione Filter
func Filter[T any](slice []T, fn func(T) bool) []T {
var result []T
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Funzione Reduce
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
// Utilizzo
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
3. Utilità Generiche per Mappe
Lavorare con le mappe diventa più sicuro in termini di tipo con i generics:
// Ottenere le chiavi di una mappa come slice
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// Ottenere i valori di una mappa come slice
func Values[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// Ottenere un valore da una mappa con un valore predefinito
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
if v, ok := m[key]; ok {
return v
}
return defaultValue
}
4. Pattern Generico Option
Il pattern Option diventa più elegante con i generics:
type Option[T any] struct {
value *T
}
func Some[T any](value T) Option[T] {
return Option[T]{value: &value}
}
func None[T any]() Option[T] {
return Option[T]{value: nil}
}
func (o Option[T]) IsSome() bool {
return o.value != nil
}
func (o Option[T]) IsNone() bool {
return o.value == nil
}
func (o Option[T]) Unwrap() T {
if o.value == nil {
panic("attempted to unwrap None value")
}
return *o.value
}
func (o Option[T]) UnwrapOr(defaultValue T) T {
if o.value == nil {
return defaultValue
}
return *o.value
}
Pattern Avanzati
Composizione dei Vincoli
Puoi comporre i vincoli per creare requisiti più specifici:
type Addable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
type Multiplicable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
type Numeric interface {
Addable
Multiplicable
}
func Multiply[T Multiplicable](a, b T) T {
return a * b
}
Interfacce Generiche
Le interfacce possono essere anche generiche, abilitando potenti astrazioni:
type Repository[T any, ID comparable] interface {
FindByID(id ID) (T, error)
Save(entity T) error
Delete(id ID) error
FindAll() ([]T, error)
}
// Implementazione
type InMemoryRepository[T any, ID comparable] struct {
data map[ID]T
}
func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
return &InMemoryRepository[T, ID]{
data: make(map[ID]T),
}
}
func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
if entity, ok := r.data[id]; ok {
return entity, nil
}
var zero T
return zero, fmt.Errorf("entity not found")
}
Inferenza dei Tipi
L’inferenza dei tipi di Go spesso ti permette di omettere i parametri di tipo espliciti:
// Inferenza dei tipi in azione
numbers := []int{1, 2, 3, 4, 5}
// Non è necessario specificare [int] - Go lo inferisce
doubled := Map(numbers, func(n int) int { return n * 2 })
// Parametri di tipo espliciti quando necessari
result := Map[int, string](numbers, strconv.Itoa)
Best Practice
1. Iniziare Semplice
Non sovroutilizzare i generics. Se un’interfaccia semplice o un tipo concreto funzionerebbero, preferisci quelle per una migliore leggibilità:
// Preferisci questo per casi semplici
func Process(items []Processor) {
for _, item := range items {
item.Process()
}
}
// Troppo generico - evita
func Process[T Processor](items []T) {
for _, item := range items {
item.Process()
}
}
2. Usare Nomi di Vincoli Significativi
Nomina i tuoi vincoli chiaramente per comunicare l’intento:
// Buono
type Sortable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
// Meno chiaro
type T interface {
int | string
}
3. Documentare Vincoli Complessi
Quando i vincoli diventano complessi, aggiungi documentazione:
// Numeric rappresenta i tipi che supportano operazioni aritmetiche.
// Questo include tutti i tipi interi e a virgola mobile.
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
4. Considerare le Prestazioni
I generics vengono compilati in tipi concreti, quindi non c’è overhead a tempo di esecuzione. Tuttavia, sii attento alla dimensione del codice se istanzi molti tipi combinati:
// Ogni combinazione crea codice separato
var intStack = NewStack[int]()
var stringStack = NewStack[string]()
var floatStack = NewStack[float64]()
Applicazioni Reali
Costruttori di Query per Database
I generics sono particolarmente utili quando si costruiscono costruttori di query per database o si lavora con ORMs. Quando si lavora con database in Go, potrebbe essere utile consultare il nostro confronto tra Go ORMs per PostgreSQL per comprendere come i generics possano migliorare la sicurezza dei tipi nelle operazioni sui database.
type QueryBuilder[T any] struct {
table string
where []string
}
func (qb *QueryBuilder[T]) Where(condition string) *QueryBuilder[T] {
qb.where = append(qb.where, condition)
return qb
}
func (qb *QueryBuilder[T]) Find() ([]T, error) {
// Implementazione
return nil, nil
}
Gestione delle Configurazioni
Quando si costruiscono applicazioni CLI o sistemi di configurazione, i generics possono aiutare a creare caricatori di configurazione sicuri in termini di tipo. Se stai costruendo strumenti per riga di comando, la nostra guida su costruire applicazioni CLI con Cobra & Viper dimostra come i generics possano migliorare la gestione della configurazione.
type ConfigLoader[T any] struct {
path string
}
func (cl *ConfigLoader[T]) Load() (T, error) {
var config T
// Carica e deserializza la configurazione
return config, nil
}
Librerie Utilità
I generics brillano quando si creano librerie utilità che funzionano con diversi tipi. Ad esempio, quando si generano report o si lavora con diversi formati di dati, i generics possono fornire sicurezza dei tipi. Il nostro articolo su generare report PDF in Go mostra come i generics possano essere applicati a utilità per la generazione di report.
Codice Critico per Prestazioni
In applicazioni sensibili alle prestazioni come funzioni serverless, i generics possono aiutare a mantenere la sicurezza dei tipi senza overhead a tempo di esecuzione. Quando si considerano le scelte di linguaggio per applicazioni serverless, comprendere le caratteristiche di prestazioni è cruciale. L’analisi di prestazioni di AWS Lambda tra JavaScript, Python e Golang dimostra come le prestazioni di Go, unite ai generics, possano essere vantaggiose.
Errori Comuni
1. Sovravincolare i Tipi
Evita di rendere i vincoli troppo restrittivi quando non è necessario:
// Troppo restrittivo
func Process[T int | string](items []T) { }
// Migliore - più flessibile
func Process[T comparable](items []T) { }
2. Ignorare l’Inferenza dei Tipi
Lascia che Go inferisca i tipi quando è possibile:
// Tipi espliciti non necessari
result := Max[int](10, 20)
// Migliore - lascia che Go inferisca
result := Max(10, 20)
3. Dimenticare i Valori Zero
Ricorda che i tipi generici hanno valori zero:
func Get[T any](slice []T, index int) (T, bool) {
if index < 0 || index >= len(slice) {
var zero T // Importante: restituisci il valore zero
return zero, false
}
return slice[index], true
}
Conclusione
I generics in Go forniscono un potente modo per scrivere codice tipo-sicuro e riutilizzabile senza sacrificare prestazioni o leggibilità. Comprendendo la sintassi, i vincoli e i pattern comuni, puoi sfruttare i generics per ridurre la duplicazione del codice e migliorare la sicurezza dei tipi nelle tue applicazioni Go.
Ricorda di utilizzare i generics con giudizio: non ogni situazione richiede loro. Quando non sei sicuro, preferisci soluzioni più semplici come le interfacce o i tipi concreti. Tuttavia, quando devi lavorare con diversi tipi mantenendo la sicurezza dei tipi, i generics sono uno strumento eccellente nel tuo toolkit Go.
Mentre Go continua ad evolversi, i generics stanno diventando una funzione essenziale per costruire applicazioni moderne e sicure in termini di tipo. Che tu stia costruendo strutture dati, librerie utilità o astrazioni complesse, i generics possono aiutarti a scrivere codice più pulito e mantenibile.
Link Utili e Articoli Correlati
- Tutorial sui Generics in Go - Blog Ufficiale Go
- Proposta sui Parametri di Tipo
- Documentazione sui Generics in Go
- Effective Go - Generics
- Note sulla Release Go 1.18 - Generics
- Specifiche del Linguaggio Go - Parametri di Tipo
- Playground sui Generics in Go
- Quando Utilizzare i Generics - Blog Go
- Go Cheatsheet
- Costruire Applicazioni CLI in Go con Cobra & Viper
- Confronto tra ORMs Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Generare PDF in GO - Librerie e esempi
- Prestazioni di AWS Lambda: JavaScript vs Python vs Golang