00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.