Skip to content

Errori Puntatore nil

Introduzione

Durante la scrittura di codice, ho dovuto chiamare il metodo Close() per chiudere più oggetti, come nel codice seguente:

go
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:

go
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:

go
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 pointer

L'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 assignment

Il 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:

go
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>
false

Il risultato è molto strano: anche se l'output di pa è nil, non è uguale a nil. Possiamo usare la reflection per vedere cosa è effettivamente:

go
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:

go
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:

go
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.

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:

go
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:

go
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:

go
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:

go
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.

Golang by www.golangdev.cn edit