Regole di soddisfacimento delle interfacce
Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 43 di 107.
Sebbene Go utilizzi il duck typing per la soddisfazione implicita delle interfacce, esistono regole specifiche che determinano se un tipo soddisfa effettivamente un’interfaccia. Comprendere queste regole ti aiuta a evitare bug sottili.
La regola più importante riguarda i receiver puntatore. Se un metodo è definito con un receiver puntatore, solo un puntatore a quel tipo soddisfa l’interfaccia, non il valore stesso:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // ricevitore puntatore
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Poiché Save() ha un ricevitore puntatore, solo *Document soddisfa Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // funciona - el puntero satisface la interfaz
// Process(doc) // error de compilación - el valor no satisface la interfaz
}
Tuttavia, il contrario è più flessibile. Se un metodo ha un ricevitore di valore, sia il tipo valore sia il tipo puntatore soddisfano l’interfaccia. Go dereferenzia automaticamente i puntatori quando chiama metodi con ricevitore di valore:
func (d Document) Info() string { // ricevitore per valore
return d.Name
}
// Sia Document sia *Document soddisfano un'interfaccia che richiede Info()
Questa asimmetria esiste perché Go può sempre ottenere un valore da un puntatore (dereferenziandolo), ma non può sempre ottenere un puntatore da un valore (il valore potrebbe non essere indirizzabile). Tenere a mente questa regola evita errori del compilatore confusi quando si lavora con le interfacce.
Sfida
FacileCostruiamo un sistema di configurazione che dimostri in che modo i ricevitori puntatore e i ricevitori per valore influiscono sulla soddisfazione delle interfacce. Creerai dei tipi in cui la scelta del ricevitore determina se con un'interfaccia si possono usare valori, puntatori o entrambi.
Organizzerai il codice in tre file:
config.go: definisci un'interfacciaConfigurableche richiede due metodi:GetValue() stringeSetValue(string). Poi crea due tipi di configurazione:ReadOnlyConfigcon un campoValue: usa un ricevitore per valore perGetValue()(restituisce il Value) e un ricevitore puntatore perSetValue()(aggiorna il Value)Settingcon un campoData: usa ricevitori per valore per entrambi i metodi (GetValue restituisce Data, SetValue si limita a stampare "Cannot modify" senza modificare nulla)
processor.go: crea una funzione chiamataProcessConfigche accetta unConfigurablee una nuova stringa di valore. Dovrebbe stampare il valore attuale usandoGetValue(), chiamareSetValue()con il nuovo valore e poi stampare di nuovo il valore per mostrare eventuali modifiche.main.go: leggi i dettagli della configurazione dall'input e mostra le regole di soddisfazione dell'interfaccia:- Crea un
ReadOnlyConfige passa un puntatore aProcessConfig(necessario perchéSetValueha un ricevitore puntatore) - Crea un
Settinge passa direttamente il valore aProcessConfig(funziona perché entrambi i metodi hanno ricevitori per valore)
- Crea un
Verranno forniti i seguenti input:
- Riga 1: valore iniziale per ReadOnlyConfig
- Riga 2: nuovo valore da impostare per ReadOnlyConfig
- Riga 3: valore iniziale per Setting
- Riga 4: nuovo valore da tentare per Setting
La tua funzione ProcessConfig dovrebbe stampare in questo formato:
Current: [value]
Current: [value after SetValue]Ad esempio, dati debug, production, localhost e remote, l'output dovrebbe essere:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostNota che ReadOnlyConfig cambia effettivamente il proprio valore (perché abbiamo passato un puntatore), mentre Setting rimane invariato (il suo SetValue con un ricevitore per valore non può modificare l'originale). Il punto fondamentale è che i valori di ReadOnlyConfig da soli non soddisferebbero Configurable, mentre lo fanno solo i puntatori; i valori di Setting, invece, funzionano direttamente perché tutti i suoi metodi usano ricevitori per valore.
Provalo tu
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Leggi gli input
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Crea un ReadOnlyConfig con il valore di rocInitial
// TODO: Passa un PUNTATORE a ProcessConfig (necessario perché SetValue ha un ricevitore puntatore)
// TODO: Crea un Setting con il valore di settingInitial
// TODO: Passa il VALORE direttamente a ProcessConfig (funziona perché entrambi i metodi hanno ricevitori per valore)
// Usa queste variabili per evitare errori dovuti a variabili inutilizzate
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti di OOP in Go
File esterniWorkspace e moduli GoPacchetti e importazioniNomi esportati e non esportatiIntroduzione all’OOP in GoGli struct come classiDefinire metodi sugli structReceiver con puntatore vs receiver con valoreInizializzazione degli structFunzioni costruttriciRiepilogo - Calcolatrice semplice4Interfacce
Introduzione alle interfacceImplementazione implicitaL’interfaccia come contrattoInterfaccia vuota (any)Asserzione di tipoSwitch sul tipoComposizione di interfacceInterfacce Stringer ed ErrorRipasso - Calcolatore di forme7Incapsulamento
Campi esportati e non esportatiIncapsulamento a livello di packageMetodi Getter e SetterNascondimento delle informazioni in GoRiepilogo - Registri degli studenti10Tipi generici (Go 1.18+)
Introduzione ai tipi genericiParametri di tipoVincoli di tipoStruct genericheSoluzione alternativa per i metodi genericiRiepilogo - Collezione generica13Modelli di progettazione Parte 1
Introduzione ai modelli di progettazioneModello SingletonModello FactoryModello Abstract FactoryModello ObserverModello Strategy2Approfondimento su tipi e struct
Tipi di base e compostiDefinizioni di tipi personalizzatiTag delle structStruct anonimeStruct annidateValori zero e predefinitiRipasso - Rubrica5Composizione invece dell'ereditarietà
Perché Go non ha l'ereditarietàFondamenti dell'embedding delle structPromozione dei metodiEmbedding di più structEmbedding e aggregazione a confrontoMascheramento dei metodi embeddedRiepilogo: gerarchia dei dipendenti8Gestione degli errori e OOP
L'interfaccia errorTipi di errore personalizzatiWrapping degli errori (fmt.Errorf)Errori sentinellaerrors.Is() ed errors.As()Panic, Defer e RecoverRiepilogo - Parser di file11Libreria standard e OOP
io.Reader e io.Writersort.InterfaceInterfaccia fmt.Stringerencoding/json con le structInterfaccia http.HandlerRipasso - modelli REST API14Modelli di progettazione – Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeMiddleware come Decorator3Puntatori e memoria
Concetti base dei puntatori in GoPuntatori a structPassaggio per valore vs per riferimentoLa funzione new()Garbage collection in GoRiepilogo - Costruttore di liste concatenate6Polimorfismo in Go
Polimorfismo tramite interfacceDuck typing in GoRegole di soddisfacimento delle interfacceCollezioni polimorficheIniezione delle dipendenzeRiepilogo - Elaboratore di pagamenti9Concorrenza e OOP
Basi delle goroutineCanali e comunicazioneCanali bufferizzati vs non bufferizzatiIstruzione selectsync.Mutex e sync.RWMutexsync.WaitGroupProgettazione di struct thread-safeRiepilogo: worker poolEsercitati da solo: Compilatore Go online