Usare le interfacce in Go
Le interfacce in Go sono un tipo di dati usato per rappresentare il comportamento di altri tipi. Un'interfaccia può essere paragonata a un modello o un contratto che deve essere soddisfatto da un oggetto. Quando si usano le interfacce, la codebase diventa più flessibile e adattabile, perché si scrive codice non associato a una particolare implementazione. Pertanto, è possibile estendere rapidamente le funzionalità di un programma. I motivi verranno spiegati in questo modulo.
Diversamente dalle interfacce in altri linguaggi di programmazione, le interfacce in Go sono soddisfatte in modo implicito. Go non offre parole chiave per implementare un'interfaccia. Se, pertanto, si ha familiarità con le interfacce in altri linguaggi di programmazione, ma non si ha familiarità con Go, questo concetto potrebbe creare confusione.
In questo modulo verranno usati più esempi per esplorare le interfacce in Go e dimostrare come sfruttarle al meglio.
Dichiarare un'interfaccia
Un'interfaccia in Go è un progetto, un tipo astratto che include solo i metodi che deve possedere o implementare un tipo concreto.
Si immagini di voler creare un'interfaccia nel pacchetto geometry, che indichi i metodi che devono essere implementati da una figura geometrica. È possibile definire un'interfaccia simile alla seguente:
type Shape interface {
Perimeter() float64
Area() float64
}
L'interfaccia Shape
indica che qualsiasi tipo che si vuole considerare una figura geometrica (Shape
) deve avere entrambi i metodi Perimeter()
e Area()
. Ad esempio, quando si crea uno struct Square
, deve implementare entrambi i metodi e non uno solo. Si noti anche che un'interfaccia non contiene i dettagli di implementazione per tali metodi (ad esempio, per il calcolo del perimetro e dell'area di una figura geometrica). Si tratta semplicemente di un contratto. I modi per calcolare l'area e il perimetro di figure geometriche come triangoli, cerchi e quadrati sono diversi.
Implementare un'interfaccia
Come illustrato in precedenza, in Go non è presente una parola chiave per implementare un'interfaccia. Un'interfaccia in Go viene soddisfatta in modo implicito da un tipo quando include tutti i metodi richiesti da un'interfaccia.
Verrà ora creato uno struct Square
con entrambi i metodi dall'interfaccia Shape
, come illustrato nel codice di esempio seguente:
type Square struct {
size float64
}
func (s Square) Area() float64 {
return s.size * s.size
}
func (s Square) Perimeter() float64 {
return s.size * 4
}
Si noti che la firma del metodo dello struct Square
corrisponde alla firma dell'interfaccia Shape
. Tuttavia, un'altra interfaccia potrebbe avere un nome diverso, ma gli stessi metodi. Come o quando sa Go quale interfaccia sta implementando un tipo concreto? Go lo scopre quando viene usata, in fase di esecuzione.
Per dimostrare il modo in cui vengono usate le interfacce, si può scrivere il codice seguente:
func main() {
var s Shape = Square{3}
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
}
Quando si esegue il programma precedente, si ottiene l'output seguente:
main.Square
Area: 9
Perimeter: 12
A questo punto, non fa alcuna differenza se si usa un'interfaccia o meno. Verrà ora creato un altro tipo, ad esempio Circle
, per esplorare i motivi per cui le interfacce sono utili. Ecco il codice per lo struct Circle
:
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
A questo punto, è possibile effettuare il refactoring della funzione main()
e creare una funzione per stampare il tipo dell'oggetto ricevuto, insieme all'area e al perimetro, come indicato di seguito:
func printInformation(s Shape) {
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
fmt.Println()
}
Si noti che la funzione printInformation
ha il parametro Shape
. È possibile inviare a questa funzione un oggetto Square
o Circle
e il codice funzionerà anche se l'output sarà diverso. La funzione main()
dovrebbe essere ora simile alla seguente:
func main() {
var s Shape = Square{3}
printInformation(s)
c := Circle{6}
printInformation(c)
}
Si noti che per l'oggetto c
non viene specificato che si tratta di un oggetto Shape
. Tuttavia, la funzione printInformation
si aspetta un oggetto che implementa i metodi definiti nell'interfaccia Shape
.
Quando si esegue il programma, si dovrebbe ottenere l'output seguente:
main.Square
Area: 9
Perimeter: 12
main.Circle
Area: 113.09733552923255
Perimeter: 37.69911184307752
Si noti che non viene visualizzato un errore e che l'output varia a seconda del tipo di oggetto ricevuto. È anche possibile notare che il tipo di oggetto nell'output non offre alcuna indicazione dell'interfaccia Shape
.
L'aspetto positivo dell'uso delle interfacce è che, per ogni nuovo tipo o implementazione di Shape
, la funzione printInformation
non deve essere modificata. Come detto in precedenza, il codice diventa più flessibile e più facile da estendere quando si usano le interfacce.
Implementare un'interfaccia Stringer
Un semplice esempio di estensione della funzionalità esistente consiste nell'usare un'interfaccia Stringer
, ovvero un'interfaccia con un metodoString()
, come segue:
type Stringer interface {
String() string
}
La funzione fmt.Printf
usa questa interfaccia per stampare i valori, il che significa che è possibile scrivere il metodo personalizzato String()
per stampare una stringa personalizzata, come segue:
package main
import "fmt"
type Person struct {
Name, Country string
}
func (p Person) String() string {
return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
rs := Person{"John Doe", "USA"}
ab := Person{"Mark Collins", "United Kingdom"}
fmt.Printf("%s\n%s\n", rs, ab)
}
Quando si esegue il programma precedente, si ottiene l'output seguente:
John Doe is from USA
Mark Collins is from United Kingdom
Come si può notare, è stato usato un tipo personalizzato (uno struct) per scrivere una versione personalizzata del metodo String()
. Si tratta di una tecnica comune per implementare un'interfaccia in Go, di cui si troveranno esempi in molti programmi, come si vedrà di seguito.
Estendere un'implementazione esistente
Si supponga di voler estendere le funzionalità del codice seguente scrivendo un'implementazione personalizzata di un metodo Writer
progettato per la manipolazione di alcuni dati.
Con questo codice è possibile creare un programma che utilizza l'API GitHub per ottenere tre repository da Microsoft:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
io.Copy(os.Stdout, resp.Body)
}
Quando si esegue il codice precedente, si ottiene un risultato simile al seguente (abbreviato per migliorare la leggibilità):
[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....
Si noti che la chiamata di io.Copy(os.Stdout, resp.Body)
è quella che stampa nel terminale il contenuto ottenuto dalla chiamata all'API GitHub. Si supponga di voler scrivere un'implementazione personalizzata per abbreviare il contenuto visualizzato nel terminale. Esaminando il codice sorgente della funzione io.Copy
si può vedere quanto segue:
func Copy(dst Writer, src Reader) (written int64, err error)
Se si approfondiscono i dettagli del primo parametro, dst Writer
, si noterà che Writer
è un'interfaccia:
type Writer interface {
Write(p []byte) (n int, err error)
}
È possibile continuare a esplorare il codice sorgente del pacchetto io
finché non si individua il punto in cui Copy
chiama il metodo Write
, ma per ora ci si limiterà all'esplorazione.
Dato che Writer
è un'interfaccia ed è un oggetto previsto dalla funzione Copy
, è possibile scrivere un'implementazione personalizzata del metodo Write
. Si può quindi personalizzare il contenuto stampato nel terminale.
Per prima cosa è necessario implementare un'interfaccia per creare un tipo personalizzato. In questo caso, è possibile creare uno struct vuoto, perché è sufficiente scrivere il metodo Write
personalizzato, come segue:
type customWriter struct{}
Ora si è pronti per scrivere la funzione Write
personalizzata. È anche necessario scrivere uno struct per analizzare la risposta dell'API in formato JSON in un oggetto Golang. È possibile usare il sito JSON-to-go per creare uno struct da un payload JSON. Il metodo Write
potrebbe essere quindi simile al seguente:
type GitHubResponse []struct {
FullName string `json:"full_name"`
}
func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}
Infine, è necessario modificare la funzione main()
per usare l'oggetto personalizzato, come indicato di seguito:
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
writer := customWriter{}
io.Copy(writer, resp.Body)
}
Quando si esegue il programma, si dovrebbe ottenere l'output seguente:
microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template
L'output ha un aspetto migliore ora, grazie al metodo Write
personalizzato. Ecco la versione finale del programma:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type GitHubResponse []struct {
FullName string `json:"full_name"`
}
type customWriter struct{}
func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
writer := customWriter{}
io.Copy(writer, resp.Body)
}
Scrivere un'API server personalizzata
Si analizzerà infine un altro caso d'uso per le interfacce che potrebbe risultare utile quando si crea un'API server. Il modo tipico per scrivere un server Web consiste nell'usare l'interfaccia http.Handler
dal pacchetto net/http
, simile alla seguente (non è necessario scrivere questo codice):
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
Si noti che la funzione ListenAndServe
si aspetta un indirizzo di server, ad esempio http://localhost:8000
, e un'istanza di Handler
che invierà la risposta dalla chiamata all'indirizzo del server.
Verrà ora creato ed esaminato il programma seguente:
package main
import (
"fmt"
"log"
"net/http"
)
type dollars float32
func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
Prima di esaminare il codice precedente, eseguirlo come segue:
go run main.go
Se non si ottiene alcun output, è un buon segno. Aprire ora http://localhost:8000
in una nuova finestra del browser o eseguire il comando seguente nel terminale:
curl http://localhost:8000
Verrà ora visualizzato l'output seguente:
Go T-Shirt: $25.00
Go Jacket: $55.00
Il codice precedente verrà analizzato in dettaglio per comprenderne il funzionamento e osservare le potenzialità delle interfacce Go. Per iniziare si crea un tipo personalizzato per un tipo float32
, con lo scopo di scrivere un'implementazione personalizzata del metodo String()
, che verrà usato in un secondo momento.
type dollars float32
func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}
Si scrive poi l'implementazione del metodo ServeHTTP
che può essere usato da http.Handler
. Si noti come viene creato di nuovo un tipo personalizzato, ma questa volta si tratta di un oggetto map e non di uno struct. Si scrive poi il metodo ServeHTTP
usando il tipo database
come ricevitore. L'implementazione di questo metodo usa i dati dal ricevitore, esegue un ciclo e stampa ogni elemento.
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
Infine, nella funzione main()
viene creata un'istanza di un tipo database
che viene poi inizializzato con alcuni valori. Il server HTTP viene avviato tramite la funzione http.ListenAndServe
, in cui viene definito l'indirizzo del server, inclusa la porta da usare e l'oggetto db
che implementa una versione personalizzata del metodo ServeHTTP
. Quando si esegue il programma, Go usa l'implementazione personalizzata di tale metodo ed è in questo che si usa e si implementa un'interfaccia in un'API server.
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
Quando si usa la funzione, è possibile trovare un altro caso d'uso per le interfacce in un'API server http.Handle
. Per altre informazioni, vedere il post Writing web applications (Scrittura di applicazioni Web) nel sito Go.