Embedding
Che cosa e l’Embedding?
In Go non esiste il concetto di ereditarieta come in linguaggi orientati agli oggetti tradizionali. Al suo posto, Go offre l’embedding (incorporamento), un meccanismo potente basato sul principio della composizione sopra l’ereditarieta. L’embedding permette di includere un tipo all’interno di un altro, promuovendo automaticamente i suoi campi e metodi.
Struct Embedding
L’embedding di struct si ottiene dichiarando un campo senza un nome esplicito:
package main
import "fmt"
type Indirizzo struct {
Via string
Citta string
CAP string
}
func (i Indirizzo) Completo() string {
return fmt.Sprintf("%s, %s %s", i.Via, i.Citta, i.CAP)
}
type Persona struct {
Nome string
Eta int
Indirizzo // Campo embedded (senza nome)
}
func main() {
p := Persona{
Nome: "Marco",
Eta: 30,
Indirizzo: Indirizzo{
Via: "Via Roma 10",
Citta: "Milano",
CAP: "20100",
},
}
// Accesso diretto ai campi dell'Indirizzo
fmt.Println(p.Via) // Via Roma 10
fmt.Println(p.Citta) // Milano
// Accesso al metodo promosso
fmt.Println(p.Completo()) // Via Roma 10, Milano 20100
// Possiamo anche accedere esplicitamente
fmt.Println(p.Indirizzo.Via) // Via Roma 10
}
Quando embeddiamo Indirizzo in Persona, tutti i campi e i metodi di Indirizzo diventano accessibili direttamente su Persona. Questo si chiama promozione dei metodi.
Promozione dei metodi
I metodi del tipo embedded vengono promossi al tipo contenitore. Questo significa che possiamo chiamarli come se fossero definiti direttamente sul tipo esterno:
package main
import "fmt"
type Motore struct {
Potenza int
Cilindrata float64
}
func (m Motore) Avvia() {
fmt.Printf("Motore avviato: %d CV, %.1fL\n", m.Potenza, m.Cilindrata)
}
func (m Motore) Spegni() {
fmt.Println("Motore spento")
}
type Auto struct {
Marca string
Modello string
Motore // Embedding
}
func main() {
auto := Auto{
Marca: "Fiat",
Modello: "500",
Motore: Motore{
Potenza: 69,
Cilindrata: 1.2,
},
}
// Metodi promossi dal Motore
auto.Avvia() // Motore avviato: 69 CV, 1.2L
auto.Spegni() // Motore spento
}
Sovrascrivere metodi promossi
Il tipo esterno puo definire un metodo con lo stesso nome di un metodo promosso. In questo caso, il metodo del tipo esterno ha la precedenza:
package main
import "fmt"
type Base struct{}
func (b Base) Saluta() string {
return "Ciao dalla Base"
}
type Derivato struct {
Base
}
// Sovrascriviamo il metodo Saluta
func (d Derivato) Saluta() string {
return "Ciao dal Derivato"
}
func main() {
d := Derivato{}
fmt.Println(d.Saluta()) // Ciao dal Derivato
fmt.Println(d.Base.Saluta()) // Ciao dalla Base (accesso diretto)
}
Il metodo originale rimane comunque accessibile tramite il nome esplicito del tipo embedded.
Interface Embedding
Anche le interfacce possono essere embedded in altre interfacce, creando interfacce composte:
package main
import "fmt"
type Lettore interface {
Leggi(p []byte) (n int, err error)
}
type Scrittore interface {
Scrivi(p []byte) (n int, err error)
}
// Interfaccia composta tramite embedding
type LettoreScrittore interface {
Lettore
Scrittore
}
type File struct {
nome string
dati []byte
}
func (f *File) Leggi(p []byte) (int, error) {
n := copy(p, f.dati)
fmt.Printf("Letti %d byte da %s\n", n, f.nome)
return n, nil
}
func (f *File) Scrivi(p []byte) (int, error) {
f.dati = append(f.dati, p...)
fmt.Printf("Scritti %d byte su %s\n", len(p), f.nome)
return len(p), nil
}
func elabora(rw LettoreScrittore) {
rw.Scrivi([]byte("dati"))
buf := make([]byte, 10)
rw.Leggi(buf)
}
func main() {
f := &File{nome: "test.txt"}
elabora(f)
}
Questo e esattamente il pattern usato nella libreria standard con io.Reader, io.Writer e io.ReadWriter.
Embedding di puntatori
E possibile anche embeddere puntatori a struct:
package main
import "fmt"
type Logger struct {
Prefisso string
}
func (l *Logger) Log(messaggio string) {
fmt.Printf("[%s] %s\n", l.Prefisso, messaggio)
}
type Servizio struct {
*Logger // Embedding di puntatore
Nome string
}
func main() {
logger := &Logger{Prefisso: "INFO"}
s := Servizio{
Logger: logger,
Nome: "AuthService",
}
s.Log("Servizio avviato") // [INFO] Servizio avviato
}
L’embedding di puntatori e utile quando piu struct devono condividere la stessa istanza del tipo embedded.
Embedding vs Ereditarieta
E importante capire le differenze tra l’embedding di Go e l’ereditarieta dei linguaggi OOP:
package main
import "fmt"
type Animale struct {
Nome string
}
func (a Animale) ChiSei() string {
return fmt.Sprintf("Sono un animale: %s", a.Nome)
}
type Cane struct {
Animale
Razza string
}
func main() {
c := Cane{
Animale: Animale{Nome: "Rex"},
Razza: "Pastore tedesco",
}
// ATTENZIONE: non e polimorfismo!
// Il metodo ChiSei() usa i campi di Animale, non di Cane
fmt.Println(c.ChiSei()) // Sono un animale: Rex
// In Go usiamo le interfacce per il polimorfismo
var a interface{ ChiSei() string } = c
fmt.Println(a.ChiSei()) // Sono un animale: Rex
}
Differenze chiave:
- Non c’e polimorfismo implicito: il metodo promosso non “conosce” il tipo esterno.
- Non c’e una relazione “e-un”:
Canenon “e” unAnimale, lo contiene. - Nessuna gerarchia di tipi: non esiste casting tra tipo base e derivato.
- Composizione esplicita: l’embedding e composizione, non ereditarieta.
Pattern pratici
Pattern Wrapper
type Contatore struct {
sync.Mutex
valore int
}
func (c *Contatore) Incrementa() {
c.Lock()
defer c.Unlock()
c.valore++
}
func (c *Contatore) Valore() int {
c.Lock()
defer c.Unlock()
return c.valore
}
Pattern Decorator
type RichiestaBase struct {
URL string
Metodo string
}
type RichiestaAutenticata struct {
RichiestaBase
Token string
}
type RichiestaConLog struct {
RichiestaAutenticata
LogAttivo bool
}
Conclusione
L’embedding e uno dei meccanismi piu eleganti di Go. Permette di comporre tipi complessi a partire da tipi piu semplici, promuovendo automaticamente campi e metodi. A differenza dell’ereditarieta, l’embedding favorisce la composizione e la flessibilita, rendendo il codice piu modulare e facile da mantenere. Ricordate che l’embedding non crea relazioni gerarchiche tra tipi: per il polimorfismo, Go usa le interfacce.