Interfacce
In Go, un'interfaccia è un tipo astratto utilizzato per definire un insieme di firme di metodi senza fornire l'implementazione dei metodi. Il concetto fondamentale delle interfacce è descrivere il comportamento, mentre l'implementazione specifica del comportamento è fornita dai tipi che implementano l'interfaccia. Le interfacce in Go sono ampiamente utilizzate per implementare polimorfismo, accoppiamento lasco e riutilizzo del codice.
Concetti
La storia dello sviluppo delle interfacce in Go ha uno spartiacque. In Go 1.17 e versioni precedenti, la definizione ufficiale di interfaccia nel manuale di riferimento era: un insieme di metodi.
An interface type specifies a method set called its interface.
La definizione di implementazione dell'interfaccia era:
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
Tradotto: quando il method set di un tipo è un superset del method set di un'interfaccia, e un valore di quel tipo può essere memorizzato da una variabile di tipo interfaccia, allora si dice che quel tipo implementa l'interfaccia.
Tuttavia, in Go 1.18, la definizione di interfaccia è cambiata. L'interfaccia è definita come: un insieme di tipi.
An interface type defines a type set.
La definizione di implementazione dell'interfaccia è:
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
Tradotto: quando un tipo si trova nell'insieme di tipi di un'interfaccia, e un valore di quel tipo può essere memorizzato da una variabile di tipo interfaccia, allora si dice che quel tipo implementa l'interfaccia. Inoltre, sono state fornite le seguenti definizioni aggiuntive:
Si può dire che un tipo T implementa un'interfaccia I quando:
- T non è un'interfaccia ed è un elemento nell'insieme di tipi dell'interfaccia I
- T è un'interfaccia e l'insieme di tipi di T è un sottoinsieme dell'insieme di tipi dell'interfaccia I
Se T implementa un'interfaccia, anche i valori di T implementano quell'interfaccia.
Il cambiamento più grande in Go nella versione 1.18 è stata l'aggiunta dei generici. La nuova definizione di interfaccia serve i generici, ma non influisce sull'uso delle interfacce precedenti. Allo stesso tempo, le interfacce sono state divise in due categorie:
- Interfacce di Base (
Basic Interface): le interfacce che contengono solo method set sono interfacce di base - Interfacce Generiche (
General Interface): le interfacce che contengono type set sono interfacce generiche
Cos'è un method set? Un method set è un insieme di metodi. Allo stesso modo, un type set è un insieme di tipi.
TIP
Potresti trovare questo concetto oscuro e difficile da capire, ma in realtà non hai bisogno di comprendere tutto quanto sopra.
Interfacce di Base
Come menzionato in precedenza, le interfacce di base sono method set, ovvero un insieme di metodi.
Dichiarazione
Vediamo prima come appare un'interfaccia:
type Person interface {
Say(string) string
Walk(int)
}Questa è un'interfaccia Person con due metodi esposti: Walk e Say. Nelle interfacce, i nomi dei parametri delle funzioni non sono più importanti, ovviamente è consentito aggiungere nomi di parametri e nomi di valori di ritorno se lo si desidera.
Inizializzazione
Un'interfaccia da sola non può essere inizializzata, poiché è solo una specifica, non un'implementazione concreta, ma può essere dichiarata:
func main() {
var person Person
fmt.Println(person)
}Output:
<nil>Implementazione
Vediamo prima un esempio. Un'azienda edile vuole una gru di specifiche speciali, quindi fornisce le specifiche e i disegni della gru, indicando che la gru dovrebbe avere le funzioni di sollevamento e carico. L'azienda edile non è responsabile della costruzione della gru, fornisce solo una specifica, questa è un'interfaccia. Quindi l'azienda A accetta l'ordine, costruisce una gru insuperabile secondo la propria tecnologia proprietaria e la consegna all'azienda edile. L'azienda edile non si preoccupa di come sia implementata, né si preoccupa della gru insuperabile, purché possa sollevare e caricare, la usa semplicemente come una gru normale. Fornire funzionalità specifiche in base alle specifiche dell'interfaccia, questa è un'implementazione. Utilizzare le funzionalità in base alle specifiche dell'interfaccia, nascondendo l'implementazione interna, questa è la programmazione orientata alle interfacce. Dopo un po', la gru insuperabile si guasta e l'azienda A fallisce. Quindi l'azienda B costruisce una gru gigante ancora più potente in base alle specifiche. Poiché ha anche le funzioni di sollevamento e carico, può integrarsi perfettamente con la gru insuperabile senza influenzare i progressi dei lavori. L'edificio viene completato con successo. L'implementazione interna cambia ma le funzionalità rimangono invariate, senza influenzare l'uso precedente, può essere sostituita arbitrariamente, questo è il vantaggio della programmazione orientata alle interfacce.
Ora descriviamo la situazione sopra in Go:
// Interfaccia della gru
type Crane interface {
JackUp() string
Hoist() string
}
// Gru A
type CraneA struct {
work int // I campi interni diversi rappresentano dettagli interni diversi
}
func (c CraneA) Work() {
fmt.Println("Utilizzo della tecnologia A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// Gru B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("Utilizzo della tecnologia B")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // Memorizza la gru solo in base al tipo Crane
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("Edificio completato")
}
func main() {
// Utilizzo della gru A
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// Sostituzione con la gru B
company.Crane = CraneB{}
company.Build()
}Output:
Utilizzo della tecnologia A
jackup
Utilizzo della tecnologia A
hoist
Edificio completato
Utilizzo della tecnologia B
jackup
Utilizzo della tecnologia B
hoist
Edificio completatoNell'esempio sopra, si può osservare che l'implementazione dell'interfaccia è implicita, corrispondente alla definizione ufficiale di implementazione dell'interfaccia di base: il method set è un superset del method set dell'interfaccia. Quindi in Go, per implementare un'interfaccia non è necessario specificare esplicitamente quale interfaccia implementare con la parola chiave implements. Basta implementare tutti i metodi di un'interfaccia per averla implementata. Dopo aver implementato, è possibile inizializzare l'interfaccia. La struttura dell'azienda edile dichiara una variabile membro di tipo Crane, che può memorizzare tutti i valori che implementano l'interfaccia Crane. Poiché è una variabile di tipo Crane, i metodi accessibili sono solo JackUp e Hoist. Altri metodi interni come Work e Boot non sono accessibili.
Come menzionato in precedenza, qualsiasi tipo personalizzato può avere metodi, quindi secondo la definizione di implementazione, qualsiasi tipo personalizzato può implementare un'interfaccia. Di seguito sono riportati alcuni esempi speciali:
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}Il method set dell'interfaccia Man è un superset di Person, quindi anche Man implementa l'interfaccia Person. Tuttavia, questo assomiglia più a un'"ereditarietà".
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("non può camminare")
}Il tipo sottostante di Number è int. Anche se questo sembra assurdo in altri linguaggi, il method set di Number è effettivamente un superset di Person, quindi conta come implementazione.
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("non può camminare")
}
func main() {
var function Func
function = func() {
fmt.Println("fa qualcosa")
}
function()
}Allo stesso modo, anche i tipi di funzione possono implementare interfacce.
Interfaccia Vuota
type Any interface{
}L'interfaccia Any non ha method set. Secondo la definizione di implementazione, tutti i tipi sono implementazioni dell'interfaccia Any, poiché il method set di tutti i tipi è un superset dell'insieme vuoto. Quindi l'interfaccia Any può memorizzare valori di qualsiasi tipo:
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}Output:
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]Dall'output si scopre che i risultati delle due stampe sono diversi. In realtà, un'interfaccia può essere vista come una tupla composta da (val, type), dove type è il tipo concreto. Quando si chiama un metodo, verrà chiamato il valore concreto del tipo concreto.
interface{}Questa è anche un'interfaccia vuota, ma è un'interfaccia vuota anonima. Durante lo sviluppo, si usa solitamente l'interfaccia vuota anonima per indicare che accetta valori di qualsiasi tipo. Ecco un esempio:
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}Negli aggiornamenti successivi, il team ufficiale ha proposto un'altra soluzione. Per comodità, si può utilizzare any al posto di interface{}. I due sono completamente equivalenti, poiché il primo è solo un alias di tipo:
type any = interface{}Quando si confrontano interfacce vuote, si confronta il tipo sottostante. Se i tipi non corrispondono, il risultato è false. Solo allora si confrontano i valori. Ad esempio:
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}Output:
false
trueSe il tipo sottostante non è confrontabile, si verificherà un panic. Per Go, la confrontabilità dei tipi di dati built-in è la seguente:
| Tipo | Confrontabile | Base |
|---|---|---|
| Tipi numerici | Sì | Se i valori sono uguali |
| Tipo stringa | Sì | Se i valori sono uguali |
| Tipo array | Sì | Se tutti gli elementi dell'array sono uguali |
| Tipo slice | No | Non confrontabile |
| Struct | Sì | Se tutti i valori dei campi sono uguali |
| Tipo map | No | Non confrontabile |
| Channel | Sì | Se gli indirizzi sono uguali |
| Puntatore | Sì | Se gli indirizzi memorizzati nei puntatori sono uguali |
| Interfaccia | Sì | Se i dati memorizzati sottostanti sono uguali |
In Go esiste un tipo di interfaccia speciale per rappresentare tutti i tipi confrontabili, ovvero comparable:
type comparable interface{ comparable }TIP
Se si tenta di confrontare tipi non confrontabili, si verificherà un panic.
Interfacce Generiche
Le interfacce generiche servono i generici. Una volta padroneggiati i generici, si padroneggiano anche le interfacce generiche. Per ulteriori informazioni, consultare Generici.
