Errori Puntatore nil
Introduzione
Durante la scrittura di codice, ho dovuto chiamare il metodo Close() per chiudere più oggetti, come nel codice seguente:
type A struct {
b B
c C
d D
}
func (a A) Close() error {
if a.b != nil {
if err := a.b.Close(); err != nil {
return err
}
}
if a.c != nil {
if err := a.c.Close(); err != nil {
return err
}
}
if a.d != nil {
if err := a.d.Close(); err != nil {
return err
}
}
return nil
}Ma scrivere così tanti controlli if non sembrava elegante. Poiché B, C e D implementano tutti il metodo Close, dovrebbe esserci un modo più conciso. Così li ho messi in una slice e ho usato un ciclo:
func (a A) Close() error {
closers := []io.Closer{
a.b,
a.c,
a.d,
}
for _, closer := range closers {
if closer != nil {
if err := closer.Close(); err != nil {
return err
}
}
}
return nil
}Questo sembra migliore. Eseguiamolo per vedere:
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}Il risultato è sorprendente: il programma va in crash. Il messaggio di errore indica che non si può chiamare un metodo su un ricevitore nil. Il controllo if closer != nil nel ciclo sembra non aver funzionato:
panic: value method main.B.Close called using nil *B pointerL'esempio sopra è una versione semplificata di un bug che ho incontrato. Molti principianti commettono questo errore. Di seguito spiego cosa succede esattamente.
Interfacce
Nei capitoli precedenti è stato menzionato che nil è il valore zero per i tipi di riferimento, come slice, map, channel, funzioni, puntatori e interfacce. Per slice, map, channel e funzioni, possono essere considerati come puntatori che puntano all'implementazione specifica.

Ma le interfacce sono diverse: un'interfaccia è composta da due cose: tipo e valore.

Quando si prova ad assegnare nil a una variabile, la compilazione fallisce con il seguente messaggio:
use of untyped nil in assignmentIl contenuto indica essenzialmente che non si può dichiarare una variabile con valore untyped nil. Poiché esiste untyped nil, deve esistere anche typed nil, e questa situazione si verifica spesso con le interfacce. Consideriamo un esempio semplice:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}Output:
<nil>
true
<nil>
falseIl risultato è molto strano: anche se l'output di pa è nil, non è uguale a nil. Possiamo usare la reflection per vedere cosa è effettivamente:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.TypeOf(pa))
fmt.Println(reflect.ValueOf(pa))
}Output:
<nil>
true
*int
<nil>Dal risultato si può vedere che è effettivamente (*int)(nil). Quindi pa memorizza il tipo *int, ma il suo valore effettivo è nil. Quando si esegue un'operazione di uguaglianza su un valore di tipo interfaccia, prima si controlla se i tipi sono uguali. Se i tipi non sono uguali, si considera direttamente non uguale. Poi si controlla se i valori sono uguali. La logica di判断 delle interfacce può essere riferita alla funzione cmd/compile/internal/walk.walkCompare.
Quindi, se si vuole che un'interfaccia sia uguale a nil, il suo valore deve essere nil e anche il suo tipo deve essere nil, poiché il tipo nell'interfaccia è effettivamente un puntatore:
type iface struct {
tab *itab
data unsafe.Pointer
}Se si vuole bypassare il tipo e controllare direttamente se il valore è nil, si può usare la reflection. Ecco un esempio:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}Con IsNil() si può controllare direttamente se il valore è nil, evitando così il problema sopra. Quindi, quando si usa normalmente, se il valore di ritorno di una funzione è di tipo interfaccia e si vuole restituire un valore zero, è meglio restituire direttamente nil, non restituire il valore zero di un'implementazione specifica. Anche se implementa l'interfaccia, non sarà mai uguale a nil, il che può causare l'errore dell'esempio.
Riepilogo
Risolti i problemi sopra, esaminiamo ora questi esempi:
Quando il ricevitore di un metodo di una struct è un ricevitore puntatore, nil è utilizzabile. Consideriamo il seguente esempio:
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}Questo codice può essere eseguito normalmente senza errori di puntatore nullo.
Quando una slice è nil, si può accedere alla sua lunghezza e capacità, e si possono aggiungere elementi:
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}Quando una map è nil, vi si può accedere, ma una map nil è di sola lettura. Tentare di scrivere causa un panic:
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// Tentativo di scrittura, causa panic
s["a"] = 1 // panic: assignment to entry in nil map
}Queste caratteristiche di nil negli esempi sopra possono creare confusione, specialmente per i principianti di Go. nil rappresenta il valore zero dei tipi sopra, cioè il valore predefinito. Il valore predefinito dovrebbe mostrare un comportamento predefinito. Questo è esattamente ciò che i progettisti di Go volevano: rendere nil più utile, invece di lanciare direttamente errori di puntatore nullo. Questa filosofia si riflette anche nella libreria standard. Ad esempio, per avviare un server HTTP si può scrivere:
http.ListenAndServe(":8080", nil)Possiamo passare direttamente un nil Handler, e la libreria http userà il Handler predefinito per gestire le richieste HTTP.
TIP
Per chi è interessato, consiglio questo video Understanding nil - Gopher Conference 2016, molto chiaro e comprensibile.
