Zusammenfassung
Es wird ein neues try
Konstrukt vorgeschlagen, das speziell entwickelt wurde, um Ausdrücke zu eliminieren, die üblicherweise mit der Fehlerbehandlung in Go verbunden sind. Dies ist die einzige Änderung in der Sprache. Autoren unterstützen die Verwendung von defer
und Standardbibliotheksfunktionen, um Fehler anzureichern oder zu verpacken. Diese kleine Erweiterung eignet sich für die meisten Szenarien, praktisch ohne die Sprache zu komplizieren.
Das try
Konstrukt ist leicht zu erklären, leicht zu implementieren, diese Funktionalität ist orthogonal zu anderen Sprachkonstrukten und vollständig abwärtskompatibel. Es ist auch erweiterbar, wenn wir es in Zukunft wollen.
Der Rest dieses Dokuments ist wie folgt organisiert: Nach einer kurzen Einführung geben wir eine Definition der integrierten Funktion und erläutern deren Verwendung in der Praxis. Im Diskussionsteil werden alternative Vorschläge und das aktuelle Design besprochen. Am Ende werden die Schlussfolgerungen und der Umsetzungsplan mit Beispielen sowie einem Abschnitt mit Fragen und Antworten gegeben.
Einleitung
Auf der letzten Gophercon-Konferenz in Denver stellten Mitglieder des Go-Teams (Russ Cox, Marcel van Lohuizen) einige neue Ideen vor, wie die mühsame manuelle Fehlerbehandlung in Go ( Entwurfsentwurf ) reduziert werden kann. Seitdem haben wir sehr viel Feedback erhalten.
Wie Russ Cox in seiner Überprüfung des Problems erklärte, ist es unser Ziel, die Fehlerbehandlung einfacher zu gestalten, indem wir die Menge an Code reduzieren, die speziell für die Fehlerprüfung verwendet wird. Wir möchten auch das Schreiben von Fehlerbehandlungscode komfortabler gestalten und die Wahrscheinlichkeit erhöhen, dass Entwickler noch Zeit für die Korrektur der Fehlerbehandlung verwenden. Gleichzeitig möchten wir den Fehlerbehandlungscode im Programmcode deutlich sichtbar lassen.
Die im Entwurfsentwurf diskutierten Ideen konzentrieren sich auf die neue unäre check
, die die explizite Überprüfung des Fehlerwerts vereinfacht, der aus einem Ausdruck (normalerweise einem Funktionsaufruf) erhalten wird, sowie auf die Deklaration von Fehlerbehandlungsroutinen ( handle
) und eine Reihe von Regeln, die diese beiden neuen Sprachkonstrukte verbinden.
Die meisten Rückmeldungen konzentrierten sich auf die Details und die Komplexität des handle
, und die Idee eines check
Bedieners erwies sich als attraktiver. Tatsächlich haben mehrere Mitglieder der Community die Idee eines check
aufgegriffen und erweitert. Hier sind einige Beiträge, die unserem Angebot am ähnlichsten sind:
Der aktuelle Vorschlag war zwar im Detail unterschiedlich, stützte sich jedoch auf diese drei und im Allgemeinen auf die Rückmeldungen zu dem im letzten Jahr vorgeschlagenen Entwurfsentwurf.
Um das Bild zu vervollständigen, möchten wir darauf hinweisen, dass auf dieser Wiki-Seite noch mehr Vorschläge zur Fehlerbehandlung zu finden sind. Es ist auch erwähnenswert, dass Liam Breck umfangreiche Anforderungen an den Fehlerbehandlungsmechanismus gestellt hat.
Schließlich haben wir nach der Veröffentlichung dieses Vorschlags erfahren, dass Ryan Hileman try
fünf Jahren mit dem og rewriter-Tool implementiert und erfolgreich in realen Projekten eingesetzt hat. Siehe ( https://news.ycombinator.com/item?id=20101417 ).
Eingebaute Try-Funktion
Angebot
Wir empfehlen, ein neues funktionsähnliches Sprachelement namens try
hinzuzufügen, das mit einer Signatur aufgerufen wird
func try(expr) (T1, T2, ... Tn)
expr
bedeutet expr
einen Ausdruck eines Eingabeparameters (normalerweise ein Funktionsaufruf), der n + 1 Werte der Typen T1, T2, ... Tn
und error
für den letzten Wert zurückgibt. Wenn expr
ein einzelner Wert ist (n = 0), muss dieser Wert vom Typ error
und try
gibt kein Ergebnis zurück. Das Aufrufen von try
mit einem Ausdruck, der nicht den letzten Wert des Typfehlers error
führt zu einem Kompilierungsfehler.
Das try
Konstrukt kann nur in einer Funktion verwendet werden, die mindestens einen Wert zurückgibt und deren letzter Rückgabewert vom Typ error
. Das Aufrufen von try
in anderen Kontexten führt zu einem Kompilierungsfehler.
Rufen Sie try
mit der Funktion f()
wie im Beispiel auf
x1, x2, … xn = try(f())
führt zu folgendem Code:
t1, … tn, te := f()
Mit anderen Worten, wenn der letzte von expr
zurückgegebene expr
nil
, geben Sie einfach die ersten n Werte zurück und entfernen Sie die letzte nil
.
Wenn der letzte von expr
Wert nicht nil
, dann:
- Der
error
der einschließenden Funktion (im oben genannten Pseudocode err
, obwohl dies ein beliebiger Bezeichner oder ein unbenannter Rückgabewert sein kann) empfängt den von expr
zurückgegebenen expr
- Es gibt einen Ausgang aus der Hüllkurvenfunktion
- Wenn die umschließende Funktion zusätzliche Rückgabeparameter enthält, behalten diese Parameter die Werte bei, die vor dem
try
Aufruf in ihnen enthalten waren. - Wenn die umschließende Funktion zusätzliche unbenannte Rückgabeparameter hat, werden die entsprechenden Nullwerte für sie zurückgegeben (was identisch ist mit dem Speichern ihrer ursprünglichen Nullwerte, mit denen sie initialisiert werden).
Wenn try
wie im obigen Beispiel in mehreren Zuweisungen verwendet wird und ein Fehler ungleich Null (im Folgenden nicht null - ca. Per.) Festgestellt wird, wird die Zuweisung (nach Benutzervariablen) nicht ausgeführt, und keine der Variablen auf der linken Seite der Zuweisung ändert sich. Das heißt, try
verhält sich wie ein Funktionsaufruf: Die Ergebnisse sind nur verfügbar, wenn try
die Kontrolle an den Aufrufer zurückgibt (im Gegensatz zum Fall mit einer Rückgabe von der umschließenden Funktion). Wenn die Variablen auf der linken Seite der Zuweisung Rückgabeparameter sind, führt die Verwendung von try
zu einem Verhalten, das sich von dem typischen Code unterscheidet, der jetzt auftritt. Wenn beispielsweise a,b, err
als Rückgabeparameter einer umschließenden Funktion bezeichnet werden, lautet der folgende Code:
a, b, err = f() if err != nil { return }
weist den Variablen a, b
und err
immer Werte zu, unabhängig davon, ob der Aufruf von f()
einen Fehler zurückgegeben hat oder nicht. Gegenteilige Herausforderung
a, b = try(f())
Lassen Sie im Fehlerfall a
und b
unverändert. Trotz der Tatsache, dass dies eine subtile Nuance ist, glauben wir, dass solche Fälle ziemlich selten sind. Wenn ein bedingungsloses Zuweisungsverhalten erforderlich ist, müssen Sie if
Ausdrücke weiterhin verwenden.
Verwenden Sie
Die Definition von try
Ihnen explizit, wie Sie es verwenden: Viele if
Ausdrücke, die nach Fehlern try
können durch try
. Zum Beispiel:
f, err := os.Open(filename) if err != nil { return …, err
kann vereinfacht werden
f := try(os.Open(filename))
Wenn die aufrufende Funktion keinen Fehler try
kann try
nicht verwendet werden (siehe Abschnitt Diskussion). In diesem Fall sollte der Fehler auf jeden Fall lokal verarbeitet werden (da keine Fehlerrückgabe erfolgt), und in diesem Fall bleibt der geeignete Mechanismus zur Überprüfung auf Fehler bestehen.
Generell ist es unser Ziel, nicht alle möglichen Fehlerprüfungen durch eine try
zu ersetzen. Code, der eine andere Semantik erfordert, kann und sollte weiterhin verwendet werden, if
Ausdrücke und explizite Variablen mit Fehlerwerten.
Testen und versuchen
Bei einem unserer früheren Versuche, eine Spezifikation zu schreiben (siehe Abschnitt Design-Iteration unten), wurde try
entwickelt, um in Panik zu geraten, wenn ein Fehler auftritt, wenn er innerhalb einer Funktion ohne Rückgabefehler verwendet wird. Dies ermöglichte die Verwendung von try
in Unit-Tests basierend auf dem testing
der Standardbibliothek.
Als eine der Optionen ist es möglich, Testfunktionen mit Signaturen im Testpaket zu verwenden
func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error
um die Verwendung von try
in Tests zu ermöglichen. Eine Testfunktion, die einen Fehler ungleich Null zurückgibt, ruft implizit t.Fatal(err)
oder b.Fatal(err)
. Dies ist eine kleine Bibliotheksänderung, bei der je nach Kontext unterschiedliche Verhaltensweisen (Rückkehr oder Panik) für den try
vermieden werden müssen.
Einer der Nachteile dieses Ansatzes besteht darin, dass t.Fatal
und b.Fatal
die Zeilennummer, auf die der Test fiel, nicht zurückgeben können. Ein weiterer Nachteil ist, dass wir auch die Untertests irgendwie ändern müssen. Die Lösung für dieses Problem ist eine offene Frage; In diesem Dokument werden keine spezifischen Änderungen am testing
.
Siehe auch # 21111 , in dem vorgeschlagen wird, Beispielfunktionen zu erlauben, einen Fehler zurückzugeben.
Fehlerbehandlung
Der ursprüngliche Entwurfsentwurf befasste sich hauptsächlich mit der Sprachunterstützung für Umbruch- oder Erweiterungsfehler. Der Entwurf schlug ein neues Schlüsselworthandle und eine neue Methode zum Deklarieren von Fehlerbehandlungsroutinen vor . Dieses neue Sprachkonstrukt zog aufgrund der nicht trivialen Semantik Probleme wie Fliegen auf sich, insbesondere wenn man seine Auswirkungen auf den Ausführungsfluss berücksichtigt. Insbesondere die handle
hat sich kläglich mit der defer
gekreuzt, wodurch die neue Sprachfunktion nicht orthogonal zu allem anderen war.
Dieser Vorschlag reduziert den ursprünglichen Entwurfsentwurf auf das Wesentliche. Wenn eine Anreicherung oder ein Fehlerumbruch erforderlich ist, gibt es zwei Ansätze: Anhängen an if err != nil { return err}
oder "Deklarieren" eines Fehlerbehandlers innerhalb des defer
Ausdrucks:
defer func() { if err != nil {
In diesem Beispiel ist err
der Name des Rückgabeparameters vom Typ error
einschließenden Funktion.
In der Praxis stellen wir uns solche Hilfsfunktionen vor wie
func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } }
oder ähnliches. Das fmt
Paket kann für solche Helfer zu einem natürlichen Ort werden (es bietet bereits fmt.Errorf
). Mithilfe von Hilfsprogrammen wird die Definition eines Fehlerbehandlers in vielen Fällen auf eine einzelne Zeile reduziert. Um beispielsweise den Fehler aus der Funktion "Kopieren" zu bereichern, können Sie schreiben
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
Wenn fmt.HandleErrorf
implizit Fehlerinformationen hinzufügt, ist eine solche Konstruktion ziemlich einfach zu lesen und hat den Vorteil, dass sie implementiert werden kann, ohne neue Elemente der Sprachsyntax hinzuzufügen.
Der Hauptnachteil dieses Ansatzes besteht darin, dass der zurückgegebene Fehlerparameter benannt werden muss, was möglicherweise zu einer weniger genauen API führt (siehe FAQ zu diesem Thema). Wir glauben, dass wir uns daran gewöhnen werden, wenn der geeignete Schreibstil für Code festgelegt ist.
Effizienzverschiebung
Ein wichtiger Gesichtspunkt bei der Verwendung von defer
als Fehlerbehandlungsroutine ist die Effizienz. Der defer
Ausdruck wird als langsam angesehen . Wir möchten nicht zwischen effizientem Code und guter Fehlerbehandlung wählen. Unabhängig von diesem Vorschlag diskutierten die Go-Laufzeit- und Compilerteams alternative Implementierungsmethoden und wir glauben, dass wir typische Methoden zur Verwendung von Defer anwenden können, um Fehler zu behandeln, deren Effizienz mit dem vorhandenen „manuellen“ Code vergleichbar ist. Wir hoffen, in Go 1.14 eine schnellere Implementierung von Defer hinzufügen zu können (siehe auch Ticket CL 171158 , was der erste Schritt in diese Richtung ist).
Sonderfälle go try(f), defer try(f)
Das try
Konstrukt sieht aus wie eine Funktion, und aus diesem Grund wird erwartet, dass es überall dort verwendet werden kann, wo ein Funktionsaufruf akzeptabel ist. Wenn der try
Aufruf jedoch in der go
Anweisung verwendet wird, werden die Dinge kompliziert:
go try(f())
Hier wird f()
ausgeführt, wenn der go-Ausdruck in der aktuellen Goroutine ausgeführt wird. Die Ergebnisse des Aufrufs von f
werden als zu try
Argumente übergeben, die in der neuen Goroutine beginnen. Wenn f
einen Fehler ungleich Null zurückgibt, wird erwartet, dass try
von der umschließenden Funktion zurückkehrt. Es gibt jedoch keine Funktion (und es gibt keinen Rückgabeparameter vom Typ error
), weil Der Code wird in einer separaten Goroutine ausgeführt. Aus diesem Grund schlagen wir vor, try
in einem go
Ausdruck zu deaktivieren.
Situation mit
defer try(f())
sieht ähnlich aus, aber hier bedeutet die Semantik von defer
, dass die Ausführung von try
verzögert wird, bis sie von der umschließenden Funktion zurückkehrt. Wie zuvor wird f()
ausgewertet, wenn der Aufschub defer
, und seine Ergebnisse werden an den zurückgestellten try
.
try
überprüft den Fehler f()
erst im allerletzten Moment zurückgegeben wird, bevor er von der umschließenden Funktion zurückkehrt. Ohne das try
ändern, kann ein solcher Fehler einen anderen Fehlerwert überschreiben, den die umschließende Funktion zurückzugeben versucht. Dies verwirrt bestenfalls, im schlimmsten Fall führt es zu Fehlern. Aus diesem Grund schlagen wir vor, dass Sie den Aufruf von try
in der defer
Anweisung verbieten. Wir können diese Entscheidung jederzeit überdenken, wenn eine solche Semantik angemessen angewendet wird.
Schließlich kann try
wie die anderen integrierten Konstrukte nur als Aufruf verwendet werden. Es kann nicht als Wertfunktion oder in einem Variablenzuweisungsausdruck wie in f := try
(genau wie f := print
und f := new
verboten sind).
Die Diskussion
Design-Iterationen
Das Folgende ist eine kurze Diskussion früherer Entwürfe, die zum aktuellen Minimalvorschlag führten. Wir hoffen, dass dies Aufschluss über ausgewählte Designentscheidungen gibt.
Unsere erste Iteration dieses Satzes wurde von zwei Ideen aus dem Artikel „Schlüsselelemente der Fehlerbehandlung“ inspiriert , nämlich der Verwendung der integrierten Funktion anstelle des Operators und der üblichen Go-Funktion zur Behandlung von Fehlern anstelle des neuen Sprachkonstrukts. Im Gegensatz zu dieser Veröffentlichung hatte unser Fehlerbehandler einen festen func(error) error
Signaturfunktion func(error) error
, um die Sache zu vereinfachen. Ein Fehlerbehandler wird von der try
Funktion aufgerufen try
wenn ein Fehler aufgetreten ist, bevor try
die umschließende Funktion beendet. Hier ist ein Beispiel:
handler := func(err error) error { return fmt.Errorf("foo failed: %v", err)
Dieser Ansatz ermöglichte zwar die Definition effektiver benutzerdefinierter Fehlerbehandlungsroutinen, warf jedoch auch viele Fragen auf, die offensichtlich nicht die richtigen Antworten hatten: Was sollte passieren, wenn nil an die Bearbeiter übergeben wird? Sollten Sie try
Panik try
geraten, oder dies als Mangel an Handler betrachten? Was ist, wenn der Handler mit einem Fehler ungleich Null aufgerufen wird und dann ein Null-Ergebnis zurückgibt? Bedeutet dies, dass der Fehler "abgebrochen" wird? Oder sollte eine umschließende Funktion einen leeren Fehler zurückgeben? Es gab auch Zweifel, dass die optionale Übertragung eines Fehlerbehandlers Entwickler dazu ermutigen würde, Fehler zu ignorieren, anstatt sie zu korrigieren. Es wäre auch einfach, überall die richtige Fehlerbehandlung durchzuführen, aber eine einmalige Verwendung von try
überspringen. Und dergleichen.
In der nächsten Iteration wurde die Möglichkeit, einen benutzerdefinierten Fehlerbehandler zu übergeben, zugunsten der Verwendung von defer
zum Umschließen von Fehlern entfernt. Dies schien ein besserer Ansatz zu sein, da dadurch Fehlerbehandler im Quellcode viel deutlicher wahrgenommen wurden. Dieser Schritt beseitigte auch alle Probleme bezüglich der optionalen Übertragung von Handlerfunktionen, verlangte jedoch, dass die zurückgegebenen Parameter mit dem error
benannt werden, wenn ein Zugriff erforderlich war (wir entschieden, dass dies normal war). Um try
nicht nur für Funktionen nützlich zu machen, die Fehler zurückgeben, war es außerdem erforderlich, das Verhalten von try
kontextsensitiv zu machen: Wenn try
auf Paketebene verwendet wurde oder wenn es in einer Funktion aufgerufen wurde, die keinen Fehler zurückgibt, wird try
automatisch in Panik versetzt, wenn ein Fehler festgestellt wurde. (Und als Nebeneffekt wurde aufgrund dieser Eigenschaft das Sprachkonstrukt in diesem Satz als must
statt try
.) Das kontextsensitive Verhalten von try
(oder must
) schien natürlich und auch sehr nützlich: Es würde viele benutzerdefinierte Funktionen eliminieren, die in Ausdrücken verwendet werden Paketvariablen initialisieren. Es eröffnete auch die Möglichkeit, try
in Unit-Tests mit dem Testpaket zu verwenden.
Das kontextsensitive Verhalten von try
war jedoch mit Fehlern behaftet: Beispielsweise konnte sich das Verhalten einer Funktion, die try
verwendet, beim Hinzufügen oder Entfernen eines Rückgabefehlers zur Funktionssignatur leise ändern (Panik oder nicht). Dies schien eine zu gefährliche Eigenschaft. Die naheliegende Lösung bestand darin, die try
Funktionalität in zwei separate must
and- try
Funktionen aufzuteilen (sehr ähnlich der in # 31442 vorgeschlagenen ). Dies würde jedoch zwei integrierte Funktionen erfordern, während nur try
direkt mit einer besseren Unterstützung der Fehlerbehandlung zusammenhängt.
Daher haben wir in der aktuellen Iteration beschlossen, anstelle der zweiten integrierten Funktion die duale Semantik von try
zu entfernen und daher die Verwendung nur in Funktionen zuzulassen, die einen Fehler zurückgeben.
Merkmale des vorgeschlagenen Entwurfs
Dieser Vorschlag ist recht kurz und scheint im Vergleich zum letztjährigen Entwurf einen Schritt zurück zu sein. Wir glauben, dass die ausgewählten Lösungen gerechtfertigt sind:
Das Wichtigste zuerst: try
hat genau die gleiche Semantik wie die im Original vorgeschlagene check
Anweisung ohne handle
. Dies bestätigt die Treue des ursprünglichen Entwurfs in einem der wichtigen Aspekte.
Die Auswahl einer integrierten Funktion anstelle von Operatoren hat mehrere Vorteile. Es ist kein neues Schlüsselwort wie check
erforderlich, wodurch das Design nicht mit vorhandenen Parsern kompatibel wäre. Es ist auch nicht erforderlich, die Syntax von Ausdrücken mit einem neuen Operator zu erweitern. Das Hinzufügen einer neuen integrierten Funktion ist relativ trivial und völlig orthogonal zu anderen Merkmalen der Sprache.
Die Verwendung einer Inline-Funktion anstelle eines Operators erfordert die Verwendung von Klammern. Wir sollten try(f())
anstelle von try f()
schreiben. Dies ist der (kleine) Preis, den wir für die Abwärtskompatibilität mit vorhandenen Parsern zahlen müssen. Dies macht das Design jedoch auch mit zukünftigen Versionen kompatibel: Wenn wir auf dem Weg entscheiden, dass das Übergeben einer Fehlerbehandlungsfunktion in irgendeiner Form oder das Hinzufügen eines zusätzlichen Parameters zu diesem Zweck eine gute Idee ist, ist das Hinzufügen eines zusätzlichen Arguments zum try
Aufruf trivial.
Wie sich herausstellte, hat die Notwendigkeit, Klammern zu schreiben, seine Vorteile. In komplexeren Ausdrücken mit mehreren try
verbessern Klammern die Lesbarkeit, indem sie sich nicht mehr mit der Priorität von Operatoren befassen müssen, wie in den folgenden Beispielen:
info := try(try(os.Open(file)).Stat())
try
, : try
, .. try
(receiver) .Stat
( os.Open
).
try
, : os.Open(file)
.. try
( , try
os
, , try
try
).
, .. .
Schlussfolgerungen
. , . defer
, .
Go - , . , Go append
. append
, . , . , try
.
, , Go : panic
recover
. error
try
.
, try
, , — — , . Go:
, , . if
-.
Implementierung
:
- Go.
try
. , . .go/types
try
. .gccgo
. ( , ).- .
- , . , . .
Robert Griesemer go/types
, () cmd/compile
. , Go 1.14, 1 2019.
, Ian Lance Taylor gccgo
, .
"Go 2, !" , .
1 , , , Go 1.14 .
Beispiele
CopyFile
:
func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst)
, " ", defer
:
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
( defer
-), defer
, .
printSum
func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
:
func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil }
main
:
func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } }
- try
, :
n, err := src.Read(buf) if err == io.EOF { break } try(err)
Fragen und Antworten
, .
: ?
: check
handle
, . , handle
defer
, handle
.
: try ?
: try
Go . - , . , . , " ". try
, .. .
: try
try?
: , check
, must
do
. try
, . try
check
(, ), - . . must
; try
— . , Rust Swift try
( ). .
: ?
Rust?
: Go ; , Go ( ; - ). , ?
, . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ?
try
— Go, , ( ) . , ?
. , , (, ..) . . , .
: ( error) , defer , go doc. ?
: go doc
, - ( _
) , . , func f() (_ A, _ B, err error)
go doc
func f() (A, B, error)
. , , , . , , . , , , -, (deferred) . Jonathan Geddes try()
.
: defer ?
: defer
. , , defer "" . . CL 171758 , defer 30%.
: ?
: , . , ( , ), . defer
, . defer
- https://golang.org/issue/29934 ( Go 2), .
: , try, error. , ?
: error
( ) , , nil
. try
. ( , . - ).
: Go , try ?
: try
, try
. super return
-, try
Go
. try
. .
: try , . ?
: try
; , . try
( ), . , if
.
: , . try, defer . ?
: , . .
: try
( catch
)?
: try
— ("") , , ( ) . try
; . . "" . , . , try
— . , , throw
try-catch
Go. , (, ), ( ) , . "" try-catch
, . , , . Go . panic
, .