Un'interface è un insieme di metodi
Un tipo interface elenca delle firme di metodi. Qualsiasi tipo che ha quei metodi soddisfa l'interface, senza nessuna dichiarazione che lo dica.
Né Rect né Circle menzionano Shape. Questa è l'implementazione implicita, la caratteristica che definisce le interface di Go. Significa che puoi definire nel tuo package un'interface che i tipi di altri package soddisfano già, senza toccare il loro codice.
Interface piccole della libreria standard
Il codice Go preferisce interface con uno o due metodi. Le più importanti:
| Interface | Metodo | Usata da |
|---|---|---|
fmt.Stringer | String() string | la stampa di fmt |
error | Error() string | ogni funzione che può fallire |
io.Reader | Read(p []byte) (n int, err error) | file, rete, gzip, body HTTP |
io.Writer | Write(p []byte) (n int, err error) | file, buffer, hash, risposte HTTP |
sort.Interface | Len, Less, Swap | il package sort |
http.Handler | ServeHTTP(w, r) | net/http |
Siccome io.Reader ha un solo metodo, decine di tipi lo implementano, e qualsiasi funzione che accetta un io.Reader funziona con tutti:
Il proverbio di Go dice "più grande è l'interface, più debole è l'astrazione". Le interface più grandi si costruiscono combinando quelle piccole: io.ReadWriter è Reader più Writer, scritta incorporando un'interface in un'altra.
any: l'interface vuota
interface{} non ha metodi, quindi ogni tipo la soddisfa. Go 1.18 ha aggiunto any come alias; sono identici.
Un valore any può contenere qualsiasi cosa, ma non puoi farci quasi niente finché non recuperi il tipo concreto con una type assertion o un type switch. Preferisci una vera interface o i generics quando l'insieme dei tipi è noto. any è adatto a dati davvero dinamici, come un JSON decodificato di forma sconosciuta, e alla stampa.
Cosa contiene un valore interface
Un valore interface è una coppia: un tipo dinamico e un valore dinamico. var s Shape = Rect{3, 4} memorizza il tipo Rect e una copia del valore. Chiamare s.Area() cerca il metodo di Rect a runtime.
Un'interface è nil solo quando entrambe le parti sono vuote. Questa regola causa il bug più confuso di Go.
La trappola dell'interface nil
Un puntatore nil memorizzato in un'interface produce un'interface non nil.
Output:
false
*main.MyError true
true
validate(true) restituisce un'interface error che contiene il tipo *MyError e il valore nil. L'interface ha un tipo, quindi non è uguale a nil, e il ramo if err != nil di chi chiama viene eseguito. Chiamare err.Error() lì andrebbe poi in panic accedendo al campo del receiver nil.
La soluzione è semplice: dichiara la variabile come error, non come il tipo puntatore concreto, oppure restituisci un nil letterale nel percorso di successo. Non restituire mai un tipo puntatore di errore concreto da una funzione il cui risultato è error. La stessa trappola vale per qualsiasi interface, non solo per gli errori.
Verificare che un tipo implementi un'interface
L'implementazione viene verificata dove un valore viene assegnato a un'interface. Se ancora nessun codice lo fa, un errore nella firma di un metodo passa inosservato. Un'assegnazione vuota a livello di package rende esplicito il controllo:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
A runtime non costa nulla. Se *LogWriter ha Write(p []byte) error invece di Write(p []byte) (int, error), la build fallisce:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Receiver puntatore e interface
Se un metodo ha un receiver puntatore, solo il tipo puntatore ha quel metodo. *Counter soddisfa un'interface grazie a esso; Counter no. Il compilatore dice Counter does not implement Incrementer (method Inc has pointer receiver). Memorizza &Counter{} nell'interface. La pagina sui metodi spiega i method set.
Accetta interface, restituisci struct
Una linea guida comune in Go: le funzioni dovrebbero accettare parametri interface e restituire tipi concreti.
- Accettare un'interface permette a chi chiama di passare qualsiasi cosa sia compatibile, compresi i fake per i test. Una funzione che legge dati dovrebbe accettare un
io.Reader, non un*os.File. - Restituire un tipo concreto permette a chi chiama di usare tutti i suoi metodi e campi, ed evita la trappola dell'interface nil.
os.Openrestituisce*os.File, nonio.Reader.
Un'abitudine collegata: definisci le interface dove vengono usate, non dove vengono implementate. Se il tuo servizio ha bisogno di qualcosa che sappia fare Get(id) di un utente, dichiara un'interface con un solo metodo nel package del servizio, e lascia che il package del database esporti semplicemente la sua struct.
Confrontare valori interface
Due valori interface sono uguali quando i loro tipi dinamici sono identici e i loro valori dinamici sono uguali. Se il tipo dinamico non è confrontabile (una slice, una map), == compila ma va in panic a runtime: runtime error: comparing uncomparable type []int.
Errori comuni
- Restituire un puntatore nil tipizzato come interface. Restituisci un
nilletterale. - Interface troppo presto. Scrivi prima il tipo concreto. Aggiungi un'interface quando ne ha bisogno una seconda implementazione o un test.
- Puntatore a interface.
*io.Readernon è quasi mai giusto. Un'interface contiene già un puntatore quando ce ne metti uno dentro. - Interface grandi. Le interface con dieci metodi sono difficili da implementare e da simulare. Dividile.
Domande frequenti
Come si implementa un'interface in Go?
Definisci sul tuo tipo i metodi elencati dall'interface, con gli stessi nomi e le stesse firme. Non esiste una parola chiave implements. Se *File ha Read(p []byte) (int, error), è un io.Reader, automaticamente. Il compilatore lo verifica ovunque assegni il valore al tipo interface.
Cos'è l'interface vuota, o any, in Go?
interface{} non ha metodi, quindi ogni tipo la soddisfa. Da Go 1.18, any è un alias predefinito di interface{}. Un valore di tipo any può contenere qualsiasi cosa, ma ti serve una type assertion o un type switch per riottenere un tipo concreto.
Perché la mia interface in Go non è nil quando le ho assegnato un puntatore nil?
Un valore interface contiene un tipo e un valore. Assegnare un *MyError nil a un error produce un'interface il cui tipo è *MyError e il cui valore è nil, e quell'interface non è uguale a nil. Quando non c'è errore, restituisci un nil letterale invece di un puntatore nil tipizzato.
Come verifico in fase di compilazione che un tipo implementi un'interface?
Aggiungi un'assegnazione vuota a livello di package: var _ io.Reader = (*MyReader)(nil). Se a *MyReader manca un metodo, la build fallisce con un messaggio che nomina il metodo mancante.