Le interface sono un costrutto che permette di organizzare il codice in linguaggio Go. Con il loro utilizzo otterremo due risultati allo stesso tempo:
- assoceremo funzioni a delle struct;
- definiremo nuovi tipi di dato nel nostro programma grazie a
type
.
Con le interface riusciamo a mettere in pratica dei tipici meccanismi della programmazione orientata agli oggetti senza allontanarci dal paradigma procedurale adottato dal linguaggio. Vedremo a brevissimo come sarà possibile espandere le potenzialità di tipi di dato inclusi in una struct ed ottenere quello strato di astrazione così utile ad articolare funzionalità simili di tipi di dato differenti.
Definire interface
Per lavorare con le interface occorre seguire due passaggi. In primo luogo serve definire una o più funzioni che vorremo associare ad una o più struct. Poi bisogna realizzare un'implementazione per ogni funzione della interface e per ognuna delle struct a cui vorremo applicarla. È un pò come se l'interface dicesse quali funzioni determinate struct debbano possedere e le relative implementazioni dicessero cosa quelle funzioni debbano fare una volta applicate ad uno specifico tipo di dato.
Mettiamo questi concetti in pratica. Supponiamo di avere un programma che gestisca un ambiente accademico nel quale siano previsti sia docenti sia studenti. Ognuno di essi dovrà avere uno username per l'accesso ai servizi informatici messi a disposizione dall'Ateneo che sarà creato con la seguente formula: inizierà con una "s" nel caso si tratti di uno studente o una "d" per i docenti e sarà seguito dalle prime due lettere del nome in minuscolo e dalle prime due del cognome anch'esse in minuscolo. Ad esempio, se Ilenia Verdi è una studentessa avrà per username "silve" se si tratterà di una docente avrà "dilve": come si può immaginare non ci cureremo in questo caso di eventuali omonimie o aliasing di username ma ci limiteremo a sfruttare l'esempio per prendere familiarità con le interface di Go.
Queste sono le due struct:
type docente struct {
nome string
cognome string
materia string
}
type studente struct {
nome string
cognome string
anno int
}
La funzione che useremo per la generazione dello username sarà definita in un'interface e sarà di questo tipo:
type gestione_utenti interface {
username() string
}
Con tale interface abbiamo definito un nuovo tipo di dato, gestione_utenti
, che contemplerà solo una funzione
denominata username()
in grado di restituire un risultato di tipo string
: potremo naturalmente includere quante funzioni
desideriamo all'interno della interface.
L'interface contiene solo definizioni in quanto le implementazioni saranno diversificate per i vari tipi di
dato. Noi qui daremo un'implementazione per la funzione username
richiamata su un oggetto docente
ed una per la sua invocazione su un
oggetto studente
:
func (d docente) username() string {
return "d" + strings.ToLower(d.nome[:2]) + strings.ToLower(d.cognome[:2])
}
func (s studente) username() string {
return "s" + strings.ToLower(s.nome[:2]) + strings.ToLower(s.cognome[:2])
}
Come vediamo si tratta di una normale implementazione di funzione (con lo stesso nome!) tranne che per le notazioni
(d docente)
e (s studente)
che specificano a quale struct è destinata quella particolare
implementazione. A questo punto potremo invocare username
su oggetti di entrambe le tipologie ed in ogni caso verrà
chiamata l'opportuna implementazione. Il package strings
è stato importato per poter usare il metodo
ToLower
per la riduzione dei caratteri in minuscolo.
Prova di funzionamento
A questo punto mettiamo insieme i vari elementi scrivendo un esempio completo funzionante:
package main
import (
"fmt"
"strings"
)
type gestione_utenti interface {
username() string
}
type docente struct {
nome string
cognome string
materia string
}
type studente struct {
nome string
cognome string
anno int
}
func (d docente) username() string {
return "d" + strings.ToLower(d.nome[:2]) + strings.ToLower(d.cognome[:2])
}
func (s studente) username() string {
return "s" + strings.ToLower(s.nome[:2]) + strings.ToLower(s.cognome[:2])
}
func main() {
d := docente{nome: "Gino", cognome: "Rossi"}
s := studente{nome: "Ilenia", cognome: "Verdi"}
fmt.Println(d.username())
fmt.Println(s.username())
}
L'output che otterremo sarà il seguente:
dgiro
silve
dove dgiro
sarà lo username del docente Gino Rossi e silve
quello della studentessa Ilenia Verdi.
L'aspetto interessante è che adesso le struct, tipicamente composte solo da variabili, si vedranno associate delle funzioni da
invocare proprio come fossero metodi di una classe in Programmazione ad oggetti: grazie a ciò abbiamo potuto chiamare
sia d.username()
sia s.username()
, ognuno portatore della propria implementazione.