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

Interfacce in Go

Le interfacce in Go sono uno dei concetti piu potenti e distintivi del linguaggio. Un’interfaccia definisce un insieme di metodi che un tipo deve implementare. A differenza di altri linguaggi, l’implementazione delle interfacce in Go e implicita: non serve dichiarare esplicitamente che un tipo implementa un’interfaccia. Basta che il tipo abbia tutti i metodi richiesti. In questa guida esploreremo tutti gli aspetti delle interfacce.

Definizione di un’Interfaccia

Un’interfaccia specifica un contratto: un insieme di firme di metodi.

package main

import (
    "fmt"
    "math"
)

type Forma interface {
    Area() float64
    Perimetro() float64
}

Qualsiasi tipo che implementa i metodi Area() e Perimetro() soddisfa automaticamente l’interfaccia Forma.

Implementazione Implicita

Non serve una dichiarazione esplicita come implements. Se un tipo ha i metodi giusti, implementa l’interfaccia.

type Rettangolo struct {
    Larghezza, Altezza float64
}

func (r Rettangolo) Area() float64 {
    return r.Larghezza * r.Altezza
}

func (r Rettangolo) Perimetro() float64 {
    return 2 * (r.Larghezza + r.Altezza)
}

type Cerchio struct {
    Raggio float64
}

func (c Cerchio) Area() float64 {
    return math.Pi * c.Raggio * c.Raggio
}

func (c Cerchio) Perimetro() float64 {
    return 2 * math.Pi * c.Raggio
}

func stampaInfo(f Forma) {
    fmt.Printf("Area: %.2f, Perimetro: %.2f\n", f.Area(), f.Perimetro())
}

func main() {
    r := Rettangolo{Larghezza: 5, Altezza: 3}
    c := Cerchio{Raggio: 4}

    stampaInfo(r) // Area: 15.00, Perimetro: 16.00
    stampaInfo(c) // Area: 50.27, Perimetro: 25.13
}

Sia Rettangolo che Cerchio implementano Forma senza dichiararlo esplicitamente.

L’Interfaccia Vuota: any e interface{}

L’interfaccia vuota non ha metodi, quindi qualsiasi tipo la soddisfa. A partire da Go 1.18, any e un alias per interface{}.

func stampaQualsiasi(v any) {
    fmt.Printf("Tipo: %T, Valore: %v\n", v, v)
}

func main() {
    stampaQualsiasi(42)
    stampaQualsiasi("ciao")
    stampaQualsiasi(true)
    stampaQualsiasi([]int{1, 2, 3})
}

Output:

Tipo: int, Valore: 42
Tipo: string, Valore: ciao
Tipo: bool, Valore: true
Tipo: []int, Valore: [1 2 3]

L’interfaccia vuota e utile quando si deve gestire valori di tipo sconosciuto, ma va usata con parsimonia perche si perde la sicurezza dei tipi.

Type Assertion

La type assertion permette di estrarre il tipo concreto da un valore di tipo interfaccia.

func elabora(v any) {
    // Type assertion semplice (puo causare panic se il tipo e sbagliato)
    // s := v.(string)

    // Type assertion sicura con comma ok
    s, ok := v.(string)
    if ok {
        fmt.Println("E una stringa:", s)
    } else {
        fmt.Println("Non e una stringa")
    }
}

func main() {
    elabora("ciao") // E una stringa: ciao
    elabora(42)      // Non e una stringa
}

La forma con due valori di ritorno non causa panic e permette di verificare il tipo in sicurezza.

Type Switch

Il type switch e una forma specializzata di switch che confronta i tipi.

func descrivi(v any) string {
    switch val := v.(type) {
    case int:
        return fmt.Sprintf("intero: %d", val)
    case float64:
        return fmt.Sprintf("decimale: %.2f", val)
    case string:
        return fmt.Sprintf("stringa: %q (lunghezza %d)", val, len(val))
    case bool:
        if val {
            return "booleano: vero"
        }
        return "booleano: falso"
    case []int:
        return fmt.Sprintf("slice di %d interi", len(val))
    case nil:
        return "nil"
    default:
        return fmt.Sprintf("tipo sconosciuto: %T", val)
    }
}

func main() {
    fmt.Println(descrivi(42))
    fmt.Println(descrivi(3.14))
    fmt.Println(descrivi("Go"))
    fmt.Println(descrivi(nil))
}

Composizione di Interfacce

Le interfacce possono essere composte includendo altre interfacce.

type Lettore interface {
    Leggi(p []byte) (n int, err error)
}

type Scrittore interface {
    Scrivi(p []byte) (n int, err error)
}

// Composizione: LettoreScrittore richiede sia Leggi che Scrivi
type LettoreScrittore interface {
    Lettore
    Scrittore
}

Questo pattern e ampiamente utilizzato nella libreria standard. Ad esempio, io.ReadWriter compone io.Reader e io.Writer.

L’Interfaccia Stringer

L’interfaccia fmt.Stringer definisce come un tipo viene convertito in stringa da fmt.Println e funzioni simili.

package main

import "fmt"

type Persona struct {
    Nome    string
    Cognome string
    Eta     int
}

// Implementa fmt.Stringer
func (p Persona) String() string {
    return fmt.Sprintf("%s %s (eta: %d)", p.Nome, p.Cognome, p.Eta)
}

func main() {
    p := Persona{Nome: "Marco", Cognome: "Rossi", Eta: 30}
    fmt.Println(p) // Marco Rossi (eta: 30)
}

L’Interfaccia error

L’interfaccia error e una delle piu importanti in Go. Ha un solo metodo: Error() string.

type ErroreValidazione struct {
    Campo    string
    Problema string
}

func (e *ErroreValidazione) Error() string {
    return fmt.Sprintf("validazione fallita per il campo %q: %s", e.Campo, e.Problema)
}

func validaEta(eta int) error {
    if eta < 0 {
        return &ErroreValidazione{
            Campo:    "eta",
            Problema: "non puo essere negativa",
        }
    }
    if eta > 150 {
        return &ErroreValidazione{
            Campo:    "eta",
            Problema: "valore non realistico",
        }
    }
    return nil
}

func main() {
    if err := validaEta(-5); err != nil {
        fmt.Println("Errore:", err)
        // Errore: validazione fallita per il campo "eta": non puo essere negativa

        // Type assertion per accedere ai dettagli
        if ve, ok := err.(*ErroreValidazione); ok {
            fmt.Println("Campo problematico:", ve.Campo)
        }
    }
}

Interfacce e Valore Nil

Un’interfaccia e nil solo se sia il tipo concreto che il valore sono nil. Un’interfaccia che contiene un puntatore nil non e essa stessa nil.

type MioErrore struct {
    Messaggio string
}

func (e *MioErrore) Error() string {
    return e.Messaggio
}

func operazione(fallisci bool) error {
    var err *MioErrore // Puntatore nil

    if fallisci {
        err = &MioErrore{Messaggio: "fallito"}
    }

    return err // ATTENZIONE: restituisce un'interfaccia non-nil!
}

func main() {
    err := operazione(false)
    if err != nil {
        fmt.Println("Questo viene stampato anche se err punta a nil!")
    }
}

La soluzione e restituire nil esplicitamente:

func operazioneCorretta(fallisci bool) error {
    if fallisci {
        return &MioErrore{Messaggio: "fallito"}
    }
    return nil // Restituisce un'interfaccia veramente nil
}

Verifica a Tempo di Compilazione

Per verificare che un tipo implementi un’interfaccia a tempo di compilazione, si usa un’assegnazione a una variabile anonima.

// Verifica che Rettangolo implementi Forma
var _ Forma = Rettangolo{}

// Verifica che *ErroreValidazione implementi error
var _ error = (*ErroreValidazione)(nil)

Se il tipo non soddisfa l’interfaccia, il compilatore segnala l’errore immediatamente.

Interfacce Piccole e Componibili

In Go, la best practice e definire interfacce piccole con pochi metodi. Questo le rende piu facili da implementare e comporre.

// Interfacce piccole e focalizzate
type Validatore interface {
    Valida() error
}

type Serializzatore interface {
    Serializza() ([]byte, error)
}

type Salvatore interface {
    Salva() error
}

// Composizione per necessita specifiche
type Elaborabile interface {
    Validatore
    Serializzatore
    Salvatore
}

Questo approccio segue il principio della segregazione delle interfacce.

Conclusione

Le interfacce in Go sono il meccanismo fondamentale per l’astrazione e il polimorfismo. L’implementazione implicita le rende estremamente flessibili e disaccoppiate. I type assertion e i type switch permettono di lavorare con i tipi concreti quando necessario. Le interfacce Stringer e error sono onnipresenti nella libreria standard. Ricorda di preferire interfacce piccole e componibili, e di fare attenzione alla trappola dell’interfaccia con valore nil sottostante.