Menu

Struct embedding in Golang: incorporamento e metodi promossi

L'embedding mette un tipo dentro un altro senza un nome di campo, così i suoi campi e metodi vengono promossi al tipo esterno. Scopri come funziona la promozione, l'embedding di interfacce, i conflitti di nomi e perché l'embedding non è ereditarietà.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

L'embedding in un esempio

Un campo con un tipo ma senza nome è un campo incorporato. I campi e i metodi del tipo incorporato diventano raggiungibili direttamente sul tipo esterno.

Output:

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

Il nome del campo incorporato è il nome del suo tipo, User. È così che lo indichi nei letterali (User: User{...}) e quando ti serve il valore interno in sé (a.User). Un campo promosso non si può impostare con il suo nome breve in un letterale: Admin{Name: "Ana"} fallisce con unknown field Name in struct literal of type Admin.

I metodi promossi soddisfano le interfacce

La promozione aggiunge i metodi del tipo incorporato al method set del tipo esterno. Quindi il tipo esterno soddisfa ogni interfaccia che soddisfa il tipo interno.

Le regole del method set seguono il tipo incorporato. Incorporare T promuove i metodi con receiver per valore di T sia al valore esterno sia al puntatore esterno, ma i metodi con receiver puntatore di *T solo al puntatore esterno. Incorporare *T promuove entrambi gli insiemi a entrambi. Nel dubbio, incorpora il puntatore oppure usa il tipo esterno tramite un puntatore.

Incorporare un puntatore ha un costo: il puntatore può essere nil. Service{} senza Logger compila, e a quel punto svc.Log(...) va in panic.

L'embedding non è ereditarietà

L'embedding sembra una sottoclasse, ma tre cose si comportano in modo diverso rispetto a Java o Python.

Il tipo esterno non è il tipo interno. Un Admin non è uno User. Una funzione che accetta uno User non accetterà un Admin; passa a.User.

Non c'è dispatch virtuale. Un metodo promosso viene eseguito sul valore incorporato e non sa nulla del tipo esterno. Se chiama un altro metodo, chiama la versione del proprio tipo, anche quando il tipo esterno ne definisce una con lo stesso nome.

Output:

woof
The animal says ...

In un linguaggio basato sulle classi, Speak stamperebbe "woof". In Go, d.Speak() è un'abbreviazione di d.Animal.Speak(), e il receiver di quella chiamata è l'Animal. Quando vuoi un comportamento che varia a seconda del tipo, usa un'interfaccia: passa un Sounder al codice che ne ha bisogno.

Il tipo interno non sa di essere incorporato. Non esiste super. Per estendere un metodo promosso, definisci sul tipo esterno un metodo con lo stesso nome e chiama esplicitamente quello interno:

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

Conflitti di nomi e shadowing

Un campo o un metodo del tipo esterno nasconde un campo o metodo promosso con lo stesso nome, come ha fatto Dog.Sound qui sopra. Vince il nome meno profondo.

Quando due tipi incorporati alla stessa profondità promuovono lo stesso nome, quel nome diventa ambiguo. Il programma compila comunque finché nessuno usa il nome ambiguo:

Risolvi il conflitto usando il percorso completo, oppure definendo ID sul tipo esterno.

Incorporare interfacce

Le interfacce possono incorporare altre interfacce. La libreria standard costruisce così le interfacce più grandi:

type ReadWriter interface {
	Reader
	Writer
}

Anche una struct può incorporare un'interfaccia. La struct soddisfa allora quell'interfaccia tramite il valore che metti nel campo, e ridefinisci solo i metodi che ti interessano. È il pattern decorator con pochissimo codice:

La chiamata esplicita c.Reader.Read(p) è obbligatoria. Scrivere c.Read(p) dentro Read richiamerebbe se stesso all'infinito.

Incorporare un'interfaccia in un fake per i test è una scorciatoia comune: incorpori l'interfaccia grande, implementi l'unico metodo che serve al test e lasci stare il resto. Qualsiasi metodo non implementato va in panic con una dereferenziazione di puntatore nil se viene chiamato, ed è spesso quello che vuoi in un test.

Un uso reale comune: incorporare sync.Mutex

type Stats struct {
	sync.Mutex
	hits map[string]int
}

func (s *Stats) Hit(page string) {
	s.Lock()
	defer s.Unlock()
	s.hits[page]++
}

Si legge bene, ma esporta anche Lock e Unlock come parte dell'API di Stats, quindi qualsiasi chiamante può bloccare la tua struct. Per i tipi usati fuori dal pacchetto, un campo con nome (mu sync.Mutex) mantiene privato il lock. Questo compromesso vale per ogni embedding: tutto ciò che il tipo incorporato esporta diventa parte della superficie pubblica del tuo tipo.

Errori comuni

  • Aspettarsi il dispatch virtuale. I metodi promossi non chiamano mai le ridefinizioni del tipo esterno.
  • Impostare campi promossi in un letterale. Usa il nome del tipo incorporato: Admin{User: User{Name: "Ana"}}.
  • Puntatori incorporati nil. Un *T o un'interfaccia incorporati vanno impostati prima di chiamarne i metodi.
  • Superficie dell'API involontaria. L'embedding esporta sul tuo tipo tutti i metodi esportati del tipo interno.

Domande frequenti

Cos'è lo struct embedding in Go?

È la dichiarazione di un campo con solo un tipo e nessun nome: type Admin struct { User; Level int }. I campi e i metodi dello User incorporato vengono promossi, quindi a.Name e a.Greet() funzionano direttamente su un Admin. Il valore incorporato resta un campo normale, raggiungibile come a.User.

Go ha l'ereditarietà?

No. Go non ha classi né ereditarietà per sottotipi. L'embedding permette di riusare il codice tramite composizione: il tipo esterno ottiene i metodi del tipo interno, ma un Admin non è uno User. Non puoi passare un Admin dove è atteso uno User, e i metodi promossi non possono richiamare i metodi del tipo esterno. In Go il polimorfismo viene dalle interfacce.

Come si inizializza una struct incorporata in Go?

In un letterale composito, indica il campo incorporato con il nome del suo tipo: Admin{User: User{Name: "Ana"}, Level: 2}. Non puoi impostare direttamente i campi promossi nel letterale: Admin{Name: "Ana"} fallisce con unknown field Name in struct literal of type Admin.

Si può incorporare un'interfaccia in una struct in Go?

Sì. La struct soddisfa allora l'interfaccia tramite il valore incorporato, e puoi ridefinire singoli metodi. È comune per i decorator e i fake nei test. Se il campo interfaccia incorporato è nil, chiamare un metodo che non hai ridefinito va in panic con una dereferenziazione di puntatore nil.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA