Skip to content

nil-Zeiger-Fehler

Einleitung

Bei einem Programmierprojekt musste ich die Close()-Methode aufrufen, um mehrere Objekte zu schließen, ähnlich wie im folgenden Code:

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
}

Allerdings erschien mir die vielen if-Bedingungen nicht sehr elegant. Da B, C und D alle die Close-Methode implementieren, sollte es einfacher gehen. Also packte ich sie in ein Slice und iterierte darüber:

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
}

Das sah besser aus. Lassen Sie uns testen:

go
func main() {
  var a A
  if err := a.Close(); err != nil {
    panic(err)
  }
  fmt.Println("success")
}

Das Ergebnis war überraschend - das Programm stürzte ab! Die Fehlermeldung lautete:

panic: value method main.B.Close called using nil *B pointer

Die Bedeutung: Man kann keine Methode auf einem nil-Empfänger aufrufen. Die if closer != nil-Bedingung im Loop schien keine Filterwirkung zu haben.

Dieses Beispiel ist eine vereinfachte Version eines Bugs, den der Autor tatsächlich erlebt hat. Viele Anfänger könnten ähnliche Fehler machen. Lassen Sie uns klären, was hier wirklich passiert.

Schnittstellen (Interfaces)

Wie in früheren Kapiteln erwähnt, ist nil der Nullwert für Referenztypen: Slices, Maps, Kanäle, Funktionen, Zeiger und Interfaces. Slices, Maps, Kanäle und Funktionen können alle als Zeiger betrachtet werden, die auf konkrete Implementierungen verweisen.

Nur Interfaces sind anders. Ein Interface besteht aus zwei Teilen: Typ und Wert.

Wenn man versucht, einer Variablen nil zuzuweisen, wird der Code nicht kompilieren. Die Fehlermeldung lautet:

use of untyped nil in assignment

Der Inhalt besagt, dass man keine Variable mit einem untyped nil deklarieren kann. Wenn es untyped nil gibt, muss es auch typed nil geben. Diese Situation tritt oft bei Interfaces auf. Betrachten Sie folgendes einfaches Beispiel:

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)
}

Ausgabe:

<nil>
true
<nil>
false

Das Ergebnis ist sehr seltsam. Obwohl die Ausgabe von pa nil ist, ist es nicht gleich nil. Mit Reflection können wir sehen, was es wirklich ist:

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))
}

Ausgabe:

<nil>
true
*int
<nil>

Aus dem Ergebnis ist ersichtlich, dass es tatsächlich (*int)(nil) ist. Das bedeutet, pa speichert den Typ *int, und der tatsächliche Wert ist nil. Bei einem Gleichheitsvergleich eines Interface-Typs wird zuerst geprüft, ob die Typen gleich sind. Wenn die Typen ungleich sind, wird direkt als ungleich bewertet. Erst danach wird geprüft, ob die Werte gleich sind. Diese Logik ist in der Funktion cmd/compile/internal/walk.walkCompare zu finden.

Damit ein Interface also gleich nil ist, muss sein Wert nil sein UND sein Typ muss ebenfalls nil sein, da der Typ im Interface tatsächlich auch ein Zeiger ist:

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

Wenn Sie den Typ umgehen und direkt prüfen möchten, ob der Wert nil ist, können Sie Reflection verwenden. Hier ist ein Beispiel:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.ValueOf(pa).IsNil())
}

Mit IsNil() können Sie direkt prüfen, ob der Wert nil ist. Dadurch wird das oben genannte Problem vermieden.

Bei der täglichen Verwendung gilt daher: Wenn der Rückgabewert einer Funktion ein Interface-Typ ist und Sie einen Nullwert zurückgeben möchten, geben Sie am besten direkt nil zurück. Geben Sie nicht den Nullwert einer konkreten Implementierung zurück. Selbst wenn diese das Interface implementiert, wird sie niemals gleich nil sein, was zu Fehlern wie im obigen Beispiel führen kann.

Zusammenfassung

Nachdem wir das oben genannte Problem gelöst haben, schauen wir uns einige weitere Beispiele an:

Wenn der Empfänger einer Struktur ein Zeiger-Empfänger ist, ist nil verwendbar. Betrachten Sie folgendes Beispiel:

go
type A struct {

}

func (a *A) Do()  {

}

func main() {
  var a *A
  a.Do()
}

Dieser Code läuft normal und verursacht keinen Nullzeiger-Fehler.

Wenn ein Slice nil ist, können Sie auf seine Länge und Kapazität zugreifen und Elemente hinzufügen:

func main() {
  var s []int
  fmt.Println(len(s))
  fmt.Println(cap(s))
  s = append(s, 1)
}

Wenn eine Map nil ist, können Sie auf sie zugreifen, aber eine nil-Map ist schreibgeschützt. Ein Schreibversuch führt zu einem Panic:

go
func main() {
  var s map[string]int
  i, ok := s[""]
  fmt.Println(i, ok)
  fmt.Println(len(s))

  // Beim Schreibversuch wird ein panic ausgelöst
  s["a"] = 1 // panic: assignment to entry in nil map

}

Diese nil-Eigenschaften in den obigen Beispielen können verwirrend sein, besonders für Go-Anfänger. nil repräsentiert den Nullwert (Standardwert) der oben genannten Typen. Standardwerte sollten Standardverhalten zeigen - genau das war die Absicht der Go-Designer: nil nützlicher machen, anstatt direkt einen Nullzeiger-Fehler auszulösen. Diese Philosophie findet sich auch in der Standardbibliothek wieder. Zum Beispiel kann ein HTTP-Server so gestartet werden:

go
http.ListenAndServe(":8080", nil)

Wir können direkt einen nil Handler übergeben, und die http-Bibliothek verwendet dann den Standard-Handler für HTTP-Anfragen.

TIP

Interessierte können sich dieses Video ansehen: Understanding nil - Gopher Conference 2016 - sehr klar und verständlich erklärt.

Golang by www.golangdev.cn edit