Umgang mit Panics in Go

Einführung

Fehler, auf die ein Programm stößt, lassen sich in zwei große Kategorien einteilen: Fehler, die der Programmierer erwartet hat, und Fehler, die der Programmierer nicht erwartet hat. Dieerror-Schnittstelle, die wir in unseren beiden vorherigen Artikeln zuerror handlingbehandelt haben, befasst sich hauptsächlich mit Fehlern, die wir beim Schreiben von Go-Programmen erwarten. Dieerror-Schnittstelle ermöglicht es uns sogar, die seltene Möglichkeit eines Fehlers bei Funktionsaufrufen zu erkennen, damit wir in diesen Situationen angemessen reagieren können.

Paniken fallen in die zweite Kategorie von Fehlern, die vom Programmierer nicht erwartet werden. Diese unvorhergesehenen Fehler führen dazu, dass ein Programm spontan beendet und das laufende Go-Programm beendet wird. Häufige Fehler sind häufig für die Entstehung von Panik verantwortlich. In diesem Lernprogramm werden einige Möglichkeiten untersucht, mit denen allgemeine Vorgänge in Go zu Paniken führen können. Außerdem werden Möglichkeiten zur Vermeidung dieser Paniken aufgezeigt. Wir werden auch die Anweisungen vondeferzusammen mit der Funktion vonrecoververwenden, um Panik zu erfassen, bevor sie unsere laufenden Go-Programme unerwartet beenden können.

Panik verstehen

Es gibt bestimmte Operationen in Go, die automatisch Panik auslösen und das Programm stoppen. Zu den allgemeinen Operationen gehören das Indizieren vonarray über seine Kapazität hinaus, das Ausführen von Typzusicherungen, das Aufrufen von Methoden für Nullzeiger, die falsche Verwendung von Mutexen und der Versuch, mit geschlossenen Kanälen zu arbeiten. Die meisten dieser Situationen sind auf Fehler zurückzuführen, die der Compiler beim Kompilieren des Programms nicht erkennen kann.

Da Panics Details enthalten, die zur Behebung eines Problems hilfreich sind, verwenden Entwickler Panics häufig als Hinweis darauf, dass sie während der Entwicklung eines Programms einen Fehler gemacht haben.

Out of Bounds Panics

Wenn Sie versuchen, auf einen Index zuzugreifen, der über die Länge eines Slices oder die Kapazität eines Arrays hinausgeht, erzeugt die Go-Laufzeit eine Panik.

Das folgende Beispiel macht den häufigen Fehler, dass versucht wird, auf das letzte Element eines Slice zuzugreifen, indem die Länge des Slice verwendet wird, das von den eingebautenlen zurückgegeben wird. Versuchen Sie, diesen Code auszuführen, um festzustellen, warum dies zu einer Panik führen kann:

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

Dies wird die folgende Ausgabe haben:

Outputpanic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

Der Name der Ausgabe der Panik gibt einen Hinweis:panic: runtime error: index out of range. Wir haben eine Scheibe mit drei Meerestieren erstellt. Wir haben dann versucht, das letzte Element des Slice zu erhalten, indem wir dieses Slice mit der Länge des Slice mithilfe der integrierten Funktionlenindiziert haben. Denken Sie daran, dass Slices und Arrays auf Null basieren. Das erste Element ist also Null und das letzte Element in diesem Slice hat den Index2. Da wir versuchen, auf das Slice am dritten Index,3, zuzugreifen, gibt es kein Element im Slice, das zurückgegeben werden kann, da es außerhalb der Grenzen des Slice liegt. Die Laufzeit hat keine andere Wahl, als zu beenden und zu beenden, da wir sie aufgefordert haben, etwas Unmögliches zu tun. Go kann während der Kompilierung auch nicht beweisen, dass dieser Code dies versucht, sodass der Compiler dies nicht abfangen kann.

Beachten Sie auch, dass der nachfolgende Code nicht ausgeführt wurde. Dies liegt daran, dass eine Panik ein Ereignis ist, das die Ausführung Ihres Go-Programms vollständig unterbricht. Die erzeugte Nachricht enthält mehrere Informationen, die für die Diagnose der Ursache der Panik hilfreich sind.

Anatomie einer Panik

Panik besteht aus einer Meldung, die die Ursache der Panik angibt, und einemstack trace, mit dem Sie feststellen können, wo in Ihrem Code die Panik ausgelöst wurde.

Der erste Teil jeder Panik ist die Nachricht. Es beginnt immer mit der Zeichenfolgepanic:, gefolgt von einer Zeichenfolge, die je nach Ursache der Panik variiert. Die Panik aus der vorherigen Übung hat die Nachricht:

panic: runtime error: index out of range [3] with length 3

Die Zeichenfolgeruntime error: nach dem Präfixpanic:gibt an, dass die Panik durch die Sprachlaufzeit generiert wurde. Diese Panik sagt uns, dass wir versucht haben, einen Index[3] zu verwenden, der außerhalb des Bereichs der Slice-Länge3 lag.

Nach dieser Nachricht folgt der Stack-Trace. Stapelverfolgungen bilden eine Karte, der wir folgen können, um genau zu ermitteln, welche Codezeile ausgeführt wurde, als die Panik ausgelöst wurde, und wie dieser Code durch früheren Code aufgerufen wurde.

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

Diese Stapelverfolgung aus dem vorherigen Beispiel zeigt, dass unser Programm die Panik aus der Datei/tmp/sandbox879828148/prog.go in Zeile 13 generiert hat. Es sagt uns auch, dass diese Panik in der Funktionmain() aus dem Paketmain erzeugt wurde.

Die Stapelverfolgung ist in separate Blöcke unterteilt - einen für jedesgoroutine in Ihrem Programm. Die Ausführung jedes Go-Programms erfolgt durch eine oder mehrere Goroutinen, die jeweils unabhängig und gleichzeitig Teile Ihres Go-Codes ausführen können. Jeder Block beginnt mit dem Headergoroutine X [state]:. Der Header gibt die ID-Nummer der Goroutine zusammen mit dem Zustand an, in dem sie sich befand, als die Panik auftrat. Nach dem Header zeigt der Stack-Trace die Funktion, die das Programm beim Auftreten der Panik ausgeführt hat, sowie den Dateinamen und die Zeilennummer, in der die Funktion ausgeführt wurde.

Die Panik im vorherigen Beispiel wurde durch einen uneingeschränkten Zugriff auf ein Slice ausgelöst. Panics können auch generiert werden, wenn Methoden für nicht gesetzte Zeiger aufgerufen werden.

Nil Empfänger

Die Programmiersprache Go verfügt über Zeiger, die auf eine bestimmte Instanz eines Typs verweisen, die zur Laufzeit im Arbeitsspeicher des Computers vorhanden ist. Zeiger können den Wertnil annehmen, der angibt, dass sie auf nichts zeigen. Wenn wir versuchen, Methoden für einen Zeiger aufzurufen, dernil ist, erzeugt die Go-Laufzeit eine Panik. In ähnlicher Weise führen Variablen, die Schnittstellentypen sind, auch zu Problemen, wenn Methoden für sie aufgerufen werden. Verwenden Sie das folgende Beispiel, um die in diesen Fällen generierten Panics anzuzeigen:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

Die Panik wird so aussehen:

Outputpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]

goroutine 1 [running]:
main.(*Shark).SayHello(...)
    /tmp/sandbox160713813/prog.go:12
main.main()
    /tmp/sandbox160713813/prog.go:18 +0x1a

In diesem Beispiel haben wir eine Struktur namensShark definiert. Shark hat auf seinem Zeigerempfänger eine Methode namensSayHello definiert, die beim Aufruf eine Begrüßung nach Standardausgabe druckt. Im Hauptteil unserermain-Funktion erstellen wir eine neue Instanz dieserShark-Struktur und fordern mit dem&-Operator einen Zeiger darauf an. Dieser Zeiger ist der Variablenszugeordnet. Anschließend weisen wir die Variables mit der Anweisungs = nil dem Wertnil neu zu. Schließlich versuchen wir, die MethodeSayHello für die Variables aufzurufen. Anstatt eine freundliche Nachricht von Sammy zu erhalten, erhalten wir die Panik, dass wir versucht haben, auf eine ungültige Speicheradresse zuzugreifen. Da die Variablesnil ist, versucht sie beim Aufrufen der FunktionSayHello, auf das FeldName des Typs*Shark zuzugreifen. Da dies ein Zeigerempfänger ist und der Empfänger in diesem Fallnil ist, gerät er in Panik, weil er einennil-Zeiger nicht dereferenzieren kann.

Während wir in diesem Beispiels explizit aufnil gesetzt haben, geschieht dies in der Praxis weniger offensichtlich. Wenn Sie Panik mitnil pointer dereference sehen, stellen Sie sicher, dass Sie alle von Ihnen erstellten Zeigervariablen ordnungsgemäß zugewiesen haben.

Panics, die aus Nullzeigern und Zugriffen außerhalb der Grenzen generiert werden, sind zwei häufig auftretende Panics, die von der Laufzeit generiert werden. Es ist auch möglich, mithilfe einer integrierten Funktion manuell eine Panik zu generieren.

Verwenden der integrierten Funktionpanic

Mit der integrierten Funktionpanickönnen wir auch eigene Panik erzeugen. Es wird eine einzelne Zeichenfolge als Argument verwendet. Dies ist die Nachricht, die die Panik erzeugt. In der Regel ist diese Meldung weniger ausführlich als das Umschreiben unseres Codes, um einen Fehler zurückzugeben. Darüber hinaus können wir dies in unseren eigenen Paketen verwenden, um Entwicklern mitzuteilen, dass sie möglicherweise einen Fehler bei der Verwendung des Codes unseres Pakets gemacht haben. Wenn immer möglich, sollten Sie versuchen, den Verbrauchern unseres Paketserror-Werte zurückzugeben.

Führen Sie diesen Code aus, um eine Panik zu sehen, die von einer Funktion generiert wurde, die von einer anderen Funktion aufgerufen wurde:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

Die erzeugte Panik-Ausgabe sieht folgendermaßen aus:

Outputpanic: oh no!

goroutine 1 [running]:
main.foo(...)
    /tmp/sandbox494710869/prog.go:8
main.main()
    /tmp/sandbox494710869/prog.go:4 +0x40

Hier definieren wir eine Funktionfoo, die die mit der Zeichenfolge"oh no!" eingebautenpanic aufruft. Diese Funktion wird von unserermain-Funktion aufgerufen. Beachten Sie, dass die Ausgabe die Nachrichtpanic: oh no! enthält und die Stapelverfolgung eine einzelne Goroutine mit zwei Zeilen in der Stapelverfolgung zeigt: eine für die Funktionmain() und eine für unsere Funktionfoo().

Wir haben festgestellt, dass Panics unser Programm an dem Ort beenden, an dem sie generiert wurden. Dies kann zu Problemen führen, wenn offene Ressourcen ordnungsgemäß geschlossen werden müssen. Go bietet einen Mechanismus, mit dem immer Code ausgeführt werden kann, auch wenn eine Panik vorliegt.

Aufgeschobene Funktionen

Ihr Programm verfügt möglicherweise über Ressourcen, die ordnungsgemäß bereinigt werden müssen, auch wenn die Laufzeit eine Panik verarbeitet. Mit Go können Sie die Ausführung eines Funktionsaufrufs verschieben, bis die aufrufende Funktion die Ausführung abgeschlossen hat. Aufgeschobene Funktionen werden auch bei Auftreten einer Panik ausgeführt und dienen als Sicherheitsmechanismus, um der chaotischen Natur von Panik vorzubeugen. Funktionen werden verzögert, indem sie wie gewohnt aufgerufen werden und der gesamten Anweisung das Schlüsselwortdefer vorangestellt wird, wie indefer sayHello(). Führen Sie dieses Beispiel aus, um zu sehen, wie eine Nachricht gedruckt werden kann, obwohl eine Panik ausgelöst wurde:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

Die Ausgabe dieses Beispiels sieht folgendermaßen aus:

Outputhello from the deferred function!
panic: oh no!

goroutine 1 [running]:
main.main()
    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

Innerhalb dermain-Funktion dieses Beispiels rufen wir zuerstdefer eine anonyme Funktion auf, die die Nachricht"hello from the deferred function!" druckt. Diemain-Funktion erzeugt dann sofort eine Panik unter Verwendung derpanic-Funktion. In der Ausgabe dieses Programms sehen wir zuerst, dass die zurückgestellte Funktion ausgeführt wird und drucken ihre Nachricht. Darauf folgt die Panik, die wir inmain erzeugt haben.

Aufgeschobene Funktionen bieten Schutz vor der Überraschung durch Panik. Innerhalb von verzögerten Funktionen bietet Go auch die Möglichkeit, das Beenden unseres Go-Programms durch eine Panik mithilfe einer anderen integrierten Funktion zu verhindern.

Umgang mit Panik

Panics haben einen einzigen Wiederherstellungsmechanismus - die inrecoverintegrierte Funktion. Mit dieser Funktion können Sie eine Panik auf dem Weg durch den Aufrufstapel abfangen und verhindern, dass Ihr Programm unerwartet beendet wird. Es hat strenge Regeln für seine Verwendung, kann aber in einer Produktionsanwendung von unschätzbarem Wert sein.

Da es Teil des Paketsbuiltin ist, kannrecover aufgerufen werden, ohne zusätzliche Pakete zu importieren:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

Dieses Beispiel gibt Folgendes aus:

Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

Unsere Funktionmain in diesem Beispiel ruft eine von uns definierte FunktiondivideByZero auf. Innerhalb dieser Funktion rufen wirdefereine anonyme Funktion auf, die für die Behandlung von Paniken verantwortlich ist, die während der Ausführung vondivideByZero auftreten können. Innerhalb dieser verzögerten anonymen Funktion rufen wir die integrierte Funktion vonrecoverauf und weisen den zurückgegebenen Fehler einer Variablen zu. WenndivideByZero in Panik gerät, wird dieser Wert fürerror festgelegt, andernfalls ist esnil. Durch Vergleichen der Variablenerr mitnil können wir feststellen, ob eine Panik aufgetreten ist, und in diesem Fall protokollieren wir die Panik mithilfe der Funktionlog.Println, als wäre es eine andereerror) s.

Nach dieser verzögerten anonymen Funktion rufen wir eine andere von uns definierte Funktion auf,divide, und versuchen, ihre Ergebnisse mitfmt.Println zu drucken. Die angegebenen Argumente bewirken, dassdivide eine Division durch Null durchführt, was zu einer Panik führt.

In der Ausgabe zu diesem Beispiel sehen wir zuerst die Protokollnachricht der anonymen Funktion, die die Panik wiederherstellt, gefolgt von der Nachrichtwe survived dividing by zero!. Wir haben dies in der Tat getan, dank der inrecoverintegrierten Funktion, die eine ansonsten katastrophale Panik stoppt, die unser Go-Programm beenden würde.

Der vonrecover() zurückgegebeneerr-Wert ist genau der Wert, der für den Aufruf vonpanic() angegeben wurde. Es ist daher wichtig sicherzustellen, dass der Wert vonerrnur dann Null ist, wenn keine Panik aufgetreten ist.

Panik mitrecover erkennen

Die Funktionrecover basiert auf dem Wert des Fehlers, um festzustellen, ob eine Panik aufgetreten ist oder nicht. Da das Argument für die Funktionpaniceine leere Schnittstelle ist, kann es sich um einen beliebigen Typ handeln. Der Nullwert für jeden Schnittstellentyp, einschließlich der leeren Schnittstelle, istnil. Es muss darauf geachtet werden,nil als Argument fürpanic zu vermeiden, wie in diesem Beispiel gezeigt:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

Dies wird Folgendes ausgeben:

Outputwe survived dividing by zero!

Dieses Beispiel ist dasselbe wie das vorherige Beispiel mitrecover mit einigen geringfügigen Änderungen. Die Funktiondivide wurde geändert, um zu überprüfen, ob ihr Teilerb gleich0 ist. Wenn dies der Fall ist, wird unter Verwendung der integriertenpanic mit dem Argumentnil eine Panik ausgelöst. Die Ausgabe enthält diesmal nicht die Protokollmeldung, die angibt, dass eine Panik aufgetreten ist, obwohl eine vondivide erstellt wurde. Aus diesem Grund ist es sehr wichtig sicherzustellen, dass das Argument für die integrierte Funktionpanicnichtnil ist.

Fazit

Wir haben eine Reihe von Möglichkeiten gesehen, wiepanic+`s can be created in Go and how they can be recovered from using the `+recovereingebaut wird. Während Siepanic nicht unbedingt selbst verwenden müssen, ist die ordnungsgemäße Wiederherstellung nach Panik ein wichtiger Schritt, um Go-Anwendungen produktionsbereit zu machen.

Sie können auchour entire How To Code in Go series erkunden.