Go Generics nel 2026: Type Parameters, Constraints e Domande da Colloquio

Questo articolo copre le domande essenziali sui Go generics per i colloqui tecnici del 2026, dai type parameters base ai pattern avanzati di constraints.

Go Generics nel 2026: Type Parameters, Constraints e Domande da Colloquio

Go 1.18 ha introdotto i generics nel linguaggio a marzo 2022, e da allora questa funzionalità si è evoluta attraverso Go 1.21, 1.22 e oltre. Questo articolo copre le domande essenziali sui Go generics per i colloqui tecnici del 2026, dai type parameters di base ai pattern avanzati di constraints comunemente richiesti nei colloqui.

Cosa sono i Go Generics?

I Go generics permettono di scrivere funzioni e tipi che funzionano con qualsiasi tipo di dato mantenendo la type safety a compile-time. A differenza delle interface che usano type assertions a runtime, i generics risolvono i tipi a compile-time, fornendo migliori performance e rilevamento anticipato degli errori.

Comprendere i Type Parameters in Go

I type parameters costituiscono la base dei Go generics. Un type parameter è un segnaposto per un tipo che viene specificato quando la funzione o il tipo generico viene utilizzato.

generic.gogo
// Basic generic function with type parameter T
func PrintSlice[T any](items []T) {
    for _, item := range items {
        fmt.Println(item)
    }
}

// Usage - type inference determines T automatically
func main() {
    PrintSlice([]int{1, 2, 3})        // T is int
    PrintSlice([]string{"a", "b"})     // T is string
}

La sintassi con parentesi quadre [T any] dichiara un type parameter T vincolato da any. Il compilatore Go inferisce il tipo concreto dagli argomenti, eliminando la necessità di specificare esplicitamente il tipo nella maggior parte dei casi.

Domanda Comune da Colloquio: Qual è la Differenza tra any e comparable?

Gli intervistatori chiedono frequentemente dei constraint built-in any e comparable. Comprendere le loro differenze dimostra una solida conoscenza dei generics.

constraints.gogo
// any: accepts all types (alias for interface{})
func Process[T any](value T) T {
    return value
}

// comparable: only types that support == and !=
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {  // This comparison requires comparable
            return true
        }
    }
    return false
}

// This compiles
Contains([]int{1, 2, 3}, 2)

// This fails: slices are not comparable
// Contains([][]int{{1}, {2}}, []int{1})

Il constraint comparable limita i type parameters ai tipi che supportano gli operatori di uguaglianza. Slice, map e funzioni sono escluse perché Go non definisce l'uguaglianza per questi a livello di linguaggio.

Creare Type Constraints Personalizzati con le Interface

Go utilizza le interface per definire constraint personalizzati, ampliando ciò che le funzioni generiche possono accettare. La specifica Go definisce come funzionano gli elementi di tipo all'interno delle interface di constraint.

number.gogo
// Custom constraint using type union
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// Generic function constrained to numeric types
func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v  // + operator works because all Number types support it
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))         // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))    // 4.0
}

Le type union con | specificano quali tipi esatti soddisfano il constraint. Questo approccio fornisce più controllo rispetto ad any evitando le type assertions a runtime.

Domanda da Colloquio: Spiegare l'Operatore Tilde ~ nei Constraints

L'operatore tilde ~ nei constraints corrisponde sia a un tipo che a tutti i tipi con quel tipo sottostante. Questa domanda testa la comprensione del type system di Go.

underlying.gogo
// ~int matches int and any type with int as underlying type
type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Custom type with int as underlying type
type UserID int
type Temperature int64

func Abs[T Signed](value T) T {
    if value < 0 {
        return -value
    }
    return value
}

func main() {
    var id UserID = -42
    var temp Temperature = -10
    
    fmt.Println(Abs(id))    // 42 - works because ~int matches UserID
    fmt.Println(Abs(temp))  // 10 - works because ~int64 matches Temperature
}

Senza ~, il constraint int corrisponderebbe solo al tipo esatto int, non a tipi personalizzati come UserID. La tilde espande il constraint per includere i tipi derivati, rendendo il codice generico più flessibile.

Pronto a superare i tuoi colloqui su Go?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Tipi Generici: Struct e Metodi

I tipi generici si estendono oltre le funzioni a struct e metodi. Questo pattern appare frequentemente nelle implementazioni di strutture dati.

stack.gogo
// Generic Stack type
type Stack[T any] struct {
    items []T
}

// Push adds an element to the stack
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

// Pop removes and returns the top element
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T  // Zero value for type T
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func main() {
    intStack := Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    
    val, ok := intStack.Pop()  // val=20, ok=true
}

Da notare che i metodi sui tipi generici devono ripetere il type parameter tra parentesi ma non possono introdurre nuovi type parameters. Il receiver (s *Stack[T]) lega il metodo all'istanziazione specifica di Stack.

Domanda da Colloquio: Perché i Metodi Non Possono Avere i Propri Type Parameters?

Go proibisce type parameters aggiuntivi sui metodi, una decisione di design che sorprende gli sviluppatori provenienti da linguaggi come Java o C#. La proposta dei Go generics spiega che questa limitazione esiste per mantenere il type system trattabile.

go
// This is NOT valid Go code
type Container[T any] struct {
    value T
}

// ERROR: methods cannot have type parameters
// func (c *Container[T]) Transform[U any](fn func(T) U) U {
//     return fn(c.value)
// }

// Valid alternative: use a standalone function
func Transform[T, U any](c *Container[T], fn func(T) U) U {
    return fn(c.value)
}

La soluzione utilizza funzioni top-level con più type parameters invece dei metodi. Questo design mantiene semplice il method dispatch ed evita interazioni complesse tra tipi receiver e type parameters dei metodi.

Type Inference e Constraint Inference

La type inference di Go riduce la verbosità quando si chiamano funzioni generiche. Capire quando sono richiesti argomenti di tipo espliciti aiuta a scrivere codice più pulito.

inference.gogo
func Map[T, R any](input []T, transform func(T) R) []R {
    result := make([]R, len(input))
    for i, v := range input {
        result[i] = transform(v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3}
    
    // Type inference: T=int, R=string inferred from arguments
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("%d", n)
    })
    
    // Explicit types sometimes required for complex cases
    // Map[int, string](numbers, converter)
}

La type inference lavora dagli argomenti della funzione ai type parameters. Quando il compilatore non riesce a inferire i tipi solo dagli argomenti, gli argomenti di tipo espliciti tra parentesi quadre risolvono l'ambiguità.

Package dei Constraints: cmp e slices nella Libreria Standard

Go 1.21 ha aggiunto il package cmp con il constraint Ordered e le funzioni di confronto. Il package slices dimostra codice generico idiomatico nella libreria standard.

stdlib.gogo
import (
    "cmp"
    "slices"
)

func main() {
    numbers := []int{3, 1, 4, 1, 5, 9}
    
    // slices.Sort uses cmp.Ordered constraint internally
    slices.Sort(numbers)  // [1, 1, 3, 4, 5, 9]
    
    // Binary search on sorted slice
    index, found := slices.BinarySearch(numbers, 4)
    
    // cmp.Compare returns -1, 0, or 1
    result := cmp.Compare(3, 5)  // -1
    
    // cmp.Or returns first non-zero value
    value := cmp.Or(0, 0, 42, 100)  // 42
}

La documentazione della libreria standard mostra come questi package sfruttano i generics per operazioni type-safe sui tipi ordinati. La familiarità con questi package dimostra conoscenza pratica dei generics oltre la comprensione teorica.

Domanda da Colloquio: Implementare una Cache Generica

Un esercizio comune nei colloqui chiede ai candidati di implementare una cache generica e thread-safe. Questo testa i generics, la concorrenza con il package sync e il design delle API.

cache.gogo
import (
    "sync"
    "time"
)

type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]cacheItem[V]
}

type cacheItem[V any] struct {
    value      V
    expiration time.Time
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{
        items: make(map[K]cacheItem[V]),
    }
}

func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = cacheItem[V]{
        value:      value,
        expiration: time.Now().Add(ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, exists := c.items[key]
    if !exists || time.Now().After(item.expiration) {
        var zero V
        return zero, false
    }
    return item.value, true
}

Questa implementazione usa comparable per le chiavi (requisito delle map) e any per i valori. Lo struct separato cacheItem mostra come i tipi generici possono essere annidati l'uno nell'altro. La thread safety deriva da sync.RWMutex, un pattern trattato nei colloqui sulla concorrenza di Go.

Zero Values e Type Constraints

Gestire gli zero values nel codice generico richiede la comprensione di come Go inizializza le variabili di tipi parametrizzati.

zero.gogo
// Return zero value when slice is empty
func First[T any](slice []T) T {
    if len(slice) == 0 {
        var zero T  // Zero value: 0 for int, "" for string, nil for pointers
        return zero
    }
    return slice[0]
}

// Alternative: return pointer to avoid ambiguity
func FirstOrNil[T any](slice []T) *T {
    if len(slice) == 0 {
        return nil
    }
    return &slice[0]
}

Il pattern var zero T crea uno zero value per qualsiasi tipo T. Per i tipi dove zero è un valore valido (come 0 per gli interi), la variante che restituisce un puntatore distingue tra "non trovato" e "trovato zero".

Avanzato: Combinare Più Constraints

Le funzioni generiche complesse possono richiedere che i tipi soddisfino più constraints. Go gestisce questo attraverso l'embedding delle interface.

combined.gogo
// Constraint requiring both ordering and string conversion
type Stringable interface {
    String() string
}

type OrderedStringable interface {
    cmp.Ordered
    Stringable
}

// Alternative: use type parameters with multiple constraints inline
func PrintSorted[T interface{ cmp.Ordered; fmt.Stringer }](items []T) {
    slices.Sort(items)
    for _, item := range items {
        fmt.Println(item.String())
    }
}

L'embedding delle interface combina i constraints, richiedendo che i tipi implementino tutte le interface incorporate. La sintassi inline interface{ A; B } fornisce la stessa funzionalità senza dichiarare un tipo constraint nominato.

Conclusione

  • I type parameters con [T any] permettono di scrivere codice riutilizzabile e type-safe senza reflection a runtime
  • comparable limita ai tipi che supportano == e !=, richiesto per le chiavi delle map
  • La tilde ~ corrisponde ai tipi con uno specifico tipo sottostante, espandendo la flessibilità dei constraint
  • I constraint personalizzati usano type union (int | string) per specificare i tipi esatti permessi
  • I metodi non possono avere propri type parameters; usare funzioni top-level come alternativa
  • I package della libreria standard cmp e slices dimostrano pattern generici idiomatici
  • Gli zero values via var zero T gestiscono dati vuoti o mancanti nelle funzioni generiche
  • Le strutture dati generiche thread-safe combinano i generics con le primitive di sync

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Condividi

Articoli correlati