Go: Vorschlag: Eine eingebaute Go-Fehlerprüffunktion, "try"

Erstellt am 5. Juni 2019  ·  808Kommentare  ·  Quelle: golang/go

Vorschlag: Eine eingebaute Go-Fehlerprüffunktion, try

Dieses Angebot wurde geschlossen .

Bevor Sie kommentieren, lesen Sie bitte das detaillierte Design-Dokument und sehen Sie sich die Diskussionszusammenfassung vom 6. Juni , die Zusammenfassung vom 10. Juni und _am wichtigsten die Ratschläge zum Fokussieren_ an . Ihre Frage oder Ihr Vorschlag wurde möglicherweise bereits beantwortet oder gestellt. Danke.

Wir schlagen eine neue integrierte Funktion namens try vor, die speziell dafür entwickelt wurde, die Boilerplate-Anweisungen if zu eliminieren, die normalerweise mit der Fehlerbehandlung in Go verbunden sind. Es werden keine anderen Sprachänderungen vorgeschlagen. Wir befürworten die Verwendung der vorhandenen defer -Anweisung und der Standardbibliotheksfunktionen, um beim Erweitern oder Umschließen von Fehlern zu helfen. Dieser minimale Ansatz adressiert die gängigsten Szenarien und fügt der Sprache nur sehr wenig Komplexität hinzu. Das eingebaute try ist leicht zu erklären, unkompliziert zu implementieren, orthogonal zu anderen Sprachkonstrukten und vollständig abwärtskompatibel. Es lässt auch einen Weg offen, den Mechanismus zu erweitern, falls wir dies in Zukunft wünschen.

[Der folgende Text wurde bearbeitet, um das Designdokument genauer wiederzugeben.]

Die eingebaute Funktion try nimmt einen einzelnen Ausdruck als Argument. Der Ausdruck muss n+1 Werte ergeben (wobei n Null sein kann), wobei der letzte Wert vom Typ error sein muss. Es gibt die ersten n Werte (falls vorhanden) zurück, wenn das (letzte) Fehlerargument null ist, ansonsten kehrt es von der einschließenden Funktion mit diesem Fehler zurück. Zum Beispiel Code wie

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

vereinfacht werden kann

f := try(os.Open(filename))

try kann nur in einer Funktion verwendet werden, die selbst ein error -Ergebnis zurückgibt, und dieses Ergebnis muss der letzte Ergebnisparameter der einschließenden Funktion sein.

Dieser Vorschlag reduziert den ursprünglichen Designentwurf , der auf der letztjährigen GopherCon vorgestellt wurde, auf seine Essenz. Wenn eine Fehlererweiterung oder -umhüllung gewünscht wird, gibt es zwei Ansätze: Bleiben Sie bei der bewährten if -Anweisung, oder „deklarieren“ Sie alternativ einen Fehlerbehandler mit einer defer -Anweisung:

defer func() {
    if err != nil { // no error may have occurred - check for it
        err = … // wrap/augment error
    }
}()

Hier ist err der Name des Fehlerergebnisses der einschließenden Funktion. In der Praxis reduzieren geeignete Hilfsfunktionen die Deklaration eines Fehlerbehandlers auf einen Einzeiler. Zum Beispiel

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

(wobei fmt.HandleErrorf *err $ schmückt) liest sich gut und kann ohne die Notwendigkeit neuer Sprachfeatures implementiert werden.

Der Hauptnachteil dieses Ansatzes besteht darin, dass der Fehlerergebnisparameter benannt werden muss, was möglicherweise zu weniger hübschen APIs führt. Letztendlich ist dies eine Frage des Stils, und wir glauben, dass wir uns an den neuen Stil anpassen werden, so wie wir uns daran gewöhnt haben, keine Semikolons zu haben.

Zusammenfassend mag try auf den ersten Blick ungewöhnlich erscheinen, aber es ist einfach syntaktischer Zucker, der für eine bestimmte Aufgabe maßgeschneidert ist, Fehlerbehandlung mit weniger Boilerplate und um diese Aufgabe gut genug zu bewältigen. Als solches passt es gut in die Philosophie von Go. try ist nicht darauf ausgelegt, _alle_ Fehlerbehandlungssituationen zu behandeln; Es ist so konzipiert, dass es den _häufigsten_ Fall gut handhabt, um das Design einfach und klar zu halten.

Kredite

Dieser Vorschlag ist stark von den Rückmeldungen beeinflusst, die wir bisher erhalten haben. Insbesondere entlehnt es Ideen von:

Detailliertes Designdokument

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

tryhard Tool zur Untersuchung der Auswirkungen von try

https://github.com/griesemer/tryhard

Go2 LanguageChange Proposal error-handling

Hilfreichster Kommentar

Hallo allerseits,

Unser Ziel mit Vorschlägen wie diesem ist es, eine gemeinschaftsweite Diskussion über Auswirkungen, Kompromisse und das weitere Vorgehen zu führen und diese Diskussion dann zu nutzen, um über den weiteren Weg zu entscheiden.

Aufgrund der überwältigenden Reaktion der Community und der ausführlichen Diskussion hier markieren wir diesen Vorschlag vorzeitig als abgelehnt .

In Bezug auf technisches Feedback hat diese Diskussion hilfreich einige wichtige Überlegungen identifiziert, die wir übersehen haben, insbesondere die Auswirkungen auf das Hinzufügen von Debugging-Ausdrucken und die Analyse der Codeabdeckung.

Noch wichtiger ist, dass wir die vielen Leute deutlich gehört haben, die argumentiert haben, dass dieser Vorschlag nicht auf ein lohnendes Problem abzielt. Wir glauben immer noch, dass die Fehlerbehandlung in Go nicht perfekt ist und sinnvoll verbessert werden kann, aber es ist klar, dass wir als Community mehr darüber sprechen müssen, welche spezifischen Aspekte der Fehlerbehandlung Probleme sind, die wir ansprechen sollten.

Was die Diskussion des zu lösenden Problems anbelangt, so haben wir im vergangenen August versucht, unsere Vision des Problems in der „ Go 2-Fehlerbehandlungsproblemübersicht“ darzulegen , aber im Nachhinein haben wir diesem Teil nicht genug Aufmerksamkeit geschenkt und nicht genug dazu ermutigt Diskussion darüber, ob das konkrete Problem das richtige war. Der try -Vorschlag mag eine gute Lösung für das dort beschriebene Problem sein, aber für viele von Ihnen ist es einfach kein Problem, das es zu lösen gilt. In Zukunft müssen wir besser auf diese frühen Problemstellungen aufmerksam machen und sicherstellen, dass es eine breite Übereinstimmung über das zu lösende Problem gibt.

(Es ist auch möglich, dass die Problemstellung zur Fehlerbehandlung durch die Veröffentlichung eines generischen Designentwurfs am selben Tag völlig in den Hintergrund gedrängt wurde.)

Zum breiteren Thema, was man an der Go-Fehlerbehandlung verbessern sollte, würden wir uns sehr über Erfahrungsberichte darüber freuen, welche Aspekte der Fehlerbehandlung in Go für Sie in Ihren eigenen Codebasen und Arbeitsumgebungen am problematischsten sind und wie viel Einfluss eine gute Lösung hätte in Ihrer eigenen Entwicklung haben. Wenn Sie einen solchen Bericht schreiben, posten Sie bitte einen Link auf der Go2ErrorHandlingFeedback-Seite .

Vielen Dank an alle, die an dieser Diskussion teilgenommen haben, hier und anderswo. Wie Russ Cox bereits betont hat, sind gemeinschaftsweite Diskussionen wie diese Open Source im besten Sinne . Wir schätzen wirklich jede Hilfe bei der Prüfung dieses speziellen Vorschlags und ganz allgemein bei der Diskussion der besten Möglichkeiten zur Verbesserung des Zustands der Fehlerbehandlung in Go.

Robert Griesemer, für das Proposal Review Committee.

Alle 808 Kommentare

Ich stimme zu, dass dies der beste Weg nach vorne ist: das Beheben des häufigsten Problems mit einem einfachen Design.

Ich möchte nicht bikeshed (zögern Sie nicht, dieses Gespräch zu verschieben), aber Rust ging dorthin und entschied sich schließlich mit dem Postfix-Operator ? und nicht mit einer eingebauten Funktion, um die Lesbarkeit zu verbessern.

Der Gophercon-Vorschlag zitiert ? in den erwogenen Ideen und nennt drei Gründe, warum er verworfen wurde: den ersten ("Steuerflussübertragungen werden in der Regel von Schlüsselwörtern begleitet") und den dritten ("Handler sind natürlicher definiert mit einem Schlüsselwort, also sollten auch Prüfungen") entfallen. Der zweite ist stilistisch: Er besagt, dass der Postfix-Operator, selbst wenn er besser für die Verkettung funktioniert, in einigen Fällen immer noch schlechter lesen kann, wie zum Beispiel:

check io.Copy(w, check newReader(foo))

eher, als:

io.Copy(w, newReader(foo)?)?

aber jetzt hätten wir:

try(io.Copy(w, try(newReader(foo))))

was meiner Meinung nach eindeutig die schlechtere der drei ist, da es nicht einmal mehr offensichtlich ist, welche Hauptfunktion aufgerufen wird.

Der Kern meines Kommentars ist also, dass alle drei Gründe, die im Gophercon-Vorschlag für die Nichtverwendung von ? angeführt werden, auf diesen try -Vorschlag nicht zutreffen; ? ist prägnant, gut lesbar, verdeckt nicht die Anweisungsstruktur (mit seiner internen Funktionsaufrufhierarchie) und ist verkettbar. Es entfernt noch mehr Unordnung aus der Sicht, während es den Kontrollfluss nicht mehr verdeckt, als es das vorgeschlagene try() bereits tut.

Um klarzustellen:

Tut

func f() (n int, err error) {
  n = 7
  try(errors.New("x"))
  // ...
}

Rückgabe (0, "x") oder (7, "x")? Ich würde letzteres vermuten.

Muss die Fehlerrückgabe in dem Fall benannt werden, in dem es keine Dekoration oder Handhabung gibt (wie in einer internen Hilfsfunktion)? Ich würde nicht annehmen.

Ihr Beispiel gibt 7, errors.New("x") zurück. Dies sollte aus dem vollständigen Dokument hervorgehen, das bald eingereicht wird (https://golang.org/cl/180557).

Der Fehlerergebnisparameter muss nicht benannt werden, um try zu verwenden. Es muss nur benannt werden, wenn die Funktion in einer zurückgestellten Funktion oder anderswo darauf verweisen muss.

Ich bin wirklich unzufrieden mit einer eingebauten _Funktion_, die den Kontrollfluss des Aufrufers beeinflusst. Ich schätze die Unmöglichkeit, neue Schlüsselwörter in Go 1 hinzuzufügen, aber dieses Problem mit magischen integrierten Funktionen zu umgehen, erscheint mir einfach falsch. Das Spiegeln anderer integrierter Elemente hat keine so unvorhersehbaren Ergebnisse wie das Ändern des Kontrollflusses.

Ich mag nicht, wie Postfix ? aussieht, aber ich denke, es schlägt immer noch try() .

Bearbeiten: Nun, ich habe es geschafft, völlig zu vergessen, dass Panik existiert und kein Schlüsselwort ist.

Der detaillierte Vorschlag ist jetzt hier (ausstehende Formatierungsverbesserungen, in Kürze) und wird hoffentlich viele Fragen beantworten.

@dominikh Der detaillierte Vorschlag diskutiert dies ausführlich, aber bitte beachten Sie, dass panic und recover zwei eingebaute Elemente sind, die sich auch auf den Kontrollfluss auswirken.

Eine Klarstellung / Verbesserungsvorschlag:

if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns

Könnte hier stattdessen is set to that non-nil error value and the enclosing function returns ? (s/vor/und)

Beim ersten Lesen sah before the enclosing function returns so aus, als würde es den Fehlerwert irgendwann in der Zukunft setzen, kurz bevor die Funktion zurückkehrte - möglicherweise in einer späteren Zeile. Die korrekte Interpretation ist, dass try dazu führen kann, dass die aktuelle Funktion zurückkehrt. Das ist ein überraschendes Verhalten für die aktuelle Sprache, daher wäre ein klarerer Text zu begrüßen.

Ich denke, das ist nur Zucker, und eine kleine Anzahl von lautstarken Gegnern hat Golang über die wiederholte Verwendung von if err != nil ... gehänselt, und jemand hat es ernst genommen. Ich denke nicht, dass es ein Problem ist. Die einzigen fehlenden Dinge sind diese beiden integrierten Funktionen:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

Ich bin mir nicht sicher, warum jemand jemals eine Funktion wie diese schreiben würde, aber wofür wäre die vorgesehene Ausgabe

try(foobar())

Wenn foobar (error, error) zurückgegeben hat

Ich ziehe meine früheren Bedenken bezüglich des Kontrollflusses zurück und schlage nicht länger vor, ? zu verwenden. Ich entschuldige mich für die spontane Antwort (obwohl ich darauf hinweisen möchte, dass dies nicht passiert wäre, wenn das Problem eingereicht worden wäre, _nachdem_ der vollständige Vorschlag verfügbar war).

Ich bin nicht einverstanden mit der Notwendigkeit einer vereinfachten Fehlerbehandlung, aber ich bin sicher, dass dies ein verlorener Kampf ist. try , wie im Vorschlag dargelegt, scheint der am wenigsten schlechte Weg zu sein, dies zu tun.

@webermaster Nur das letzte error Ergebnis ist speziell für den an try übergebenen Ausdruck, wie im Proposal-Dokument beschrieben.

Wie @dominikh stimme auch ich der Notwendigkeit einer vereinfachten Fehlerbehandlung nicht zu.

Es verschiebt vertikale Komplexität in horizontale Komplexität, was selten eine gute Idee ist.

Wenn ich mich jedoch unbedingt zwischen Vorschlägen zur Vereinfachung der Fehlerbehandlung entscheiden müsste, wäre dies mein bevorzugter Vorschlag.

Es wäre hilfreich, wenn dies (in einem gewissen Stadium der Akzeptanz) von einem Tool begleitet werden könnte, um den Go-Code so umzuwandeln, dass er try in einer Teilmenge von fehlerrückgebenden Funktionen verwendet, wo eine solche Transformation leicht ohne durchgeführt werden kann Semantik ändern. Drei Vorteile fallen mir ein:

  • Bei der Bewertung dieses Vorschlags würde es den Leuten ermöglichen, schnell ein Gefühl dafür zu bekommen, wie try in ihrer Codebasis verwendet werden könnte.
  • Wenn try in einer zukünftigen Version von Go landet, werden die Leute wahrscheinlich ihren Code ändern wollen, um davon Gebrauch zu machen. Ein Tool zur Automatisierung der einfachen Fälle zu haben, wird sehr hilfreich sein.
  • Eine Möglichkeit zu haben, eine große Codebasis schnell für die Verwendung try umzuwandeln, macht es einfach, die Auswirkungen der Implementierung in großem Maßstab zu untersuchen. (Korrektheit, Leistung und Codegröße zum Beispiel.) Die Implementierung kann jedoch einfach genug sein, um dies zu einer vernachlässigbaren Überlegung zu machen.

Ich möchte nur zum Ausdruck bringen, dass ich denke, dass ein bloßes try(foo()) , das tatsächlich aus der aufrufenden Funktion aussteigt, uns den visuellen Hinweis nimmt, dass sich der Funktionsfluss abhängig vom Ergebnis ändern kann.

Ich habe das Gefühl, dass ich mit try arbeiten kann, wenn ich mich genug daran gewöhnt habe, aber ich glaube auch, dass wir zusätzliche IDE-Unterstützung (oder so etwas) brauchen werden, um try hervorzuheben und den impliziten Fluss in Code-Reviews effizient zu erkennen /Debugging-Sitzungen

Was mir am meisten Sorgen macht, ist die Notwendigkeit, benannte Rückgabewerte zu haben, nur damit die defer-Anweisung glücklich ist.

Ich denke, das allgemeine Fehlerbehandlungsproblem, über das sich die Community beschwert, ist eine Kombination aus der Textbausteine ​​von if err != nil UND dem Hinzufügen von Kontext zu Fehlern. Die FAQ besagt eindeutig, dass letzteres absichtlich als separates Problem weggelassen wird, aber ich habe das Gefühl, dass dies dann zu einer unvollständigen Lösung wird, aber ich bin bereit, ihm eine Chance zu geben, nachdem ich über diese beiden Dinge nachgedacht habe:

  1. Deklarieren Sie am Anfang der Funktion err .
    Funktioniert das? Ich erinnere mich an Probleme mit zurückgestellten und unbenannten Ergebnissen. Wenn dies nicht der Fall ist, muss der Vorschlag dies berücksichtigen.
func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}
  1. Weisen Sie Werte wie in der Vergangenheit zu, aber verwenden Sie eine wrapf -Hilfsfunktion, die die if err != nil -Boilerplate enthält.
func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Wenn beide funktionieren, kann ich damit umgehen.

func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}

Das wird nicht funktionieren. Die Verzögerung aktualisiert die lokale Variable err , die nichts mit dem Rückgabewert zu tun hat.

func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Das sollte funktionieren. Wrapf wird jedoch auch bei einem Null-Fehler aufgerufen.
Dies wird auch (weiterhin) funktionieren und ist meiner Meinung nach viel klarer:

func sample() (string, error) {
  s, err := f()
  if err != nil {
      return "", wrap(err)
  }
  return s, nil
}

Niemand wird Sie dazu zwingen, try zu verwenden.

Ich bin mir nicht sicher, warum jemand jemals eine Funktion wie diese schreiben würde, aber wofür wäre die vorgesehene Ausgabe

try(foobar())

Wenn foobar (error, error) zurückgegeben hat

Warum würden Sie mehr als einen Fehler von einer Funktion zurückgeben? Wenn Sie mehr als einen Fehler von der Funktion zurückgeben, sollte die Funktion vielleicht zunächst in zwei separate aufgeteilt werden, von denen jede nur einen Fehler zurückgibt.

Könnten Sie mit einem Beispiel näher darauf eingehen?

@cespare : Es sollte jemandem möglich sein, ein go fix zu schreiben, das vorhandenen Code umschreibt, der für try geeignet ist, sodass er try verwendet. Es kann nützlich sein, ein Gefühl dafür zu bekommen, wie vorhandener Code vereinfacht werden könnte. Wir erwarten keine signifikanten Änderungen in der Codegröße oder -leistung, da try nur syntaktischer Zucker ist, der ein gemeinsames Muster durch ein kürzeres Stück Quellcode ersetzt, das im Wesentlichen denselben Ausgabecode erzeugt. Beachten Sie auch, dass Code, der try verwendet, zwangsläufig eine Go-Version verwenden muss, die mindestens die Version ist, in der try eingeführt wurde.

@lestrrat : Einverstanden, dass man lernen muss, dass try den Kontrollfluss ändern kann. Wir vermuten, dass IDEs dies leicht genug hervorheben könnten.

@Goodwine : Wie @randall77 bereits betonte, wird Ihr erster Vorschlag nicht funktionieren. Eine Option, über die wir nachgedacht haben (aber in der Dokumentation nicht diskutiert), ist die Möglichkeit, eine vordeklarierte Variable zu haben, die das Ergebnis error bezeichnet (falls überhaupt eine vorhanden ist). Das würde die Notwendigkeit beseitigen, dieses Ergebnis zu benennen, nur damit es in einem defer verwendet werden kann. Aber das wäre noch magischer; es erscheint nicht gerechtfertigt. Das Problem bei der Benennung des Rückgabeergebnisses ist im Wesentlichen kosmetischer Natur, und das Wichtigste sind die automatisch generierten APIs, die von go doc und seinen Freunden bereitgestellt werden. Es wäre einfach, dies in diesen Tools anzugehen (siehe auch die FAQ des ausführlichen Designdokuments zu diesem Thema).

@nictuku : In Bezug auf Ihren Vorschlag zur Klarstellung (s/before/and/): Ich denke, der Code unmittelbar vor dem Absatz, auf den Sie sich beziehen, macht deutlich, was genau passiert, aber ich verstehe Ihren Punkt, s/before/and/ may machen die Prosa klarer. Ich werde die Änderung vornehmen.

Siehe CL 180637 .

Ich finde diesen Vorschlag eigentlich ganz gut. Allerdings habe ich einen Kritikpunkt. Der Ausstiegspunkt von Funktionen in Go wurde schon immer durch ein return gekennzeichnet. Paniken sind auch Ausstiegspunkte, aber das sind katastrophale Fehler, die normalerweise nie auftreten sollen.

Das Erstellen eines Ausstiegspunkts für eine Funktion, die kein return ist und alltäglich sein soll, kann zu viel weniger lesbarem Code führen. Ich hatte davon in einem Vortrag gehört und es ist schwer zu übersehen, wie schön dieser Code strukturiert ist:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

Dieser Code mag wie ein großes Durcheinander aussehen und war im Fehlerbehandlungsentwurf _gewollt_, aber vergleichen wir ihn mit try .

func CopyFile(src, dst string) error {
    defer func() {
        err = fmt.Errorf("copy %s %s: %v", src, dst, err)
    }()
    r, err := try(os.Open(src))
    defer r.Close()

    w, err := try(os.Create(dst))

    defer w.Close()
    defer os.Remove(dst)
    try(io.Copy(w, r))
    try(w.Close())

    return nil
}

Sie können sich das auf den ersten Blick ansehen und denken, dass es besser aussieht, weil es viel weniger wiederholten Code gibt. Es war jedoch sehr einfach, alle Stellen zu erkennen, die die Funktion im ersten Beispiel zurückgegeben hat. Sie waren alle eingerückt und begannen mit return , gefolgt von einem Leerzeichen. Dies liegt an der Tatsache, dass alle bedingten Rückgaben innerhalb von bedingten Blöcken stehen _müssen_ und daher durch gofmt -Standards eingerückt sind. return ist auch, wie bereits erwähnt, die einzige Möglichkeit, eine Funktion zu verlassen, ohne zu sagen, dass ein katastrophaler Fehler aufgetreten ist. Im zweiten Beispiel gibt es nur ein einziges return , also sieht es so aus, als ob die Funktion _ever_ nur nil zurückgeben sollte. Die letzten beiden try Aufrufe sind leicht zu sehen, aber die ersten beiden sind etwas schwieriger und wären sogar noch schwieriger, wenn sie irgendwo verschachtelt wären, dh so etwas wie proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) .

Die Rückkehr von einer Funktion scheint eine "heilige" Sache gewesen zu sein, weshalb ich persönlich der Meinung bin, dass alle Austrittspunkte einer Funktion mit return gekennzeichnet sein sollten.

Das hat schon mal jemand vor 5 Jahren umgesetzt. Wenn Sie interessiert sind, können Sie
Probieren Sie diese Funktion aus

https://news.ycombinator.com/item?id=20101417

Ich habe try() in Go vor fünf Jahren mit einem AST-Präprozessor implementiert und in echten Projekten verwendet, es war ziemlich nett: https://github.com/lunixbochs/og

Hier sind einige Beispiele, wie ich es in fehlerüberprüfungslastigen Funktionen verwende: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13

Ich weiß die Mühe zu schätzen, die in diese Sache gesteckt wurde. Ich denke, es ist die beste Lösung, die ich bisher gesehen habe. Aber ich denke, es bringt eine Menge Arbeit beim Debuggen mit sich. Es ist mühsam, jedes Mal, wenn ich debugge, einen if-Block auszupacken und hinzuzufügen, und ihn neu zu verpacken, wenn ich fertig bin. Und ich habe auch etwas Angst vor der magischen err-Variablen, die ich berücksichtigen muss. Ich habe mich nie um die explizite Fehlerprüfung gekümmert, also bin ich vielleicht die falsche Person, um zu fragen. Es erschien mir immer als "bereit zum Debuggen".

@griesemer
Mein Problem mit Ihrer vorgeschlagenen Verwendung von defer als Möglichkeit zur Behandlung des Fehlerumbruchs besteht darin, dass das Verhalten aus dem von mir gezeigten Snippet (weiter unten) nicht sehr häufig AFAICT ist, und weil es sehr selten ist, kann ich mir vorstellen, dass Leute dies schreiben und denken, dass es funktioniert wenn nicht.

Zum Beispiel würde ein Anfänger das nicht wissen, wenn er deswegen einen Fehler hat, wird er nicht sagen "natürlich brauche ich eine benannte Rückgabe", er würde gestresst, weil es funktionieren sollte und es nicht tut.

var err error
defer fmt.HandleErrorf(err);

try ist bereits zu magisch, also können Sie genauso gut den ganzen Weg gehen und diesen impliziten Fehlerwert hinzufügen. Denken Sie an die Anfänger, nicht an diejenigen, die alle Nuancen von Go kennen. Wenn es nicht klar genug ist, denke ich nicht, dass es die richtige Lösung ist.

Oder ... Schlagen Sie nicht vor, defer so zu verwenden, versuchen Sie es mit einem anderen Weg, der sicherer, aber immer noch lesbar ist.

@deanveloper Es ist wahr, dass dieser Vorschlag (und übrigens jeder Vorschlag, der versucht, dasselbe zu versuchen) explizit sichtbare return -Anweisungen aus dem Quellcode entfernen wird - das ist schließlich der springende Punkt des Vorschlags. nicht wahr? Um die Textbausteine ​​von if -Anweisungen und returns zu entfernen, die alle gleich sind. Wenn Sie die return behalten möchten, verwenden Sie nicht try .

Wir sind daran gewöhnt, return -Anweisungen (und panic 's) sofort zu erkennen, da diese Art von Kontrollfluss in Go (und vielen anderen Sprachen) ausgedrückt wird. Es scheint nicht weit hergeholt, dass wir nach einiger Eingewöhnungszeit auch try als sich ändernden Kontrollfluss erkennen werden, genau wie wir es für return tun. Ich habe keinen Zweifel, dass eine gute IDE-Unterstützung auch dabei helfen wird.

Ich habe zwei Bedenken:

  • benannte Rückgaben waren sehr verwirrend, und dies ermutigt sie mit einem neuen und wichtigen Anwendungsfall
  • dies wird davon abhalten, Kontext zu Fehlern hinzuzufügen

Meiner Erfahrung nach ist das Hinzufügen von Kontext zu Fehlern unmittelbar nach jeder Aufrufseite entscheidend für Code, der leicht debuggt werden kann. Und benannte Rückgaben haben bei fast jedem Go-Entwickler, den ich kenne, irgendwann für Verwirrung gesorgt.

Ein kleinerer, stilistischer Aspekt ist, dass es unglücklich ist, wie viele Codezeilen jetzt in try(actualThing()) eingeschlossen werden. Ich kann mir vorstellen, die meisten Zeilen in einer Codebasis in try() zu sehen. Das fühlt sich unglücklich an.

Ich denke, diese Bedenken würden mit einer Optimierung angegangen:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() würde sich ähnlich verhalten wie try() , würde aber das Verhalten der generischen Übergabe von Funktionsrückgabewerten aufgeben und stattdessen die Möglichkeit bieten, Kontext hinzuzufügen. Es würde immer noch eine Rückkehr auslösen.

Dies würde viele der Vorteile von try() beibehalten:

  • es ist ein eingebautes
  • es folgt dem bestehenden Kontrollfluss WRT zum Verzögern
  • Es passt gut zu der bestehenden Praxis, Fehlern Kontext hinzuzufügen
  • Es stimmt mit aktuellen Vorschlägen und Bibliotheken für das Umschließen von Fehlern überein, wie z. B. errors.Wrap(err, "context message")
  • es führt zu einer sauberen Call-Site: Es gibt keine Textbausteine ​​in der a, b, err := myFunc() -Zeile
  • Das Beschreiben von Fehlern mit defer fmt.HandleError(&err, "msg") ist immer noch möglich, muss aber nicht gefördert werden.
  • Die Signatur von check ist etwas einfacher, da sie keine beliebige Anzahl von Argumenten von der Funktion, die sie umschließt, zurückgeben muss.

@s4n-gt Danke für diesen Link. Ich war mir dessen nicht bewusst.

@Goodwine Point genommen. Der Grund dafür, keine direktere Fehlerbehandlungsunterstützung bereitzustellen, wird im Designdokument ausführlich erörtert. Fakt ist auch, dass im Laufe von etwa einem Jahr (seit der Veröffentlichung der Entwurfsentwürfe auf der letztjährigen Gophercon) keine zufriedenstellende Lösung für eine explizite Fehlerbehandlung gefunden wurde. Aus diesem Grund lässt dieser Vorschlag dies absichtlich weg (und schlägt stattdessen vor, ein defer zu verwenden). Dieser Vorschlag lässt noch die Tür für zukünftige Verbesserungen in dieser Hinsicht offen.

Der Vorschlag erwähnt das Ändern von Pakettests, damit Tests und Benchmarks einen Fehler zurückgeben können. Obwohl es keine „bescheidene Bibliotheksänderung“ wäre, könnten wir auch in Betracht ziehen, func main() error zu akzeptieren. Es würde das Schreiben kleiner Skripte viel schöner machen. Die Semantik wäre äquivalent zu:

func main() {
  if err := newmain(); err != nil {
    println(err.Error())
    os.Exit(1)
  }
}

Eine letzte Kritik. Nicht wirklich eine Kritik am Vorschlag selbst, sondern eine Kritik an einer gemeinsamen Antwort auf das Gegenargument "Funktion steuert den Fluss".

Die Antwort auf "Ich mag es nicht, dass eine Funktion den Ablauf steuert" ist, dass " panic auch den Ablauf des Programms steuert!". Es gibt jedoch ein paar Gründe, warum es für panic in Ordnung ist, dies zu tun, die nicht für try gelten.

  1. panic ist freundlich zu Programmieranfängern, da es intuitiv ist und den Stack weiter auspackt. Man sollte nicht einmal nachschlagen müssen, wie panic funktioniert, um zu verstehen, was es tut. Programmieranfänger brauchen sich nicht einmal um recover zu kümmern, da Anfänger normalerweise keine Panikwiederherstellungsmechanismen bauen, zumal sie fast immer ungünstiger sind, als die Panik von vornherein zu vermeiden.

  2. panic ist ein leicht zu erkennender Name. Es bringt Sorgen, und es muss. Wenn man panic in einer Codebasis sieht, sollte man sofort darüber nachdenken, wie man die Panik _vermeidet_, auch wenn es trivial ist.

  3. Neben dem letzten Punkt kann panic nicht in einen Aufruf verschachtelt werden, wodurch es noch einfacher zu sehen ist.

Es ist in Ordnung, wenn Panik den Ablauf des Programms steuert, da es extrem leicht zu erkennen ist und es intuitiv ist, was es tut.

Die Funktion try erfüllt keinen dieser Punkte.

  1. Man kann nicht erraten, was try tut, ohne die Dokumentation dafür nachzuschlagen. Viele Sprachen verwenden das Schlüsselwort auf unterschiedliche Weise, was es schwierig macht, zu verstehen, was es in Go bedeuten würde.

  2. try fällt mir nicht ins Auge, besonders wenn es sich um eine Funktion handelt. _Besonders_ wenn die Syntaxhervorhebung sie als Funktion hervorhebt. _BESONDERS_ nach der Entwicklung in einer Sprache wie Java, wo try als unnötige Boilerplate angesehen wird (wegen geprüfter Ausnahmen).

  3. try kann in einem Argument für einen Funktionsaufruf verwendet werden, wie in meinem Beispiel in meinem vorherigen Kommentar proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) . Dies macht es noch schwieriger zu erkennen.

Meine Augen ignorieren die try -Funktionen, auch wenn ich gezielt danach suche. Meine Augen werden sie sehen, aber sofort zu den Anrufen os.FindProcess oder strconv.Atoi springen. try ist eine bedingte Rückgabe. Sowohl Kontrollfluss als auch Rückgaben werden in Go auf Sockeln gehalten. Der gesamte Kontrollfluss innerhalb einer Funktion ist eingerückt und alle Rückgaben beginnen mit return . Das Mischen dieser beiden Konzepte zu einem leicht zu übersehenden Funktionsaufruf fühlt sich einfach ein bisschen daneben an.


Dieser und mein letzter Kommentar sind jedoch meine einzigen wirklichen Kritikpunkte an der Idee. Ich denke, ich mag diesen Vorschlag vielleicht nicht, aber ich denke immer noch, dass es ein Gesamtsieg für Go ist. Diese Lösung fühlt sich immer noch Go-ähnlicher an als die anderen Lösungen. Wenn dies hinzugefügt würde, wäre ich glücklich, aber ich denke, dass es noch verbessert werden kann, ich bin mir nur nicht sicher, wie.

@buchanae interessant. Wie geschrieben verschiebt es jedoch die Formatierung im fmt-Stil aus einem Paket in die Sprache selbst, was eine Dose mit Würmern öffnet.

Wie geschrieben verschiebt es jedoch die Formatierung im fmt-Stil aus einem Paket in die Sprache selbst, was eine Dose mit Würmern öffnet.

Guter Punkt. Ein einfacheres Beispiel:

a, b, err := myFunc()
check(err, "calling myFunc")

@buchanae Wir haben darüber nachgedacht, eine explizite Fehlerbehandlung direkter mit try zu verbinden - bitte sehen Sie sich das detaillierte Designdokument an, insbesondere den Abschnitt über Design-Iterationen. Ihr spezifischer Vorschlag von check würde nur erlauben, Fehler durch so etwas wie eine fmt.Errorf -ähnliche API (als Teil von check ) zu erweitern, wenn ich das richtig verstehe. Im Allgemeinen möchten die Leute vielleicht alle möglichen Dinge mit Fehlern machen und nicht nur eine neue erstellen, die über ihre Fehlerzeichenfolge auf die ursprüngliche verweist.

Auch dieser Vorschlag versucht nicht, alle Fehlerbehandlungssituationen zu lösen. Ich vermute, in den meisten Fällen macht try Sinn für Code, der jetzt im Wesentlichen so aussieht:

a, b, c, ... err := try(someFunctionCall())
if err != nil {
   return ..., err
}

Es gibt eine Menge Code, der so aussieht. Und nicht jedes Stück Code, das so aussieht, benötigt mehr Fehlerbehandlung. Und wo defer nicht richtig ist, kann man immer noch eine if -Anweisung verwenden.

Ich folge dieser Zeile nicht:

defer fmt.HandleErrorf(&err, “foobar”)

Es lässt den eingehenden Fehler auf den Boden fallen, was ungewöhnlich ist. Soll es eher so verwendet werden?

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

Die Vervielfältigung von err ist etwas stotternd. Dies bezieht sich nicht wirklich direkt auf den Vorschlag, sondern nur auf eine Randbemerkung zum Dokument.

Ich teile die beiden von @buchanae geäußerten Bedenken bezüglich benannter Rückgaben und kontextbezogener Fehler.

Ich finde benannte Rückgaben ein bisschen lästig, wie es ist; Ich denke, sie sind nur als Dokumentation wirklich nützlich. Sich stärker auf sie zu stützen, ist eine Sorge. Tut mir leid, dass ich so vage bin. Ich werde darüber mehr nachdenken und einige konkretere Gedanken liefern.

Ich denke, es gibt wirklich Bedenken, dass die Leute danach streben, ihren Code so zu strukturieren, dass try verwendet werden kann, und daher vermeiden, Kontext zu Fehlern hinzuzufügen. Dies ist ein besonders seltsamer Zeitpunkt, um dies einzuführen, da wir gerade jetzt bessere Möglichkeiten bieten, Fehlern durch offizielle Fehlerumbruchfunktionen Kontext hinzuzufügen.

Ich denke, dass try wie vorgeschlagen einige Codes erheblich schöner macht. Hier ist eine Funktion, die ich mehr oder weniger zufällig aus der Codebasis meines aktuellen Projekts ausgewählt habe, wobei einige der Namen geändert wurden. Besonders beeindruckt bin ich davon, wie try beim Zuweisen zu Struct-Feldern funktioniert. (Das setzt voraus, dass ich den Vorschlag richtig gelesen habe und dass dies funktioniert?)

Der vorhandene Code:

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations)
        if err != nil {
                return nil, err
        }
        t := &Thing{
                thingy: thingy,
        }
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil {
                return nil, err
        }
        t.initOtherThing()
        return t, nil
}

Mit try :

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Kein Verlust an Lesbarkeit, außer vielleicht, dass es weniger offensichtlich ist, dass newScanner fehlschlagen könnte. Aber in einer Welt mit try Go würden Programmierer empfindlicher auf seine Anwesenheit reagieren.

@josharian In Bezug auf main , das ein error zurückgibt: Es scheint mir, dass Ihre kleine Hilfsfunktion alles ist, was benötigt wird, um den gleichen Effekt zu erzielen. Ich bin mir nicht sicher, ob das Ändern der Signatur von main gerechtfertigt ist.

Bezüglich des "foobar"-Beispiels: Es ist nur ein schlechtes Beispiel. Ich sollte es wahrscheinlich ändern. Danke, dass du es angesprochen hast.

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

Das kann eigentlich nicht stimmen, da err zu früh ausgewertet wird. Es gibt ein paar Möglichkeiten, dies zu umgehen, aber keiner davon ist so sauber wie das ursprüngliche (ich denke, fehlerhafte) HandleErrorf. Ich denke, es wäre gut, ein oder zwei realistischere Beispiele für eine Hilfsfunktion zu haben.

BEARBEITEN: Dieser frühe Evaluierungsfehler ist in einem Beispiel vorhanden
am Ende des Dokuments:

defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)

@adg Ja, try kann so verwendet werden, wie Sie es in Ihrem Beispiel verwenden. Ich lasse Ihre Kommentare zu den benannten Renditen so stehen, wie sie sind.

Die Leute möchten vielleicht alle möglichen Dinge mit Fehlern tun und nicht nur eine neue erstellen, die über ihre Fehlerzeichenfolge auf die ursprüngliche verweist.

try versucht nicht, all die Dinge zu handhaben, die Leute mit Fehlern machen wollen, sondern nur die, die wir auf praktische Weise wesentlich einfacher machen können. Ich glaube, mein check -Beispiel geht in die gleiche Richtung.

Meiner Erfahrung nach ist die häufigste Form von Fehlerbehandlungscode Code, der im Wesentlichen einen Stack-Trace hinzufügt, manchmal mit zusätzlichem Kontext. Ich habe festgestellt, dass der Stack-Trace für das Debugging sehr wichtig ist, wo ich einer Fehlermeldung durch den Code folge.

Aber vielleicht werden andere Vorschläge allen Fehlern Stacktraces hinzufügen? Ich habe den Überblick verloren.

In dem von @adg gegebenen Beispiel gibt es zwei mögliche Fehler, aber keinen Kontext. Wenn newScanner und RunMigrations selbst keine Meldungen liefern, die Sie darauf hinweisen, welche falsch gelaufen ist, dann müssen Sie raten.

In dem von @adg gegebenen Beispiel gibt es zwei mögliche Fehler, aber keinen Kontext. Wenn newScanner und RunMigrations selbst keine Meldungen liefern, die Sie darüber informieren, bei welcher Methode ein Fehler aufgetreten ist, müssen Sie raten.

Das ist richtig, und das ist die Designentscheidung, die wir in diesem speziellen Codestück getroffen haben. Wir verpacken Fehler häufig in anderen Teilen des Codes.

Ich teile die Sorge wie @deanveloper und andere, dass dies das Debuggen erschweren könnte. Es ist wahr, dass wir uns dafür entscheiden können, es nicht zu verwenden, aber die Stile von Abhängigkeiten von Drittanbietern sind nicht unter unserer Kontrolle.
Wenn weniger sich wiederholendes if err := ... { return err } der Hauptpunkt ist, frage ich mich, ob eine "bedingte Rückgabe" ausreichen würde, wie https://github.com/golang/go/issues/27794 vorgeschlagen hat.

        return nil, err if f, err := os.Open(...)
        return nil, err if _, err := os.Write(...)

Ich denke, ? würde besser passen als try , und es wäre auch schwierig, defer immer nach Fehlern suchen zu müssen.

Dies schließt auch die Tore für Ausnahmen mit try/catch für immer.

Dies schließt auch die Tore für Ausnahmen, die try/catch für immer verwenden.

Ich bin _mehr_ damit einverstanden.

Ich stimme einigen der oben geäußerten Bedenken bezüglich des Hinzufügens von Kontext zu einem Fehler zu. Ich versuche langsam, davon abzukommen, nur einen Fehler zurückzugeben, um ihn immer mit einem Kontext zu dekorieren und ihn dann zurückzugeben. Mit diesem Vorschlag muss ich meine Funktion komplett ändern, um benannte Rückgabeparameter zu verwenden (was ich seltsam finde, weil ich kaum nackte Rückgaben verwende).

Wie @griesemer sagt:

Auch dieser Vorschlag versucht nicht, alle Fehlerbehandlungssituationen zu lösen. Ich vermute, in den meisten Fällen macht try Sinn für Code, der jetzt im Grunde so aussieht:
a, b, c, ... err := try(someFunctionCall())
wenn err != nil {
zurück ..., äh
}
Es gibt eine Menge Code, der so aussieht. Und nicht jedes Stück Code, das so aussieht, benötigt mehr Fehlerbehandlung. Und wo defer nicht richtig ist, kann man immer noch eine if-Anweisung verwenden.

Ja, aber sollte guter, idiomatischer Code ihre Fehler nicht immer umhüllen/dekorieren? Ich glaube, das ist der Grund, warum wir verfeinerte Fehlerbehandlungsmechanismen einführen, um Kontext-/Wrap-Fehler in stdlib hinzuzufügen. Wie ich sehe, scheint dieser Vorschlag nur den grundlegendsten Anwendungsfall zu berücksichtigen.

Darüber hinaus spricht dieser Vorschlag nur den Fall an, mehrere mögliche Fehlerrückgabestellen an einem _einzelnen Ort_ zu verpacken/dekorieren, wobei benannte Parameter mit einem Verzögerungsaufruf verwendet werden.

Aber es tut nichts für den Fall, wenn man verschiedene Kontexte zu verschiedenen Fehlern in einer einzigen Funktion hinzufügen muss. Zum Beispiel ist es sehr wichtig, die DB-Fehler zu dekorieren, um mehr Informationen darüber zu erhalten, woher sie kommen (vorausgesetzt, es gibt keine Stack-Traces).

Dies ist ein Beispiel für einen echten Code, den ich habe -

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    if err != nil {
        return err
    }
    var res int64
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table: %w", err)
    }

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table2: %w", err)
    }
    return tx.Commit()
}

Laut Vorschlag:

Wenn eine Fehlererweiterung oder -umhüllung gewünscht wird, gibt es zwei Ansätze: Bleiben Sie bei der bewährten if-Anweisung oder „deklarieren“ Sie alternativ einen Fehlerbehandler mit einer defer-Anweisung:

Ich denke, dies wird in die Kategorie "bleib bei der bewährten if-Anweisung" fallen. Ich hoffe, dass der Vorschlag verbessert werden kann, um auch dies anzugehen.

Ich empfehle dem Go-Team dringend, Generika zu priorisieren , da Go dort die meiste Kritik hört, und auf die Fehlerbehandlung zu warten. Die heutige Technik ist nicht so schmerzhaft (obwohl go fmt sie auf einer Linie sitzen lassen sollte).

Das try() Konzept hat alle Probleme von check von check/handle:

  1. Es liest sich nicht wie Go. Die Leute wollen eine Zuweisungssyntax ohne den anschließenden Nulltest, da das wie Go aussieht. Dreizehn separate Antworten auf check/handle legten dies nahe; siehe _Wiederkehrende Themen_ hier:
    https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

    f, #      := os.Open(...) // return on error
    f, #panic := os.Open(...) // panic on error
    f, #hname := os.Open(...) // invoke named handler on error
    // # is any available symbol or unambiguous pair
    
  2. Das Verschachteln von Funktionsaufrufen, die Fehler zurückgeben, verschleiert die Reihenfolge der Operationen und behindert das Debuggen. Der Sachverhalt beim Auftreten eines Fehlers und damit die Aufrufreihenfolge sollte klar sein, ist es hier aber nicht:
    try(step4(try(step1()), try(step3(try(step2())))))
    Denken Sie jetzt daran, dass die Sprache Folgendes verbietet:
    f(t ? a : b) und f(a++)

  3. Es wäre trivial, Fehler ohne Kontext zurückzugeben. Ein Schlüsselgrund für Check/Handle war die Förderung der Kontextualisierung.

  4. Es ist an den Typ error und den letzten Rückgabewert gebunden. Wenn wir andere Rückgabewerte/-typen auf Ausnahmezustände untersuchen müssen, sind wir wieder bei: if errno := f(); errno != 0 { ... }

  5. Es bietet nicht mehrere Wege. Code, der Speicher- oder Netzwerk-APIs aufruft, behandelt solche Fehler anders als Fehler aufgrund falscher Eingaben oder unerwarteter interner Zustände. Mein Code macht eines davon viel öfter als return err :

    • log.Fatal()
    • panic() für Fehler, die niemals auftreten sollten
    • Protokollieren Sie eine Nachricht und versuchen Sie es erneut

@gopherbot fügt Go2, LanguageChange hinzu

Wie wäre es, wenn Sie nur ? verwenden, um das Ergebnis genau wie rust auszupacken

Der Grund, warum wir dem Aufruf von try() skeptisch gegenüberstehen, könnte in zwei impliziten Bindungen liegen. Wir können die Bindung für den Rückgabewertfehler und die Argumente für try() nicht sehen. Für try() können wir eine Regel aufstellen, dass wir try() mit einer Argumentfunktion verwenden müssen, die einen Fehler in den Rückgabewerten hat. Aber die Bindung an Rückgabewerte ist nicht. Ich denke also, dass mehr Ausdruck erforderlich ist, damit Benutzer verstehen, was dieser Code tut.

func doSomething() (int, %error) {
  f := try(foo())
  ...
}
  • Wir können try() nicht verwenden, wenn doSomething nicht %error in Rückgabewerten hat.
  • Wir können try() nicht verwenden, wenn foo() keinen Fehler im letzten Rückgabewert hat.

Es ist schwierig, der bestehenden Syntax neue Anforderungen/Features hinzuzufügen.

Um ehrlich zu sein, denke ich, dass foo() auch %error haben sollte.

Fügen Sie eine weitere Regel hinzu

  • %error kann nur einer in der Rückgabewertliste einer Funktion sein.

Im ausführlichen Designdokument ist mir aufgefallen, dass in einer früheren Iteration vorgeschlagen wurde, einen Fehlerbehandler an die eingebaute Funktion try zu übergeben. So was:

handler := func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
}

f := try(os.Open(filename), handler)  

oder noch besser so:

f := try(os.Open(filename), func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
})  

Obwohl dies, wie das Dokument feststellt, mehrere Fragen aufwirft, denke ich, dass dieser Vorschlag weitaus wünschenswerter und nützlicher wäre, wenn er diese Möglichkeit beibehalten hätte, optional eine solche Fehlerbehandlungsfunktion oder -schließung zu spezifizieren.

Zweitens stört es mich nicht, dass eine eingebaute Funktion dazu führen kann, dass die Funktion zurückkehrt, aber, um ein bisschen Fahrrad zu werfen, der Name „versuchen“ ist zu kurz, um darauf hinzuweisen, dass es eine Rückkehr verursachen kann. Daher scheint mir ein längerer Name wie attempt besser zu sein.

BEARBEITEN: Drittens sollte Go-Sprache idealerweise zuerst Generika erhalten, wobei ein wichtiger Anwendungsfall die Möglichkeit wäre, diese Try-Funktion als Generika zu implementieren, damit das Bikeshedding enden kann und jeder die Fehlerbehandlung erhalten kann, die er selbst bevorzugt.

Hacker-News haben einen Sinn: try verhält sich nicht wie eine normale Funktion (sie kann zurückkehren), also ist es nicht gut, ihr eine funktionsähnliche Syntax zu geben. Eine return oder defer Syntax wäre besser geeignet:

func CopyFile(src, dst string) (err error) {
        r := try os.Open(src)
        defer r.Close()

        w := try os.Create(dst)
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try io.Copy(w, r)
        try w.Close()
        return nil
}

@sheerun Das übliche Gegenargument dazu ist, dass panic auch eine eingebaute Funktion ist, die den Kontrollfluss verändert. Ich persönlich bin dagegen, aber es ist richtig.

  1. In Anlehnung an @deanveloper oben und ähnliche Kommentare anderer befürchte ich sehr, dass wir die Kosten für das Hinzufügen eines neuen, etwas subtilen und – insbesondere wenn es in andere Funktionsaufrufe eingebettet ist – leicht zu übersehenden Schlüsselworts unterschätzen, das die Steuerung des Aufrufstapels verwaltet fließen. panic(...) ist eine relativ klare Ausnahme (Wortspiel nicht beabsichtigt) von der Regel, dass return der einzige Ausweg aus einer Funktion ist. Ich denke nicht, dass wir seine Existenz als Rechtfertigung dafür verwenden sollten, ein drittes hinzuzufügen.
  2. Dieser Vorschlag würde die Rückgabe eines nicht umschlossenen Fehlers als Standardverhalten kanonisieren und Umbruchfehler als etwas degradieren, für das Sie sich mit zusätzlicher Zeremonie entscheiden müssen. Aber nach meiner Erfahrung ist das genau rückwärts zu guter Praxis. Ich hoffe, dass ein Vorschlag in diesem Bereich es einfacher oder zumindest nicht schwieriger machen würde, Kontextinformationen zu Fehlern an der Fehlerstelle hinzuzufügen.

vielleicht können wir mit dieser Semantik eine Variante mit optionaler Erweiterungsfunktion hinzufügen, etwa tryf :

func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)

übersetzt dies

x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })

das mögen

t1, … tn, te := f()
if te != nil {
    if fn != nil {
        te = fn(te)
    }
    err = te
    return
}

Da dies eine explizite Wahl ist (anstatt try zu verwenden), können wir vernünftige Antworten auf die Fragen in der früheren Version dieses Designs finden. Wenn die Erweiterungsfunktion beispielsweise nil ist, tun Sie nichts und geben Sie nur den ursprünglichen Fehler zurück.

Ich befürchte, dass try die traditionelle Fehlerbehandlung verdrängen wird und dass dies das Kommentieren von Fehlerpfaden dadurch erschweren wird.

Code, der Fehler behandelt, indem er Nachrichten protokolliert und Telemetriezähler aktualisiert, wird sowohl von Linters als auch von Entwicklern als fehlerhaft oder unangemessen angesehen, die erwarten, dass alles try wird.

a, b, err := doWork()
if err != nil {
  updateCounters()
  writeLogs()
  return err
}

Go ist eine äußerst soziale Sprache mit gemeinsamen Redewendungen, die durch Werkzeuge (fmt, lint usw.) erzwungen werden. Bitte behalten Sie die gesellschaftlichen Auswirkungen dieser Idee im Hinterkopf - es wird eine Tendenz geben, sie überall verwenden zu wollen.

@Politiker , Entschuldigung, aber das gesuchte Wort ist nicht _social_, sondern _opinionated_. Go ist eine rechthaberische Programmiersprache. Im Übrigen stimme ich dem, worauf Sie hinauswollen, weitgehend zu.

@beoran Community-Tools wie Godep und die verschiedenen Linters zeigen, dass Go sowohl eigensinnig als auch sozial ist, und viele der Dramen mit der Sprache stammen aus dieser Kombination. Hoffentlich können wir uns beide darauf einigen, dass try nicht das nächste Drama sein sollte.

@politiker Danke für die Klarstellung, so hatte ich das nicht verstanden. Ich kann sicherlich zustimmen, dass wir versuchen sollten, Drama zu vermeiden.

Ich bin verwirrt darüber.

Aus dem Blog: Fehler sind Werte , aus meiner Sicht sollen sie geschätzt und nicht ignoriert werden.

Und ich glaube, was Rop Pike gesagt hat: "Werte können programmiert werden, und da Fehler Werte sind, können Fehler programmiert werden."

Wir sollten error nicht als exception betrachten, es ist, als würden wir Komplexität nicht nur zum Denken, sondern auch zum Codieren importieren, wenn wir dies tun.

"Verwenden Sie die Sprache, um Ihre Fehlerbehandlung zu vereinfachen." - Rob Pike

Und mehr noch, wir können diese Folie überprüfen

image

Eine Situation, in der ich die Fehlerprüfung über if besonders umständlich finde, ist das Schließen von Dateien (z. B. auf NFS). Ich denke, derzeit sollen wir Folgendes schreiben, ob Fehlerrückgaben von .Close() möglich sind?

r, err := os.Open(src)
if err != nil {
    return err
}
defer func() {
    // maybe check whether a previous error occured?
    return r.Close()
}()

Könnte defer try(r.Close()) eine gute Möglichkeit sein, eine handhabbare Syntax für den Umgang mit solchen Fehlern zu haben? Zumindest wäre es sinnvoll, das Beispiel CopyFile() im Vorschlag irgendwie anzupassen, um Fehler von r.Close() und w.Close() nicht zu ignorieren.

@seehuhn Ihr Beispiel wird nicht kompiliert, da die verzögerte Funktion keinen Rückgabetyp hat.

func doWork() (err error) {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer func() {
    err = r.Close()  // overwrite the return value
  }()
}

Funktioniert wie erwartet. Der Schlüssel ist der benannte Rückgabewert.

Ich mag den Vorschlag, aber ich denke, dass das Beispiel von @seehuhn auch angesprochen werden sollte:

defer try(w.Close())

würde den Fehler von Close() nur zurückgeben, wenn der Fehler nicht bereits gesetzt war.
Dieses Muster wird so oft verwendet...

Ich stimme den Bedenken hinsichtlich des Hinzufügens von Kontext zu Fehlern zu. Ich sehe es als eine der besten Methoden, die Fehlermeldungen sehr freundlich (und klar) hält und den Debug-Prozess vereinfacht.

Das erste, woran ich dachte, war, fmt.HandleErrorf durch eine tryf -Funktion zu ersetzen, die dem Fehler zusätzlichen Kontext voranstellt.

func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)

Zum Beispiel (von einem echten Code, den ich habe):

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil {
        return nil, errors.WithMessage(err, "load config dir")
    }
    b := bytes.NewBuffer(nil)
    if err = templates.ExecuteTemplate(b, "main", c); err != nil {
        return nil, errors.WithMessage(err, "execute main template")
    }
    buf, err := format.Source(b.Bytes())
    if err != nil {
        return nil, errors.WithMessage(err, "format main template")
    }
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    if err := ioutil.WriteFile(target, buf, 0644); err != nil {
        return nil, errors.WithMessagef(err, "write file %s", target)
    }
    // ...
}

Kann geändert werden in etwas wie:

func (c *Config) Build() error {
    pkgPath := tryf(c.load(), "load config dir")
    b := bytes.NewBuffer(nil)
    tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
    buf := tryf(format.Source(b.Bytes()), "format main template")
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
    // ...
}

Oder, wenn ich das Beispiel von @agnivade nehme:

func (p *pgStore) DoWork() (err error) {
    tx := tryf(p.handle.Begin(), "begin transaction")
        defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    var res int64
    tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
    _, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
    return tryf(tx.Commit(), "commit transaction")
}

@josharian hat jedoch einen guten Punkt angesprochen, der mich bei dieser Lösung zögern lässt:

Wie geschrieben verschiebt es jedoch die Formatierung im fmt-Stil aus einem Paket in die Sprache selbst, was eine Dose mit Würmern öffnet.

Ich bin mit diesem Vorschlag voll und ganz einverstanden und kann seine Vorteile anhand einer Reihe von Beispielen erkennen.

Meine einzige Sorge bei dem Vorschlag ist die Benennung von try , ich habe das Gefühl, dass seine Konnotationen mit anderen Sprachen die Wahrnehmung der Entwickler über den Zweck verzerren können, wenn sie aus anderen Sprachen stammen. Hier kommt Java zum Einsatz.

Für mich würde ich bevorzugen, dass das Builtin pass heißt. Ich habe das Gefühl, dass dies eine bessere Darstellung dessen gibt, was passiert. Schließlich behandeln Sie den Fehler nicht, sondern geben ihn zurück, damit er vom Aufrufer behandelt werden kann. try vermittelt den Eindruck, dass der Fehler behandelt wurde.

Es ist ein Daumen nach unten von mir, hauptsächlich weil das Problem, das es ansprechen soll ("die Boilerplate if-Anweisungen, die typischerweise mit der Fehlerbehandlung verbunden sind") einfach kein Problem für mich ist. Wenn alle Fehlerprüfungen einfach if err != nil { return err } wären, dann könnte ich einen gewissen Wert darin sehen, dafür syntaktischen Zucker hinzuzufügen (obwohl Go von Natur aus eine relativ zuckerfreie Sprache ist).

Tatsächlich variiert das, was ich im Falle eines Nicht-Null-Fehlers tun möchte, ziemlich stark von einer Situation zur nächsten. Vielleicht möchte ich t.Fatal(err) . Vielleicht möchte ich eine schmückende Nachricht hinzufügen return fmt.Sprintf("oh no: %v", err) . Vielleicht protokolliere ich einfach den Fehler und fahre fort. Vielleicht setze ich ein Fehler-Flag für mein SafeWriter-Objekt und fahre fort, indem ich das Flag am Ende einer Reihe von Operationen überprüfe. Vielleicht muss ich andere Maßnahmen ergreifen. Nichts davon kann mit try automatisiert werden. Wenn also das Argument für try darin besteht, dass alle if err != nil -Blöcke eliminiert werden, ist dieses Argument nicht gültig.

Wird es _some_ von ihnen eliminieren? Sicher. Ist das ein attraktives Angebot für mich? Meh. Ich mache mir wirklich keine Sorgen. Für mich gehört if err != nil einfach zu Go, wie die geschweiften Klammern oder defer . Ich verstehe, dass es für Leute, die neu bei Go sind, ausführlich und sich wiederholend aussieht, aber Leute, die neu bei Go sind, sind aus einer ganzen Reihe von Gründen nicht am besten in der Lage, dramatische Änderungen an der Sprache vorzunehmen.

Die Messlatte für signifikante Änderungen an Go war traditionell, dass die vorgeschlagene Änderung ein Problem lösen muss, das (A) signifikant ist, (B) viele Menschen betrifft und (C) durch den Vorschlag gut gelöst ist. Ich bin in keinem dieser drei Kriterien überzeugt. Ich bin ziemlich zufrieden mit der Fehlerbehandlung von Go, so wie sie ist.

Um @peterbourgon und @deanveloper zu wiederholen, eines meiner Lieblingsdinge an Go ist, dass der Codefluss klar ist und panic() nicht wie ein Standard-Flusssteuerungsmechanismus behandelt wird, wie es in Python der Fall ist.

Was die Debatte über Panik betrifft, so erscheint panic() fast immer alleine in einer Zeile, weil es keinen Wert hat. Du kannst nicht fmt.Println(panic("oops")) . Dies erhöht seine Sichtbarkeit enorm und macht es weit weniger vergleichbar mit try() , als die Leute glauben.

Wenn es ein anderes Flusskontrollkonstrukt für Funktionen geben soll, würde ich _weit_ vorziehen, dass es sich um eine Anweisung handelt, die garantiert das Element ganz links in einer Zeile ist.

Eines der Beispiele im Vorschlag bringt das Problem für mich auf den Punkt:

func printSum(a, b string) error {
        fmt.Println(
                "result:",
                try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
        )
        return nil
}

Der Kontrollfluss wird wirklich weniger offensichtlich und sehr undurchsichtig.

Dies widerspricht auch der ursprünglichen Absicht von Rob Pike, dass alle Fehler explizit behandelt werden müssen.

Während eine Reaktion darauf sein kann "dann nicht verwenden", ist das Problem -- andere Bibliotheken werden es verwenden, und das Debuggen, Lesen und Verwenden wird problematischer. Dies wird mein Unternehmen dazu motivieren, niemals go 2 zu übernehmen und nur Bibliotheken zu verwenden, die try nicht verwenden. Wenn ich damit nicht alleine bin, könnte es zu einer Division a-la Python 2/3 führen.

Außerdem impliziert die Benennung von try automatisch, dass schließlich catch in der Syntax auftaucht, und wir werden wieder Java sein.

Aus diesem Grund bin ich _stark_ gegen diesen Vorschlag.

Ich mag den Namen try nicht. Es impliziert einen _Versuch_, etwas mit einem hohen Misserfolgsrisiko zu tun (ich habe möglicherweise eine kulturelle Voreingenommenheit gegen _try_, da ich kein englischer Muttersprachler bin), während stattdessen try verwendet würde, falls wir seltene Fehler erwarten (Motivation für den Wunsch, die Ausführlichkeit der Fehlerbehandlung zu reduzieren) und optimistisch sind. Außerdem _fängt_ try in diesem Vorschlag tatsächlich einen Fehler, um es vorzeitig zurückzugeben. Ich mag den pass Vorschlag von @HiImJC.

Neben dem Namen finde ich es unangenehm, return -ähnliche Anweisungen jetzt in der Mitte von Ausdrücken zu haben. Dies bricht den Go-Flow-Stil. Es wird Code-Reviews schwieriger machen.

Im Allgemeinen finde ich, dass dieser Vorschlag nur dem faulen Programmierer zugute kommt, der jetzt eine Waffe für kürzeren Code und noch weniger Grund hat, sich die Mühe zu machen, Fehler zu umschließen. Da es auch Überprüfungen erschweren wird (Rückkehr in der Mitte des Ausdrucks), denke ich, dass dieser Vorschlag gegen das Ziel der „Programmierung im Maßstab“ von Go verstößt.

Eine meiner Lieblingssachen an Go, die ich im Allgemeinen sage, wenn ich die Sprache beschreibe, ist, dass es für die meisten Dinge nur einen Weg gibt, Dinge zu tun. Dieser Vorschlag widerspricht diesem Prinzip ein wenig, indem er mehrere Möglichkeiten bietet, dasselbe zu tun. Ich persönlich denke, dass dies nicht notwendig ist und dass es die Einfachheit und Lesbarkeit der Sprache eher beeinträchtigen als verbessern würde.

Ich finde diesen Vorschlag insgesamt gut. Die Interaktion mit defer scheint ausreichend zu sein, um einen Fehler auf ergonomische Weise zurückzugeben und gleichzeitig zusätzlichen Kontext hinzuzufügen. Obwohl es schön wäre, den Haken anzugehen, auf den @josharian hingewiesen hat, wie man den ursprünglichen Fehler in die umschlossene Fehlermeldung einfügt.

Was fehlt, ist eine ergonomische Art und Weise, wie dies mit dem/den Fehlerinspektionsvorschlag(en) auf dem Tisch interagiert. Ich glaube, APIs sollten sehr bewusst sein, welche Arten von Fehlern sie zurückgeben, und der Standardwert sollte wahrscheinlich "zurückgegebene Fehler sind in keiner Weise inspizierbar" sein. Es sollte dann einfach sein, in einen Zustand zu gelangen, in dem Fehler auf präzise Weise inspizierbar sind, wie durch die Funktionssignatur dokumentiert ("Es meldet einen Fehler der Art X in Umstand A und einen Fehler der Art Y in Umstand B").

Leider macht dieser Vorschlag ab sofort die ergonomischste Option zur unerwünschtesten (für mich); blindes Durchlaufen beliebiger Fehlerarten. Ich denke, das ist nicht wünschenswert, weil es dazu anregt, nicht darüber nachzudenken, welche Art von Fehlern Sie zurückgeben und wie Benutzer Ihrer API sie verwenden werden. Die zusätzliche Bequemlichkeit dieses Vorschlags ist sicherlich schön, aber ich fürchte, er wird schlechtes Verhalten fördern, da die wahrgenommene Bequemlichkeit den wahrgenommenen Wert überwiegen wird, sorgfältig darüber nachzudenken, welche Fehlerinformationen Sie bereitstellen (oder lecken).

Ein Pflaster wäre, wenn Fehler, die von try zurückgegeben werden, in Fehler umgewandelt werden, die nicht "auspackbar" sind. Leider hat dies auch ziemlich schwerwiegende Nachteile, da es dazu führt, dass defer die Fehler selbst nicht untersuchen kann. Außerdem verhindert es die Verwendung, bei der try tatsächlich einen Fehler der erwünschten Art zurückgibt (d. h. Anwendungsfälle, bei denen try vorsichtig und nicht fahrlässig verwendet wird).

Eine andere Lösung wäre, die (verworfene) Idee, ein optionales zweites Argument für try zu haben, um die Fehlerart(en) zu definieren/auf die weiße Liste zu setzen, die von dieser Site zurückgegeben werden können. Dies ist etwas mühsam, da wir zwei verschiedene Möglichkeiten haben, eine "Fehlerart" zu definieren, entweder nach Wert ( io.EOF usw.) oder nach Typ ( *os.PathError , *exec.ExitError ). Es ist einfach, Fehlerarten anzugeben, die Werte als Argumente für eine Funktion sind, aber es ist schwieriger, Typen anzugeben. Ich bin mir nicht sicher, wie ich damit umgehen soll, aber wirf die Idee raus.

Das Problem, auf das @josharian hingewiesen hat, kann vermieden werden, indem die Auswertung von err verzögert wird:

defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()

Sieht nicht toll aus, sollte aber funktionieren. Ich würde es jedoch vorziehen, wenn dies durch Hinzufügen eines neuen Formatierungsverbs/-flags für Fehlerzeiger oder vielleicht für Zeiger im Allgemeinen behoben werden kann, das den dereferenzierten Wert wie bei einfachem %v druckt. Nennen wir es für das Beispiel %*v :

defer fmt.HandleErrorf(&err, "oops: %*v", &err)

Abgesehen von dem Haken beiseite, ich denke, dass dieser Vorschlag vielversprechend aussieht, aber es scheint entscheidend zu sein, die Ergonomie des Hinzufügens von Kontext zu Fehlern in Schach zu halten.

Bearbeiten:

Ein anderer Ansatz besteht darin, den Fehlerzeiger in eine Struktur einzuschließen, die Stringer implementiert:

type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }

...

defer handleErrorf(&err, "oops: %v", wraperr{&err})

Paar Dinge aus meiner Sicht. Warum sind wir so besorgt darüber, ein paar Codezeilen einzusparen? Ich betrachte dies ähnlich wie kleine Funktionen als schädlich .

Außerdem finde ich, dass ein solcher Vorschlag die Verantwortung für die korrekte Behandlung des Fehlers einer "Magie" entziehen würde, von der ich befürchte, dass sie nur missbraucht wird, und Faulheit fördern, die zu Code von schlechter Qualität und Fehlern führt.

Der Vorschlag hat, wie angegeben, auch eine Reihe unklarer Verhaltensweisen, sodass dies bereits problematischer ist als _explizit_ zusätzliche ~ 3 Zeilen, die klarer sind.

Wir verwenden das Zurückstellungsmuster derzeit nur sparsam im Haus. Es gibt hier einen Artikel, der ähnlich gemischt aufgenommen wurde, als wir ihn schrieben – https://bet365techblog.com/better-error-handling-in-go

Wir haben es jedoch in Erwartung des check / handle -Vorschlags verwendet.

Check/handle war ein viel umfassenderer Ansatz, um die Fehlerbehandlung in go prägnanter zu gestalten. Sein handle -Block behielt den gleichen Funktionsumfang bei wie der, in dem er definiert wurde, wohingegen alle defer -Anweisungen neue Kontexte mit einem noch so großen Overhead sind. Dies schien eher im Einklang mit den Idiomen von go zu stehen, da Sie, wenn Sie das Verhalten "nur den Fehler zurückgeben, wenn er auftritt" wollten, dies explizit als handle { return err } deklarieren könnten.

Defer ist offensichtlich darauf angewiesen, dass auch die err-Referenz beibehalten wird, aber wir haben gesehen, dass Probleme entstehen, wenn die Fehlerreferenz mit blockbezogenen Variablen verdeckt wird. Es ist also nicht idiotensicher genug, um als Standardmethode zur Fehlerbehandlung im Go angesehen zu werden.

try scheint in diesem Fall nicht allzu viel zu lösen, und ich teile die gleiche Befürchtung wie andere, dass dies einfach zu faulen Implementierungen führen würde oder zu solchen, die das Verzögerungsmuster überbeanspruchen.

Wenn die verzögerungsbasierte Fehlerbehandlung A Thing sein soll, sollte dem Fehlerpaket wahrscheinlich so etwas hinzugefügt werden:

        f := try(os.Create(filename))
        defer errors.Deferred(&err, f.Close)

Das Ignorieren der Fehler verzögerter Close-Anweisungen ist ein ziemlich häufiges Problem. Es sollte ein Standardtool geben, das dabei hilft.

Eine eingebaute Funktion, die zurückkehrt, ist schwerer zu verkaufen als ein Schlüsselwort, das dasselbe tut.
Ich würde es mehr mögen, wenn es ein Schlüsselwort wäre, wie es in Zig[1] ist.

  1. https://ziglang.org/documentation/master/#try

Eingebaute Funktionen, deren Typsignatur nicht mit dem Typsystem der Sprache ausgedrückt werden kann und deren Verhalten verwirrt, was eine Funktion normalerweise ist, scheinen nur eine Notluke zu sein, die wiederholt verwendet werden kann, um die tatsächliche Sprachentwicklung zu vermeiden.

Wir sind es gewohnt, Return-Anweisungen (und Panik) sofort zu erkennen, weil diese Art von Kontrollfluss in Go (und vielen anderen Sprachen) ausgedrückt wird. Es scheint nicht weit hergeholt, dass wir nach einiger Eingewöhnungszeit auch try als sich ändernden Kontrollfluss erkennen werden, genau wie wir es für return tun. Ich habe keinen Zweifel, dass eine gute IDE-Unterstützung auch dabei helfen wird.

Ich halte das für ziemlich weit hergeholt. In Standardcode stimmt ein Return immer mit /^\t*return / überein – es ist ein sehr triviales Muster, das ohne Hilfe mit dem Auge zu erkennen ist. try hingegen kann überall im Code vorkommen, beliebig tief in Funktionsaufrufen verschachtelt. Selbst noch so viel Training wird uns nicht in die Lage versetzen, den gesamten Kontrollfluss in einer Funktion ohne Werkzeugunterstützung sofort zu erkennen.

Darüber hinaus ist ein Feature, das von "guter IDE-Unterstützung" abhängt, in allen Umgebungen im Nachteil, in denen es keine gute IDE-Unterstützung gibt. Code-Review-Tools fallen mir sofort ein – wird Gerrit alle Versuche für mich hervorheben? Was ist mit Leuten, die sich aus verschiedenen Gründen dafür entscheiden, keine IDEs oder ausgefallene Code-Hervorhebungen zu verwenden? Wird acme damit beginnen, try hervorzuheben?

Eine Sprachfunktion sollte von sich aus leicht verständlich sein und nicht von der Unterstützung eines Editors abhängen.

@kungfusheep Ich mag diesen Artikel. Wenn Sie sich um das Einschließen einer Verzögerung kümmern, erhöht sich die Lesbarkeit bereits erheblich, ohne try .

Ich bin in dem Lager, das Fehler in Go nicht wirklich als Problem empfindet. Trotzdem kann if err != nil { return err } bei einigen Funktionen ziemlich stottern. Ich habe Funktionen geschrieben, die nach fast jeder Anweisung eine Fehlerprüfung benötigten, und keine benötigte eine besondere Behandlung außer Wrap und Return. Manchmal gibt es einfach keine clevere Buffer-Struktur, die die Dinge schöner macht. Manchmal ist es nur ein anderer kritischer Schritt nach dem anderen und Sie müssen einfach kurzschließen, wenn etwas schief gelaufen ist.

Obwohl try diesen Code sicherlich viel einfacher und besser lesbar machen würde, während er vollständig abwärtskompatibel wäre, stimme ich zu, dass try kein kritisches Must-Have-Feature ist, also wenn die Leute zu viel Angst davor haben es ist vielleicht am besten, es nicht zu haben.

Die Semantik ist jedoch recht eindeutig. Jedes Mal, wenn Sie try sehen, folgt es entweder dem glücklichen Weg oder es kehrt zurück. Einfacher geht es wirklich nicht.

Dies sieht aus wie ein spezielles Case-Makro.

@dominikh try stimmt immer mit /try\(/ überein, also weiß ich nicht, was Sie wirklich sagen wollen. Es ist genauso durchsuchbar und jeder Editor, von dem ich je gehört habe, hat eine Suchfunktion.

@qrpnxz Ich denke, der Punkt, den er machen wollte, ist nicht, dass Sie nicht programmatisch danach suchen können, sondern dass es schwieriger ist, mit Ihren Augen danach zu suchen. Der reguläre Ausdruck war nur eine Analogie mit Betonung auf /^\t* , was bedeutet, dass sich alle Zeilenumbrüche deutlich dadurch abheben, dass sie am Anfang einer Zeile stehen (ohne führende Leerzeichen).

Wenn man genauer darüber nachdenkt, sollte es ein paar allgemeine Hilfsfunktionen geben. Vielleicht sollten sie in einem Paket namens "deferred" sein.

Wenn Sie den Vorschlag für ein check mit Format adressieren, um die Benennung der Rückgabe zu vermeiden, können Sie dies einfach mit einer Funktion tun, die auf nil prüft, so wie hier

func Format(err error, message string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(...)
}

Dies kann ohne eine benannte Rückgabe wie folgt verwendet werden:

func foo(s string) (int, error) {
    n, err := strconv.Atoi(s)
    try(deferred.Format(err, "bad string %q", s))
    return n, nil
}

Der vorgeschlagene fmt.HandleError könnte stattdessen in das zurückgestellte Paket gestellt werden, und die Hilfsfunktion my errors.Defer könnte deferred.Exec heißen, und es könnte eine bedingte Ausführung für Prozeduren geben, die nur ausgeführt werden, wenn der Fehler nicht null ist.

Wenn Sie es zusammensetzen, erhalten Sie so etwas wie

func CopyFile(src, dst string) (err error) {
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer deferred.Exec(&err, r.Close)

    w := try(os.Create(dst))
    defer deferred.Exec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    try(io.Copy(w, r))

    return nil
}

Ein anderes Beispiel:

func (p *pgStore) DoWork() (err error) {
    tx := try(p.handle.Begin())

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(deferred.Format(err, "insert table")

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(deferred.Format(err, "insert table2"))

    return tx.Commit()
}

Dieser Vorschlag bringt uns von if err != nil überall zu try überall. Es verschiebt das vorgeschlagene Problem und löst es nicht.

Obwohl ich argumentieren würde, dass der aktuelle Fehlerbehandlungsmechanismus zunächst kein Problem darstellt. Wir müssen nur die Werkzeuge und die Überprüfung drumherum verbessern.

Außerdem würde ich argumentieren, dass if err != nil tatsächlich besser lesbar ist als try , weil es die Zeile der Geschäftslogiksprache nicht unübersichtlich macht, sondern direkt darunter sitzt:

file := try(os.OpenFile("thing")) // less readable than, 

file, err := os.OpenFile("thing")
if err != nil {

}

Und wenn Go in seiner Fehlerbehandlung magischer sein sollte, warum sollte man es nicht einfach vollständig besitzen. Zum Beispiel kann Go implizit das eingebaute try aufrufen, wenn ein Benutzer keinen Fehler zuweist. Zum Beispiel:

func getString() (string, error) { ... }

func caller() {
  defer func() {
    if err != nil { ... } // whether `err` must be defined or not is not shown in this example. 
  }

  // would call try internally, because a user is not 
  // assigning an error value. Also, it can add a compile error
  // for "defined and not used err value" if the user does not 
  // handle the error. 
  str := getString()
}

Für mich würde das das Redundanzproblem tatsächlich auf Kosten der Magie und der möglichen Lesbarkeit lösen.

Daher schlage ich vor, dass wir entweder das „Problem“ wie im obigen Beispiel wirklich lösen oder die aktuelle Fehlerbehandlung beibehalten, aber anstatt die Sprache zu ändern, um Redundanz und Wrapping zu lösen, ändern wir nicht die Sprache, sondern verbessern die Tools und die Überprüfung von Code, um das Erlebnis zu verbessern.

Zum Beispiel gibt es in VSCode ein Snippet namens iferr , wenn Sie es eingeben und die Eingabetaste drücken, wird es zu einer vollständigen Fehlerbehandlungsanweisung erweitert ... daher fühlt sich das Schreiben für mich nie ermüdend an und das spätere Lesen ist besser .

@josharian

Obwohl es keine „bescheidene Bibliotheksänderung“ wäre, könnten wir auch in Betracht ziehen, den Fehler von func main() zu akzeptieren.

Das Problem dabei ist, dass nicht alle Plattformen eine klare Semantik darüber haben, was das bedeutet. Ihre Umschreibung funktioniert gut in "traditionellen" Go-Programmen, die auf einem vollständigen Betriebssystem laufen - aber sobald Sie Mikrocontroller-Firmware oder auch nur WebAssembly schreiben, ist nicht ganz klar, was os.Exit(1) bedeuten würde. Derzeit ist os.Exit ein Bibliotheksaufruf, daher steht es Go-Implementierungen frei, ihn einfach nicht bereitzustellen. Die Form von main ist jedoch ein Sprachproblem.


Eine Frage zum Vorschlag, die wahrscheinlich am besten mit "Nein" beantwortet werden kann: Wie interagiert try mit variadischen Argumenten? Es ist der erste Fall einer variadischen (ish) Funktion, die ihre variadische Funktion nicht im letzten Argument hat. Ist das erlaubt:

var e []error
try(e...)

Abgesehen davon, warum du das jemals tun würdest. Ich vermute, die Antwort ist "nein" (ansonsten lautet die Fortsetzung "was ist, wenn die Länge des erweiterten Slice 0 ist). Ich bringe das nur zur Sprache, damit es bei der Formulierung der Spezifikation berücksichtigt werden kann.

  • Einige der größten Features von go sind, dass aktuelle Builtins einen klaren Kontrollfluss gewährleisten, die Fehlerbehandlung explizit ist und gefördert wird und Entwickler dringend davon abgehalten werden, „magischen“ Code zu schreiben. Der try -Vorschlag steht nicht im Einklang mit diesen Grundprinzipien, da er die Kurzschrift auf Kosten der Kontrollfluss-Lesbarkeit fördert.
  • Wenn dieser Vorschlag angenommen wird, sollten Sie vielleicht erwägen, das eingebaute try statt einer Funktion zu einer Anweisung zu machen. Dann ist es konsistenter mit anderen Kontrollflussanweisungen wie if . Zusätzlich verbessert das Entfernen der verschachtelten Klammern die Lesbarkeit geringfügig.
  • Nochmals, wenn der Vorschlag angenommen wird, implementieren Sie ihn vielleicht, ohne defer oder ähnliches zu verwenden. Es kann bereits nicht in reinem Go implementiert werden (wie von anderen betont), daher kann es auch eine effizientere Implementierung unter der Haube verwenden.

Ich sehe dabei zwei Probleme:

  1. Es fügt eine Menge Code in Funktionen ein. Das fügt eine Menge zusätzlicher kognitiver Belastung hinzu, wenn Sie versuchen, den Code in Ihrem Kopf zu analysieren.
  1. Es gibt uns Stellen, an denen der Code aus der Mitte einer Anweisung aussteigen kann.

Nummer 2 finde ich viel schlimmer. Alle Beispiele hier sind einfache Aufrufe, die einen Fehler zurückgeben, aber viel heimtückischer ist Folgendes:

func doit(abc string) error {
    a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
    log.Println(a)
    return nil
}

Dieser Code kann mitten in diesem sprintf beendet werden, und es wird SUPER einfach sein, diese Tatsache zu übersehen.

Meine Stimme ist nein. Go-Code wird dadurch nicht besser. Das Lesen wird dadurch nicht einfacher. Robuster wird es dadurch nicht.

Ich habe es bereits gesagt, und dieser Vorschlag veranschaulicht es - ich glaube, dass 90% der Beschwerden über Go lauten: "Ich möchte keine if-Anweisung oder Schleife schreiben" . Dies entfernt einige sehr einfache if-Anweisungen, erhöht jedoch die kognitive Belastung und macht es einfach, Austrittspunkte für eine Funktion zu übersehen.

Ich möchte nur darauf hinweisen, dass Sie dies im Wesentlichen nicht verwenden konnten und es für neue Benutzer oder beim Unterrichten verwirrend sein könnte. Offensichtlich gilt dies für jede Funktion, die keinen Fehler zurückgibt, aber ich denke, main ist etwas Besonderes, da es in vielen Beispielen vorkommt.

func main() {
    f := try(os.Open("foo.txt"))
    defer f.Close()
}

Ich bin mir nicht sicher, ob es auch akzeptabel wäre, in Main Panik zu versuchen.

Außerdem wäre es in Tests ( func TestFoo(t* testing.T) ) nicht besonders nützlich, was unglücklich ist :(

Das Problem, das ich damit habe, ist, dass davon ausgegangen wird, dass Sie den Fehler immer nur zurückgeben möchten, wenn er auftritt. Wenn Sie dem Fehler möglicherweise Kontext hinzufügen und ihn zurückgeben möchten, oder wenn Sie sich einfach anders verhalten möchten, wenn ein Fehler auftritt. Vielleicht hängt das von der Art des zurückgegebenen Fehlers ab.

Ich würde so etwas wie ein Try/Catch bevorzugen, wie es aussehen könnte

Angenommen, foo() ist definiert als

func foo() (int, error) {}

Das könntest du dann machen

n := try(foo()) {
    case FirstError:
        // do something based on FirstError
    case OtherError:
        // do something based on OtherError
    default:
        // default behavior for any other error
}

Was übersetzt bedeutet

n, err := foo()
if errors.Is(err, FirstError) {
    // do something based on FirstError
if errors.Is(err, OtherError) {
    // do something based on OtherError
} else {
    // default behavior for any other error
}

Für mich ist die Fehlerbehandlung einer der wichtigsten Teile einer Codebasis.
Bereits zu viel Go-Code ist if err != nil { return err } , der einen Fehler aus der Tiefe des Stacks zurückgibt, ohne zusätzlichen Kontext hinzuzufügen, oder sogar (möglicherweise) noch schlimmer, wenn Kontext hinzugefügt wird, indem der zugrunde liegende Fehler mit fmt.Errorf Wrapping maskiert wird.

Die Bereitstellung eines neuen Schlüsselworts, das eine Art Magie ist und nichts anderes tut, als if err != nil { return err } zu ersetzen, scheint ein gefährlicher Weg zu sein.
Jetzt wird der gesamte Code einfach in einen Aufruf eingeschlossen, um es zu versuchen. Dies ist etwas in Ordnung (obwohl die Lesbarkeit scheiße ist) für Code, der sich nur mit Fehlern im Paket befasst, wie zum Beispiel:

func foo() error {
  /// stuff
  try(bar())
  // more stuff
}

Aber ich würde argumentieren, dass das gegebene Beispiel wirklich schrecklich ist und den Aufrufer im Grunde versucht, einen Fehler zu verstehen, der wirklich tief im Stapel ist, ähnlich wie bei der Behandlung von Ausnahmen.
Natürlich ist es Sache des Entwicklers, hier das Richtige zu tun, aber es gibt dem Entwickler eine großartige Möglichkeit, sich nicht um seine Fehler zu kümmern, vielleicht mit einem „Wir beheben das später“ (und wir alle wissen, wie das geht ).

Ich wünschte, wir würden das Thema aus einer anderen Perspektive betrachten als * „wie können wir Wiederholungen reduzieren“ und mehr über „wie können wir die (richtige) Fehlerbehandlung einfacher und Entwickler produktiver machen“.
Wir sollten darüber nachdenken, wie sich dies auf den laufenden Produktionscode auswirkt.

*Hinweis: Dies reduziert die Wiederholung nicht wirklich, sondern ändert nur, was wiederholt wird, während der Code weniger lesbar wird, da alles in try() eingeschlossen ist.

Ein letzter Punkt: Das Lesen des Vorschlags scheint zunächst nett zu sein, dann fängt man an, sich auf alle Fallstricke einzulassen (zumindest die aufgelisteten) und es ist nur wie "ok, ja, das ist zu viel".


Mir ist klar, dass vieles davon subjektiv ist, aber es ist etwas, das mir wichtig ist. Diese Semantik ist unglaublich wichtig.
Was ich sehen möchte, ist eine Möglichkeit, das Schreiben und Verwalten von Code auf Produktionsebene zu vereinfachen, sodass Sie Fehler auch für Code auf POC-/Demo-Ebene "richtig" machen können.

Da der Fehlerkontext ein wiederkehrendes Thema zu sein scheint ...

Hypothese: Die meisten Go-Funktionen geben (T, error) zurück, im Gegensatz zu (T1, T2, T3, error)

Was wäre, wenn wir try nicht als try(T1, T2, T3, error) (T1, T2, T3) definieren, sondern als
try(func (args) (T1, T2, T3, error))(T1, T2, T3) ? (Dies ist eine Annäherung)

Das heißt, dass die syntaktische Struktur eines try -Aufrufs immer ein erstes Argument ist, das ein Ausdruck ist, der mehrere Werte zurückgibt, von denen der letzte ein Fehler ist.

Dann öffnet dies ähnlich wie make die Tür zu einer 2-Argument-Form des Aufrufs, wobei das zweite Argument der Kontext des Versuchs ist (z. B. eine feste Zeichenfolge, eine Zeichenfolge mit einem %v , eine Funktion, die ein Fehlerargument akzeptiert und einen anderen Fehler zurückgibt usw.)

Dies ermöglicht immer noch das Verketten für den Fall (T, error) , aber Sie können nicht mehr mehrere Rücksendungen verketten, was meiner Meinung nach normalerweise nicht erforderlich ist.

@ cpuguy83 Wenn Sie den Vorschlag lesen, sehen Sie, dass nichts Sie daran hindert, den Fehler zu umschließen. Tatsächlich gibt es mehrere Möglichkeiten, dies zu tun, während Sie immer noch try verwenden. Viele Leute scheinen das aus irgendeinem Grund anzunehmen.

if err != nil { return err } ist genauso wie "wir werden das später beheben" wie try , außer dass es beim Prototyping lästiger ist.

Ich weiß nicht, wie Dinge, die sich in Klammern befinden, weniger lesbar sind als Funktionsschritte, die alle vier Zeilen der Textbausteine ​​sind.

Es wäre nett, wenn Sie auf einige dieser besonderen "Fallstricke" hinweisen würden, die Sie störten, da das das Thema ist.

Die Lesbarkeit scheint ein Problem zu sein, aber was ist mit go fmt, das try() präsentiert, damit es auffällt, so etwas wie:

f := try(
    os.Open("file.txt")
)

@MrTravisB

Das Problem, das ich damit habe, ist, dass davon ausgegangen wird, dass Sie den Fehler immer nur zurückgeben möchten, wenn er auftritt.

Ich bin nicht einverstanden. Es wird davon ausgegangen, dass Sie dies oft genug tun möchten, um eine Abkürzung dafür zu rechtfertigen. Wenn Sie dies nicht tun, steht es der einfachen Behandlung von Fehlern nicht im Wege.

Wenn Sie dem Fehler möglicherweise Kontext hinzufügen und ihn zurückgeben möchten, oder wenn Sie sich einfach anders verhalten möchten, wenn ein Fehler auftritt.

Der Vorschlag beschreibt ein Muster zum Hinzufügen von blockweitem Kontext zu Fehlern. @josharian wies darauf hin, dass die Beispiele jedoch einen Fehler enthalten und nicht klar ist, wie er am besten vermieden werden kann. Ich habe ein paar Beispiele geschrieben, wie man damit umgehen kann.

Für einen spezifischeren Fehlerkontext macht try wieder etwas, und wenn Sie das nicht wollen, verwenden Sie nicht try .

@boomlinde Genau mein Punkt. Dieser Vorschlag versucht, einen einzelnen Anwendungsfall zu lösen, anstatt ein Werkzeug bereitzustellen, um das größere Problem der Fehlerbehandlung zu lösen. Ich denke, die grundlegende Frage, ob genau das, was Sie darauf hingewiesen haben.

Es wird davon ausgegangen, dass Sie dies oft genug tun möchten, um eine Abkürzung dafür zu rechtfertigen.

Meiner Meinung nach und Erfahrung ist dieser Anwendungsfall eine kleine Minderheit und rechtfertigt keine Kurzschreibweise.

Außerdem hat der Ansatz, defer zur Behandlung von Fehlern zu verwenden, Probleme, da er davon ausgeht, dass Sie alle möglichen Fehler gleich behandeln möchten. defer Kontoauszüge können nicht storniert werden.

defer fmt.HandleErrorf(&err, “foobar”)

n := try(foo())

x : try(foo2())

Was ist, wenn ich eine andere Fehlerbehandlung für Fehler haben möchte, die möglicherweise von foo() im Vergleich zu foo2() zurückgegeben werden?

@MrTravisB

Was ist, wenn ich eine andere Fehlerbehandlung für Fehler haben möchte, die möglicherweise von foo() vs. foo2() zurückgegeben werden?

Dann benutzt du etwas anderes. Das ist der Punkt, den @boomlinde machen wollte.

Vielleicht sehen Sie diesen Anwendungsfall persönlich nicht oft, aber viele Leute tun dies, und das Hinzufügen try betrifft Sie nicht wirklich. Je seltener der Anwendungsfall für Sie ist, desto weniger betrifft es Sie, dass try hinzugefügt wird.

@qrpnxz

f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))

(Ja, ich verstehe, dass es ReadFile gibt und dass dieses spezielle Beispiel nicht der beste Weg ist, Daten irgendwohin zu kopieren, nicht der Punkt)

Dies erfordert mehr Leseaufwand, da Sie die Inline des Versuchs analysieren müssen. Die Anwendungslogik wird in einem weiteren Aufruf zusammengefasst.
Ich würde auch argumentieren, dass ein defer Fehlerbehandler hier nicht gut wäre, außer den Fehler einfach in eine neue Nachricht einzuschließen ... was nett ist, aber es gibt mehr beim Umgang mit Fehlern, als es einfach zu machen Mensch zu lesen, was passiert ist.

Zumindest in Rust ist der Operator ein Postfix ( ? an das Ende eines Aufrufs angehängt), was keine zusätzliche Belastung darstellt, um die eigentliche Logik herauszufinden.

Ausdrucksbasierte Flusskontrolle

panic kann eine weitere Flusssteuerungsfunktion sein, aber sie gibt keinen Wert zurück, was sie effektiv zu einer Anweisung macht. Vergleichen Sie dies mit try , was ein Ausdruck ist und überall vorkommen kann.

recover hat einen Wert und wirkt sich auf die Flusssteuerung aus, muss aber in einer defer -Anweisung vorkommen. Diese defer s sind typischerweise Funktionsliterale, recover wird immer nur einmal aufgerufen, daher tritt recover effektiv auch als Anweisung auf. Vergleichen Sie dies erneut mit try , das überall auftreten kann.

Ich denke, diese Punkte bedeuten, dass try es erheblich schwieriger macht, dem Kontrollfluss auf eine Weise zu folgen, die wir zuvor nicht hatten, wie bereits erwähnt wurde, aber ich habe den Unterschied zwischen Anweisungen und Ausdrücken nicht gesehen wies darauf hin.


Noch ein Vorschlag

Erlauben Sie Aussagen wie

if err != nil {
    return nil, 0, err
}

auf einer Zeile mit gofmt formatiert werden, wenn der Block nur eine return -Anweisung enthält und diese Anweisung keine Zeilenumbrüche enthält. Zum Beispiel:

if err != nil { return nil, 0, err }

Begründung

  • Es erfordert keine Sprachänderungen
  • Die Formatierungsregel ist einfach und klar
  • Die Regel kann so gestaltet werden, dass sie aktiviert wird, wobei gofmt Zeilenumbrüche behält, wenn sie bereits vorhanden sind (wie Strukturliterale). Opt-in ermöglicht es dem Autor auch, eine gewisse Fehlerbehandlung hervorzuheben
  • Wenn es nicht aktiviert ist, kann der Code mit einem Aufruf von gofmt automatisch auf den neuen Stil portiert werden
  • Es ist nur für return -Anweisungen, also wird es nicht unnötigerweise für Golf-Code missbraucht
  • Interagiert gut mit Kommentaren, die beschreiben, warum einige Fehler auftreten können und warum sie zurückgegeben werden. Die Verwendung vieler verschachtelter try -Ausdrücke handhabt dies schlecht
  • Es reduziert den vertikalen Raum der Fehlerbehandlung um 66 %
  • Kein ausdrucksbasierter Kontrollfluss
  • Code wird viel häufiger gelesen als geschrieben, daher sollte er für den Leser optimiert werden. Sich wiederholender Code, der weniger Platz einnimmt, ist für den Leser hilfreich, während try sich mehr in Richtung des Schreibers neigt
  • Die Leute haben bereits try vorgeschlagen, die auf mehreren Linien existieren. Zum Beispiel dieser Kommentar oder dieser Kommentar , der einen Stil einführt, wie
f, err := os.Open(file)
try(maybeWrap(err))
  • Der Stil „in eigener Zeile versuchen“ beseitigt jegliche Zweideutigkeit darüber, welcher err -Wert zurückgegeben wird. Daher vermute ich, dass dieses Formular häufig verwendet wird. Das Zulassen eines linierten if-Blocks ist fast dasselbe, außer dass es auch explizit gibt, was die Rückgabewerte sind
  • Es fördert nicht die Verwendung benannter Rücksendungen oder unklarer defer -basierter Verpackungen. Beides erhöht die Hürde für Verpackungsfehler, und ersteres kann godoc Änderungen erfordern
  • Es muss nicht diskutiert werden, wann try im Vergleich zur herkömmlichen Fehlerbehandlung verwendet werden sollte
  • Schließt nicht aus, in Zukunft try oder etwas anderes zu tun. Die Änderung kann auch dann positiv sein, wenn try akzeptiert wird
  • Keine negative Interaktion mit der testing -Bibliothek oder main -Funktionen. In der Tat, wenn der Vorschlag jede einzeilige Anweisung statt nur Rückgaben zulässt, kann dies die Verwendung von zusicherungsbasierten Bibliotheken reduzieren. Erwägen
value, err := something()
if err != nil { t.Fatal(err) }
  • Keine negative Wechselwirkung mit der Prüfung auf bestimmte Fehler. Erwägen
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

Zusammenfassend lässt sich sagen, dass dieser Vorschlag geringe Kosten verursacht, optional gestaltet werden kann, keine weiteren Änderungen ausschließt, da er nur stilistisch ist, und den Aufwand für das Lesen von ausführlichem Fehlerbehandlungscode verringert, während alles explizit bleibt. Ich denke, es sollte zumindest als erster Schritt betrachtet werden, bevor man mit try All-in geht.


Einige Beispiele portiert

Von https://github.com/golang/go/issues/32437#issuecomment -498941435

Mit Versuch

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Mit diesem

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations))
        if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }

        t := &Thing{thingy: thingy}
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }

        t.initOtherThing()
        return t, nil
}

Es ist wettbewerbsfähig in der Platznutzung und ermöglicht dennoch das Hinzufügen von Kontext zu Fehlern.

Von https://github.com/golang/go/issues/32437#issuecomment -499007288

Mit Versuch

func (c *Config) Build() error {
    pkgPath := try(c.load())
    b := bytes.NewBuffer(nil)
    try(emplates.ExecuteTemplate(b, "main", c))
    buf := try(format.Source(b.Bytes()))
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644))
    // ...
}

Mit diesem

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

Der ursprüngliche Kommentar verwendete ein hypothetisches tryf , um die Formatierung anzuhängen, die entfernt wurde. Es ist unklar, wie man alle unterschiedlichen Kontexte am besten hinzufügt, und vielleicht wäre try nicht einmal anwendbar.

@cpuguy83
Für mich ist es besser lesbar mit try . In diesem Beispiel lese ich "Datei öffnen, alle Bytes lesen, Daten senden". Bei normaler Fehlerbehandlung würde ich lesen: "Datei öffnen, prüfen, ob ein Fehler aufgetreten ist, die Fehlerbehandlung tut dies, dann alle Bytes lesen, jetzt prüfen, ob etwas passiert ist ..." Ich weiß, dass Sie das err != nil können try einfach einfacher, denn wenn ich es sehe, kenne ich das Verhalten sofort: gibt zurück, wenn err != nil. Wenn Sie einen Zweig haben, muss ich sehen, was er tut. Es könnte alles tun.

Ich würde auch argumentieren, dass ein Defer-Error-Handler hier nicht gut wäre, außer um den Fehler einfach mit einer neuen Nachricht zu umschließen

Ich bin sicher, es gibt andere Dinge, die Sie in der Zurückstellung tun können, aber egal, try ist sowieso für den einfachen allgemeinen Fall. Immer wenn Sie etwas mehr tun möchten, gibt es immer die gute alte Go-Fehlerbehandlung. Das geht nicht weg.

@zeebo Ja , das gefällt mir.
Der Artikel von @kungfusheep verwendete eine solche einzeilige Fehlerprüfung, und ich war aufgeregt, sie auszuprobieren. Dann, sobald ich speichere, hat gofmt es in drei Zeilen erweitert, was traurig war. Viele Funktionen in der stdlib sind so in einer Zeile definiert, daher hat es mich überrascht, dass gofmt das erweitern würde.

@qrpnxz

Ich lese zufällig viel Go-Code. Eines der besten Dinge an der Sprache ist die Leichtigkeit, die sich daraus ergibt, dass der meiste Code einem bestimmten Stil folgt (danke, gofmt).
Ich möchte keinen Haufen Code lesen, der in try(f()) verpackt ist.
Das bedeutet, dass es entweder zu Abweichungen im Code-Stil/zur Code-Praxis oder zu Linters wie „oh, du hättest hier try() verwenden sollen“ kommen wird (was ich wiederum nicht einmal mag, was der Punkt ist, an dem ich und andere kommentieren zu diesem Vorschlag).

Es ist objektiv nicht besser als if err != nil { return err } , nur weniger zu tippen.


Eine letzte Sache:

Wenn Sie den Vorschlag lesen, werden Sie sehen, dass nichts Sie daran hindert

Können wir diese Sprache bitte unterlassen? Natürlich habe ich den Vorschlag gelesen. Es ist einfach so, dass ich es gestern Abend gelesen und dann heute Morgen kommentiert habe, nachdem ich darüber nachgedacht habe, und nicht die Einzelheiten meiner Absicht erklärt habe.
Das ist ein unglaublich feindseliger Ton.

@cpuguy83
Mein böser CPU-Typ. So habe ich das nicht gemeint.

Und ich denke, Sie müssen darauf hinweisen, dass Code, der try verwendet, ziemlich anders aussieht als Code, der dies nicht tut, also kann ich mir vorstellen, dass dies die Erfahrung beim Parsen dieses Codes beeinflussen würde, aber ich kann dem nicht ganz zustimmen bedeutet in diesem Fall schlimmer, obwohl ich verstehe, dass Sie es persönlich nicht mögen, genauso wie ich es persönlich mag. Viele Dinge in Go sind so. Was Linters Ihnen sagen, ist eine ganz andere Sache, denke ich.

Klar ist es objektiv nicht besser. Ich drückte aus, dass es für mich auf diese Weise besser lesbar war. Ich habe das vorsichtig formuliert.

Nochmals Entschuldigung, dass ich mich so anhöre. Obwohl dies ein Argument ist, wollte ich Sie nicht verärgern.

https://github.com/golang/go/issues/32437#issuecomment -498908380

Niemand wird Sie dazu zwingen, es zu versuchen.

Ich ignoriere die Geschmeidigkeit, ich denke, das ist eine ziemlich handgewellte Art, eine Designkritik abzutun.

Sicher, ich muss es nicht benutzen. Aber jeder, mit dem ich Code schreibe, könnte ihn verwenden und mich dazu zwingen, zu versuchen, try(try(try(to()).parse().this)).easily()) zu entschlüsseln. Es ist wie gesagt

Niemand wird Sie dazu zwingen, die leere Schnittstelle zu verwenden{}.

Wie auch immer, Go ist ziemlich streng in Bezug auf Einfachheit: gofmt lässt den gesamten Code gleich aussehen. Der glückliche Pfad bleibt links und alles, was teuer oder überraschend sein könnte, ist explizit . try wie vorgeschlagen ist eine 180-Grad-Wendung davon. Einfachheit != prägnant.

Zumindest try sollte ein Schlüsselwort mit lvalues ​​sein.

Es ist _objektiv_ nicht besser als if err != nil { return err } , nur weniger zu tippen.

Es gibt einen objektiven Unterschied zwischen den beiden: try(Foo()) ist ein Ausdruck. Für einige ist dieser Unterschied ein Nachteil (die try(strconv.Atoi(x))+try(strconv.Atoi(y)) -Kritik). Für andere ist dieser Unterschied aus dem gleichen Grund ein Vorteil. Immer noch nicht objektiv besser oder schlechter - aber ich denke auch nicht, dass der Unterschied unter den Teppich gekehrt werden sollte und die Behauptung, es sei "nur weniger zu tippen", wird dem Vorschlag nicht gerecht.

@elagergren-spideroak schwer zu sagen, dass try ärgerlich ist, in einem Atemzug zu sehen und dann zu sagen, dass es im nächsten nicht explizit ist. Du musst dir einen aussuchen.

Es ist üblich, dass Funktionsargumente zuerst in temporäre Variablen geschrieben werden. Ich bin mir sicher, dass es häufiger zu sehen wäre

this := try(to()).parse().this
that := try(this.easily())

als dein Beispiel.

try Nichtstun ist der glückliche Weg, das sieht also wie erwartet aus. Auf dem unglücklichen Weg kehrt es nur zurück. Zu sehen, dass es try gibt, reicht aus, um diese Informationen zu sammeln. Es ist auch nicht teuer, von einer Funktion zurückzukehren, also glaube ich nicht, dass try nach dieser Beschreibung eine 180 macht

@josharian In Bezug auf Ihren Kommentar in https://github.com/golang/go/issues/32437#issuecomment -498941854 glaube ich nicht, dass hier ein früher Bewertungsfehler vorliegt.

defer fmt.HandleErrorf(&err, „foobar: %v“, err)

Der unveränderte Wert von err wird an HandleErrorf übergeben, und ein Zeiger auf err wird übergeben. Wir prüfen, ob err nil ist (mit dem Zeiger). Wenn nicht, formatieren wir die Zeichenfolge mit dem unveränderten Wert von err . Dann setzen wir err mit dem Zeiger auf den formatierten Fehlerwert.

@Merovius Der Vorschlag ist jedoch wirklich nur ein Syntax-Zucker-Makro, also wird es am Ende darum gehen, was die Leute denken, dass es schöner aussieht oder am wenigsten Probleme verursacht. Wenn Sie das nicht glauben, erklären Sie es mir bitte. Deshalb bin ich persönlich dafür. Es ist eine nette Ergänzung, ohne irgendwelche Schlüsselwörter aus meiner Sicht hinzuzufügen.

@ianlancetaylor , ich denke, @josharian hat Recht: Der „unmodifizierte“ Wert von err ist der Wert zu dem Zeitpunkt, zu dem defer auf den Stack geschoben wird, nicht der (vermutlich beabsichtigte) Wert von err gesetzt durch try vor der Rückkehr.

Das andere Problem, das ich mit try habe, ist, dass es für die Leute so viel einfacher ist, mehr und mehr Logik in eine einzige Zeile zu packen. Das ist mein größtes Problem mit den meisten anderen Sprachen, dass sie es wirklich einfach machen, etwa 5 Ausdrücke in eine einzige Zeile zu schreiben, und das möchte ich nicht einfach so lassen.

this := try(to()).parse().this
that := try(this.easily())

^^ Auch das ist geradezu schrecklich. In der ersten Zeile muss ich hin und her springen, um in meinem Kopf Paren-Matching zu machen. Schon die zweite Zeile, die eigentlich ganz einfach ist... ist wirklich schwer zu lesen.
Verschachtelte Funktionen sind schwer zu lesen.

parser, err := to()
if err != nil {
    return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
    return err
}

^^ Das ist meiner Meinung nach so viel einfacher und besser. Es ist super einfach und klar. Ja, es sind viel mehr Codezeilen, das ist mir egal. Es ist sehr offensichtlich.

@bcmills @josharian Ah, natürlich, danke. So müsste es sein

defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()

Nicht sehr nett. Vielleicht sollte fmt.HandleErrorf doch implizit den Fehlerwert als letztes Argument übergeben.

Diese Ausgabe hat sehr schnell viele Kommentare erhalten, und viele von ihnen scheinen mir Kommentare zu wiederholen, die bereits gemacht wurden. Natürlich können Sie gerne einen Kommentar abgeben, aber ich möchte freundlich vorschlagen, dass Sie, wenn Sie einen bereits gemachten Punkt wiederholen möchten, dies tun, indem Sie die Emojis von GitHub verwenden, anstatt den Punkt zu wiederholen. Danke.

@ianlancetaylor Wenn fmt.HandleErrorf err als erstes Argument nach dem Format sendet, wird die Implementierung schöner und der Benutzer kann immer mit %[1]v darauf verweisen.

@natefinch Absolut einverstanden.

Ich frage mich, ob ein Ansatz im Roststil schmackhafter wäre?
Beachten Sie, dass dies kein Vorschlag ist, nur darüber nachzudenken ...

this := to()?.parse().this
that := this.easily()?

Am Ende denke ich, dass dies schöner ist, aber (könnte auch ein ! oder etwas anderes verwenden ...), aber das Problem der Fehlerbehandlung wird immer noch nicht gut behoben.


Rost hat natürlich auch try() , ziemlich genau so, aber... der andere Roststil.

Es ist _objektiv_ nicht besser als if err != nil { return err } , nur weniger zu tippen.

Es gibt einen objektiven Unterschied zwischen den beiden: try(Foo()) ist ein Ausdruck. Für einige ist dieser Unterschied ein Nachteil (die try(strconv.Atoi(x))+try(strconv.Atoi(y)) -Kritik). Für andere ist dieser Unterschied aus dem gleichen Grund ein Vorteil. Immer noch nicht objektiv besser oder schlechter - aber ich denke auch nicht, dass der Unterschied unter den Teppich gekehrt werden sollte und die Behauptung, es sei "nur weniger zu tippen", wird dem Vorschlag nicht gerecht.

Das ist einer der Hauptgründe, warum ich diese Syntax mag; Dadurch kann ich eine Fehler zurückgebende Funktion als Teil eines größeren Ausdrucks verwenden, ohne alle Zwischenergebnisse benennen zu müssen. In manchen Situationen ist es einfach, sie zu benennen, aber in anderen gibt es keinen besonders aussagekräftigen oder nicht überflüssigen Namen, den ich ihnen geben könnte. In diesem Fall würde ich ihnen lieber überhaupt keinen Namen geben.

@MrTravisB

Genau mein Standpunkt. Dieser Vorschlag versucht, einen einzelnen Anwendungsfall zu lösen, anstatt ein Werkzeug bereitzustellen, um das größere Problem der Fehlerbehandlung zu lösen. Ich denke, die grundlegende Frage, ob genau das, was Sie darauf hingewiesen haben.

Was genau habe ich gesagt, das ist genau Ihr Punkt? Mir scheint eher, dass Sie meinen Punkt grundlegend missverstanden haben, wenn Sie glauben, dass wir uns einig sind.

Meiner Meinung nach und Erfahrung ist dieser Anwendungsfall eine kleine Minderheit und rechtfertigt keine Kurzschreibweise.

In der Go-Quelle gibt es Tausende von Fällen, die von try standardmäßig behandelt werden könnten, selbst wenn es keine Möglichkeit gäbe, Fehlern Kontext hinzuzufügen. Wenn geringfügig, ist es immer noch ein häufiger Grund für Beschwerden.

Außerdem hat der Ansatz, Fehler mit defer zu behandeln, Probleme, da davon ausgegangen wird, dass Sie alle möglichen Fehler gleich behandeln möchten. defer-Anweisungen können nicht storniert werden.

In ähnlicher Weise geht der Ansatz, + zur Behandlung von Arithmetik zu verwenden, davon aus, dass Sie nicht subtrahieren möchten, also tun Sie es nicht, wenn Sie es nicht tun. Die interessante Frage ist, ob der blockweite Fehlerkontext zumindest ein gemeinsames Muster darstellt.

Was ist, wenn ich eine andere Fehlerbehandlung für Fehler haben möchte, die möglicherweise von foo() vs. foo2() zurückgegeben werden?

Auch hier verwenden Sie nicht try . Dann gewinnen Sie nichts von try , aber Sie verlieren auch nichts.

@cpuguy83

Ich frage mich, ob ein Ansatz im Roststil schmackhafter wäre?

Dagegen spricht der Vorschlag.

An diesem Punkt denke ich, dass es besser lesbar ist, try{}catch{} zu haben :upside_down_face:

  1. Benannte Importe zu verwenden, um defer -Eckfälle zu umgehen, ist nicht nur schrecklich für Dinge wie godoc, sondern vor allem sehr fehleranfällig. Es ist mir egal, ich kann das Ganze mit weiteren func() umwickeln, um das Problem zu umgehen, es sind nur mehr Dinge, die ich beachten muss, ich denke, es fördert eine "schlechte Praxis".
  2. Niemand wird Sie dazu zwingen, es zu versuchen.

    Das bedeutet nicht, dass es eine gute Lösung ist, ich weise darauf hin, dass die aktuelle Idee einen Fehler im Design hat, und ich bitte darum, dass dies auf eine weniger fehleranfällige Weise angegangen wird.

  3. Ich denke, Beispiele wie try(try(try(to()).parse().this)).easily()) sind unrealistisch, dies könnte bereits mit anderen Funktionen geschehen, und ich denke, es wäre fair, wenn diejenigen, die den Code überprüfen, eine Aufteilung verlangen würden.
  4. Was ist, wenn ich 3 Stellen habe, an denen ein Fehler auftreten kann, und ich jede Stelle separat umschließen möchte? try() macht dies sehr schwierig, tatsächlich entmutigt try() angesichts der Schwierigkeit bereits Verpackungsfehler, aber hier ist ein Beispiel dafür, was ich meine:

    func before() error {
      x, err := foo()
      if err != nil {
        wrap(err, "error on foo")
      }
      y, err := bar(x)
      if err != nil {
        wrapf(err, "error on bar with x=%v", x)
      }
      fmt.Println(y)
      return nil
    }
    
    func after() (err error) {
      defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
      x := try(foo())
      y := try(bar(x))
      fmt.Println(y)
      return nil
    }
    
  5. Auch hier verwenden Sie nicht try . Dann gewinnen Sie nichts von try , aber Sie verlieren auch nichts.

    Nehmen wir an, es ist eine gute Praxis, Fehler mit nützlichem Kontext zu umschließen, try() würde als schlechte Praxis betrachtet werden, weil es keinen Kontext hinzufügt. Dies bedeutet, dass try() eine Funktion ist, die niemand verwenden möchte, und zu einer Funktion wird, die so selten verwendet wird, dass sie möglicherweise genauso gut nicht existiert hat.

    Anstatt nur zu sagen "Nun, wenn es dir nicht gefällt, benutze es nicht und halt die Klappe" (so liest es sich), wäre es meiner Meinung nach besser zu versuchen, das anzusprechen, was viele Benutzer als Fehler betrachten in dem Design. Können wir stattdessen diskutieren, was am vorgeschlagenen Design geändert werden könnte, damit unser Anliegen besser gehandhabt werden kann?

@boomlinde Der Punkt, dem wir zustimmen, ist, dass dieser Vorschlag versucht, einen geringfügigen Anwendungsfall zu lösen, und die Tatsache, dass "wenn Sie es nicht brauchen, verwenden Sie es nicht" das Hauptargument dafür ist, fördert diesen Punkt. Wie @elagergren-spideroak feststellte, funktioniert dieses Argument nicht, denn selbst wenn ich es nicht verwenden möchte, werden andere es tun, was mich dazu zwingt, es zu verwenden. Nach der Logik Ihres Arguments sollte Go auch eine ternäre Aussage haben. Und wenn Sie ternäre Aussagen nicht mögen, verwenden Sie sie nicht.

Haftungsausschluss - Ich denke, Go sollte eine ternäre Aussage haben, aber da Gos Ansatz für Sprachfunktionen darin besteht, keine Funktionen einzuführen, die das Lesen von Code erschweren könnten , sollte dies nicht der Fall sein.

Mir fällt noch etwas ein: Ich sehe viel Kritik aufgrund der Idee, dass try Entwickler dazu ermutigen könnte, sorglos mit Fehlern umzugehen. Aber meiner Meinung nach trifft dies eher auf die aktuelle Sprache zu; Die Textbausteine ​​zur Fehlerbehandlung sind so ärgerlich, dass sie dazu anregen, einige Fehler zu schlucken oder zu ignorieren, um sie zu vermeiden. Zum Beispiel habe ich ein paar Mal so etwas geschrieben:

func exists(filename string) bool {
  _, err := os.Stat(filename)
  return err == nil
}

um if exists(...) { ... } schreiben zu können, obwohl dieser Code einige mögliche Fehler stillschweigend ignoriert. Wenn ich try hätte, würde ich mich wahrscheinlich nicht darum kümmern und einfach (bool, error) zurückgeben.

Da ich hier chaotisch bin, werfe ich die Idee auf, eine zweite eingebaute Funktion namens catch hinzuzufügen, die eine Funktion empfängt, die einen Fehler akzeptiert und einen überschriebenen Fehler zurückgibt, wenn dann ein nachfolgendes catch aufgerufen wird, würde es den Handler überschreiben. zum Beispiel:

func catch(handler func(err error) error) {
  // .. impl ..
}

Nun wird diese eingebaute Funktion auch eine Makro-ähnliche Funktion sein, die den nächsten Fehler behandeln würde, der von try wie folgt zurückgegeben wird:

func wrapf(format string, ...values interface{}) func(err error) error {
  // user defined
  return func(err error) error {
    return fmt.Errorf(format + ": %v", ...append(values, err))
  }
}
func sample() {
  catch(wrapf("something failed in foo"))
  try(foo()) // "something failed in foo: <error>"
  x := try(foo2()) // "something failed in foo: <error>"
  // Subsequent calls for catch overwrite the handler
  catch(wrapf("something failed in bar with x=%v", x))
  try(bar(x)) // "something failed in bar with x=-1: <error>"
}

Das ist nett, weil ich Fehler ohne defer umbrechen kann, was fehleranfällig sein kann, es sei denn, wir verwenden benannte Rückgabewerte oder umbrechen mit einer anderen Funktion, es ist auch nett, weil defer den gleichen Fehlerhandler für hinzufügen würde alle Fehler, auch wenn ich 2 davon anders behandeln möchte. Sie können es auch verwenden, wie Sie es für richtig halten, zum Beispiel:

func foo(a, b string) (int64, error) {
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
  })
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, err: %v", a, err)
  })
  x := try(strconv.Atoi(a))
  catch(func (err error) error {
    return fmt.Errorf("can't parse b: %s, err: %v", b, err)
  })
  y := try(strconv.Atoi(b))
  return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
  catch(wrapf("can't parse a: %s", a))
  x := try(strconv.Atoi(a))
  catch(wrapf("can't parse b: %s", b))
  y := try(strconv.Atoi(b))
  return x + y
}
func before(a, b string) (int64, error) {
  x, err := strconv.Atoi(a)
  if err != nil {
    return 0,  fmt.Errorf("can't parse a: %s, err: %v", a, err)
  }
  y, err := strconv.Atoi(b)
  if err != nil {
    return 0,  fmt.Errorf("can't parse b: %s, err: %v", b, err)
  }
  return x + y
}

Und immer noch in der chaotischen Stimmung (um Ihnen zu helfen, sich einzufühlen) Wenn Sie catch nicht mögen, müssen Sie es nicht verwenden.

Nun ... Ich meine den letzten Satz nicht wirklich, aber es fühlt sich an, als wäre er für die Diskussion nicht hilfreich, sehr aggressiv IMO.
Trotzdem, wenn wir diesen Weg gehen, denke ich, dass wir stattdessen genauso gut try{}catch(error err){} haben könnten :stuck_out_tongue:

Siehe auch #27519 - das Fehlermodell #id/catch

Niemand wird Sie dazu zwingen, es zu versuchen.

Ich ignoriere die Geschmeidigkeit, ich denke, das ist eine ziemlich handgewellte Art, eine Designkritik abzutun.

Entschuldigung, Glib war nicht meine Absicht.

Was ich versuche zu sagen, ist, dass try keine 100%ige Lösung sein soll. Es gibt verschiedene Fehlerbehandlungsparadigmen, die von try nicht gut gehandhabt werden. Zum Beispiel, wenn Sie dem Fehler callsite-abhängigen Kontext hinzufügen müssen. Sie können immer auf if err != nil { zurückgreifen, um diese komplizierteren Fälle zu handhaben.

Es ist sicherlich ein gültiges Argument, dass try für verschiedene Instanzen von X nicht mit X umgehen kann. Aber oft bedeutet die Behandlung von Fall X, dass der Mechanismus komplizierter wird. Hier gibt es einen Kompromiss, der einerseits mit X umgeht, aber den Mechanismus für alles andere kompliziert. Was wir alles tun, hängt davon ab, wie verbreitet X ist und wie viel Komplikation es erfordern würde, mit X umzugehen.

Mit "Niemand wird Sie dazu bringen, es zu versuchen" meine ich, dass ich denke, dass das fragliche Beispiel zu den 10% gehört, nicht zu den 90%. Diese Behauptung steht sicherlich zur Debatte, und ich freue mich über Gegenargumente. Aber irgendwann müssen wir irgendwo eine Grenze ziehen und sagen: "Ja, try wird diesen Fall nicht behandeln. Sie müssen die Fehlerbehandlung im alten Stil verwenden. Entschuldigung.".

Es ist nicht so, dass "try diesen speziellen Fall der Fehlerbehandlung nicht behandeln kann", das das Problem ist, sondern "try ermutigt Sie, Ihre Fehler nicht zu umschließen". Die check-handle -Idee zwang Sie dazu, eine return-Anweisung zu schreiben, daher war das Schreiben eines Fehlerumbruchs ziemlich trivial.

Bei diesem Vorschlag müssen Sie eine benannte Rückgabe mit einem defer verwenden, was nicht intuitiv ist und sehr hacky erscheint.

Die check-handle -Idee zwang Sie dazu, eine return-Anweisung zu schreiben, daher war das Schreiben eines Fehlerumbruchs ziemlich trivial.

Das stimmt nicht – im Designentwurf hat jede Funktion, die einen Fehler zurückgibt, einen Standard-Handler , der nur den Fehler zurückgibt.

Aufbauend auf dem schelmischen Punkt von @Goodwine brauchen Sie nicht wirklich separate Funktionen wie HandleErrorf , wenn Sie eine einzelne Brückenfunktion wie haben

func handler(err *error, handle func(error) error) {
  // nil handle is treated as the identity
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

die Sie gerne verwenden würden

defer handler(&err, func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Sie könnten handler selbst zu einem halbmagischen eingebauten Element wie try machen.

Wenn es magisch ist, könnte es sein erstes Argument implizit nehmen – wodurch es sogar in Funktionen verwendet werden kann, die ihre error -Rückgabe nicht benennen, wodurch einer der weniger glücklichen Aspekte des aktuellen Vorschlags ausgeschaltet und weniger gut gemacht wird pingelig und fehleranfällig, um Fehler zu schmücken. Das reduziert das vorherige Beispiel natürlich nicht wesentlich:

defer handler(func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Wenn es auf diese Weise magisch wäre, müsste es ein Kompilierzeitfehler sein, wenn es irgendwo verwendet würde, außer als Argument für defer . Sie könnten noch einen Schritt weiter gehen und es implizit aufschieben, aber defer handler liest sich ganz gut.

Da es defer verwendet, könnte es seine Funktion handle immer dann aufrufen, wenn ein Nicht-Null-Fehler zurückgegeben wird, was es auch ohne try nützlich macht, da Sie ein hinzufügen könnten

defer handler(wrapErrWithPackageName)

ganz oben bis fmt.Errorf("mypkg: %w", err) alles.

Das gibt Ihnen viele der älteren check / handle Vorschläge, aber es funktioniert natürlich (und explizit) mit defer, während es in den meisten Fällen die Notwendigkeit beseitigt, err explizit zu benennen try ist es ein relativ unkompliziertes Makro, das (glaube ich) vollständig im Frontend implementiert werden könnte.

Das stimmt nicht - im Designentwurf hat jede Funktion, die einen Fehler zurückgibt, einen Standardhandler, der nur den Fehler zurückgibt.

Meine Güte, du hast Recht.

Ich meine, dass ich denke, dass das fragliche Beispiel zu den 10% gehört, nicht zu den 90%. Diese Behauptung steht sicherlich zur Debatte, und ich freue mich über Gegenargumente. Aber irgendwann müssen wir irgendwo eine Grenze ziehen und sagen: "Ja, try wird diesen Fall nicht behandeln. Sie müssen die Fehlerbehandlung im alten Stil verwenden. Entschuldigung.".

Einverstanden, meine Meinung ist, dass diese Linie bei der Prüfung auf EOF oder ähnliches gezogen werden sollte, nicht beim Wickeln. Aber vielleicht wäre dies kein Problem mehr, wenn Fehler mehr Kontext hätten.

Könnte try() Fehler automatisch mit nützlichem Kontext zum Debuggen umbrechen? Wenn beispielsweise xerrors zu errors wird, sollten Fehler etwas haben, das wie ein Stack-Trace aussieht, den try() hinzufügen könnte, oder? Wenn ja, würde das vielleicht reichen 🤔

Wenn die Ziele sind (Lesen von https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md):

  • eliminieren Sie die Kesselplatte
  • minimale Sprachänderungen
  • Abdeckung der „häufigsten Szenarien“
  • fügt der Sprache nur sehr wenig Komplexität hinzu

Ich würde den Vorschlag nehmen, ihm einen Winkel zu geben und eine Codemigration in "kleinen Schritten" für all die Milliarden Codezeilen da draußen zuzulassen.

statt wie vorgeschlagen:

func printSum(a, b string) error {
        defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err) 
        x := try(strconv.Atoi(a))
        y := try(strconv.Atoi(b))
        fmt.Println("result:", x + y)
        return nil
}

Wir können:

func printSum(a, b string) error {
        var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)} 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

Was würden wir gewinnen?
twoStringsErr kann in printSum oder in einen allgemeinen Handler eingebunden werden, der Fehler erfassen kann (in diesem Fall mit 2 Zeichenfolgenparametern). Zeit
Auf die gleiche Weise kann ich den ErrHandler-Typ wie folgt erweitern:

type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{

}

oder

type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{

}

oder

type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{

}

und verwenden Sie dies rund um meinen Code:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

Die eigentliche Notwendigkeit wäre also, einen Trigger zu entwickeln, wenn err.Error auf nicht null gesetzt ist
Mit dieser Methode können wir auch:

func (s str2IntErrHandler) Handle() bool{
   **return false**
}

Was der aufrufenden Funktion mitteilen würde, fortzufahren, anstatt zurückzukehren

Und verwenden Sie verschiedene Fehlerbehandler in derselben Funktion:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        var oErr overflowError 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        totalAsByte,oErr := sumBytes(x,y)
        sunAsByte,oErr := subtractBytes(x,y)
        return nil
}

usw.

Gehen Sie die Ziele noch einmal durch

  • Boilerplate eliminieren - fertig
  • minimale Sprachänderungen - fertig
  • Abdeckung "häufigster Szenarien" - mehr als die vorgeschlagene IMO
  • fügt der Sprache sehr wenig Komplexität hinzu - sone
    Plus - einfachere Codemigration von
x, err := strconv.Atoi(a)

zu

x, err.Error := strconv.Atoi(a)

und tatsächlich - bessere Lesbarkeit (IMO, wieder)

@guybrand du bist der neueste Anhänger dieses wiederkehrenden Themas (was mir gefällt).

Siehe https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

@guybrand Das scheint ein ganz anderer Vorschlag zu sein; Ich denke, Sie sollten es als eigene Ausgabe einreichen, damit sich diese auf die Diskussion des Vorschlags von @griesemer konzentrieren kann.

@natefinch stimme zu. Ich denke, dies ist eher darauf ausgerichtet, die Erfahrung beim Schreiben von Go zu verbessern, anstatt es für das Lesen zu optimieren. Ich frage mich, ob IDE-Makros oder -Snippets das Problem lösen könnten, ohne dass dies zu einem Merkmal der Sprache wird.

@Guter Wein

Nehmen wir an, es ist eine gute Praxis, Fehler mit nützlichem Kontext zu umschließen, try() würde als schlechte Praxis betrachtet werden, weil es keinen Kontext hinzufügt. Dies bedeutet, dass try() eine Funktion ist, die niemand verwenden möchte, und zu einer Funktion wird, die so selten verwendet wird, dass sie möglicherweise genauso gut nicht existiert hat.

Wie im Vorschlag erwähnt (und anhand eines Beispiels gezeigt), hindert try Sie nicht grundsätzlich daran, Kontext hinzuzufügen. Ich würde sagen, dass die Art und Weise, wie vorgeschlagen wird, Fehlern Kontext hinzuzufügen, völlig orthogonal dazu ist. Dies wird speziell in den FAQ des Vorschlags angesprochen.

Ich erkenne an, dass try nicht nützlich ist, wenn innerhalb einer einzelnen Funktion eine Vielzahl verschiedener Kontexte vorhanden sind, die Sie zu verschiedenen Fehlern aus Funktionsaufrufen hinzufügen möchten. Allerdings glaube ich auch, dass etwas in der Art von HandleErrorf einen großen Anwendungsbereich abdeckt, weil es nicht ungewöhnlich ist, nur funktionsweiten Kontext zu Fehlern hinzuzufügen.

Anstatt nur zu sagen "Nun, wenn es dir nicht gefällt, benutze es nicht und halt die Klappe" (so liest es sich), wäre es meiner Meinung nach besser zu versuchen, das anzusprechen, was viele Benutzer als Fehler betrachten in dem Design.

Wenn es sich so liest entschuldige ich mich. Mein Punkt ist nicht, dass Sie so tun sollten, als ob es nicht existiert, wenn Sie es nicht mögen. Es ist offensichtlich, dass es Fälle gibt, in denen try nutzlos wären und dass Sie es in solchen Fällen nicht verwenden sollten, was meiner Meinung nach für diesen Vorschlag eine gute Balance zwischen KISS und allgemeinem Nutzen bietet. Ich dachte nicht, dass ich in diesem Punkt unklar war.

Danke an alle für das produktive Feedback bisher; das ist sehr informativ.
Hier mein Versuch einer ersten Zusammenfassung, um ein besseres Gefühl für das Feedback zu bekommen. Entschuldigung im Voraus für alle, die ich verpasst oder falsch dargestellt habe; Ich hoffe, dass ich den Gesamteindruck richtig verstanden habe.

0) Auf der positiven Seite haben @rasky , @adg , @eandre , @dpinela und andere ihre Freude über die Code-Vereinfachung ausgedrückt, die try bietet.

1) Die wichtigste Sorge scheint zu sein, dass try keinen guten Fehlerbehandlungsstil fördert, sondern stattdessen den "schnellen Ausstieg" fördert. ( @agnivade , @peterbourgon , @politician , @a8m , @eandre , @prologic , @kungfusheep , @cpuguy und andere haben ihre Besorgnis darüber geäußert.)

2) Viele Leute mögen die Idee eines eingebauten oder der damit verbundenen Funktionssyntax nicht, weil sie ein return verbirgt. Es wäre besser, ein Schlüsselwort zu verwenden. ( @sheerun , @Redundancy , @dolmen , @komuw , @RobertGrantEllis , @elagergren-spideroak). try kann auch leicht übersehen werden (@peterbourgon), besonders weil es in Ausdrücken vorkommen kann, die willkürlich verschachtelt sind. @natefinch ist besorgt, dass try es „zu einfach macht, zu viel in eine Zeile zu packen“, etwas, das wir normalerweise in Go zu vermeiden versuchen. Außerdem ist die IDE-Unterstützung zum Hervorheben try möglicherweise nicht ausreichend (@dominikh); try muss "für sich stehen".

3) Für einige ist der Status Quo expliziter if -Aussagen kein Problem, sie sind damit zufrieden ( @bitfield , @marwan-at-work, @natefinch). Es ist besser, nur einen Weg zu haben, Dinge zu erledigen (@gbbr); und explizite if -Anweisungen sind besser als implizite return -Anweisungen ( @DavexPro , @hmage , @prologic , @natefinch).
In ähnlicher Weise ist @mattn besorgt über die "implizite Bindung" des Fehlerergebnisses an try - die Verbindung ist im Code nicht explizit sichtbar.

4) Die Verwendung try erschwert das Debuggen von Code; Beispielsweise kann es erforderlich sein, einen try -Ausdruck wieder in eine if -Anweisung umzuschreiben, damit Debugging-Anweisungen eingefügt werden können ( @deanveloper , @typeless , @networkimprov , andere).

5) Es gibt einige Bedenken hinsichtlich der Verwendung benannter Rückgaben ( @buchanae , @adg).

Mehrere Personen haben Vorschläge zur Verbesserung oder Änderung des Vorschlags gemacht:

6) Einige haben die Idee einer optionalen Fehlerbehandlungsroutine (@beoran) oder einer Formatzeichenfolge aufgegriffen, die für try ( @unexge , @a8m , @eandre , @gotwarlost) bereitgestellt wird, um eine gute Fehlerbehandlung zu fördern.

7) @pierrec schlug vor, dass gofmt try -Ausdrücke passend formatieren könnte, um sie besser sichtbar zu machen.
Alternativ könnte man vorhandenen Code kompakter machen, indem man gofmt erlaubt, $ if -Anweisungen zu formatieren, die in einer Zeile auf Fehler prüfen (@zeebo).

8) @marwan-at-work argumentiert, dass try die Fehlerbehandlung einfach von if -Anweisungen auf try -Ausdrücke verschiebt. Wenn wir das Problem tatsächlich lösen wollen, sollte Go stattdessen die Fehlerbehandlung "besitzen", indem es sie wirklich implizit macht. Das Ziel sollte sein, die (richtige) Fehlerbehandlung einfacher und Entwickler produktiver zu machen (@cpuguy).

9) Schließlich mögen manche Leute den Namen try ( @beoran , @HiImJC , @dolmen) nicht oder bevorzugen ein Symbol wie ? ( @twisted1919 , @leaxoy , andere) .

Einige Kommentare zu diesem Feedback (entsprechend nummeriert):

0) Danke für das positive Feedback! :-)

1) Es wäre gut, mehr über dieses Anliegen zu erfahren. Der aktuelle Codierungsstil, der if -Anweisungen verwendet, um auf Fehler zu testen, ist so explizit wie möglich. Es ist sehr einfach, auf individueller Basis zusätzliche Informationen zu einem Fehler hinzuzufügen (für jeweils if ). Oft ist es sinnvoll, alle in einer Funktion erkannten Fehler einheitlich zu behandeln, was mit einem defer möglich ist - das ist bereits jetzt möglich. Die Tatsache, dass wir bereits alle Werkzeuge für eine gute Fehlerbehandlung in der Sprache haben, und das Problem, dass ein Handler-Konstrukt nicht orthogonal zu defer ist, hat uns dazu veranlasst, einen neuen Mechanismus wegzulassen, der ausschließlich Fehler vergrößert .

2) Es besteht natürlich die Möglichkeit, anstelle einer eingebauten ein Schlüsselwort oder eine spezielle Syntax zu verwenden. Ein neues Schlüsselwort ist nicht abwärtskompatibel. Ein neuer Betreiber könnte, scheint aber noch weniger sichtbar zu sein. Der detaillierte Vorschlag diskutiert ausführlich die verschiedenen Vor- und Nachteile. Aber vielleicht schätzen wir das falsch ein.

3) Der Grund für diesen Vorschlag ist, dass die Fehlerbehandlung (insbesondere der zugehörige Boilerplate-Code) von der Go-Community als ein wichtiges Problem in Go (neben dem Fehlen von Generika) erwähnt wurde. Dieser Vorschlag befasst sich direkt mit dem Boilerplate-Problem. Es löst nicht mehr als den grundlegendsten Fall, da jeder komplexere Fall besser mit dem behandelt werden kann, was wir bereits haben. Während also eine gute Anzahl von Menschen mit dem Status quo zufrieden ist, gibt es eine (wahrscheinlich) ebenso große Anzahl von Menschen, die einen rationaleren Ansatz wie try lieben würden, wohl wissend, dass dies „nur“ syntethischer Zucker.

4) Der Debugging-Punkt ist ein berechtigtes Anliegen. Wenn zwischen dem Erkennen eines Fehlers und einem return Code hinzugefügt werden muss, kann es lästig sein, einen try -Ausdruck in eine if -Anweisung umzuschreiben.

5) Benannte Rückgabewerte: Das ausführliche Dokument diskutiert dies ausführlich. Wenn dies das Hauptanliegen dieses Vorschlags ist, dann sind wir, denke ich, an einem guten Ort.

6) Optionales Handler-Argument für try : Das ausführliche Dokument behandelt dies ebenfalls. Siehe den Abschnitt über Entwurfsiterationen.

7) Die Verwendung gofmt zum Formatieren try -Ausdrücken, sodass sie besonders sichtbar sind, wäre sicherlich eine Option. Aber es würde einige der Vorteile von try wegnehmen, wenn es in einem Ausdruck verwendet wird.

8) Wir haben in Betracht gezogen, das Problem aus der Sicht der Fehlerbehandlung ( handle ) und nicht aus der Sicht der Fehlerprüfung ( try ) zu betrachten. Insbesondere haben wir kurz darüber nachgedacht, nur den Begriff eines Fehlerbehandlers einzuführen (ähnlich dem ursprünglichen Designentwurf, der auf der letztjährigen Gophercon vorgestellt wurde). Der Gedanke war, dass, wenn (und nur wenn) ein Handler deklariert wird, in Zuweisungen mit mehreren Werten, bei denen der letzte Wert vom Typ error ist, dieser Wert einfach in einer Zuweisung weggelassen werden kann. Der Compiler würde implizit prüfen, ob es nicht null ist, und wenn ja, zum Handler verzweigen. Das würde die explizite Fehlerbehandlung vollständig verschwinden lassen und jeden ermutigen, stattdessen einen Handler zu schreiben. Dies schien ein extremer Ansatz zu sein, da es völlig implizit wäre – die Tatsache, dass eine Überprüfung stattfindet, wäre unsichtbar.

9) Darf ich vorschlagen, dass wir den Namen an dieser Stelle nicht radeln. Sobald alle anderen Bedenken geklärt sind, ist ein besserer Zeitpunkt, um den Namen zu verfeinern.

Das soll nicht heißen, dass die Bedenken unbegründet sind – die obigen Antworten geben lediglich unsere derzeitige Denkweise wieder. Für die Zukunft wäre es gut, neue Bedenken (oder neue Beweise zur Untermauerung dieser Bedenken) zu kommentieren – eine bloße Wiederholung dessen, was bereits gesagt wurde, liefert uns keine weiteren Informationen.

Und schließlich scheint es, dass nicht jeder, der sich zu diesem Thema äußert, das ausführliche Dokument gelesen hat. Bitte tun Sie dies, bevor Sie einen Kommentar abgeben, um eine Wiederholung des bereits Gesagten zu vermeiden. Danke.

Dies ist kein Kommentar zum Vorschlag, sondern ein Tippfehlerbericht. Es wurde nicht behoben, seit der vollständige Vorschlag veröffentlicht wurde, also dachte ich, ich erwähne es:

func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)

sollte sein:

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Würde es sich lohnen, offen verfügbaren Go-Code für Anweisungen zur Fehlerprüfung zu analysieren, um herauszufinden, ob sich die meisten Fehlerprüfungen wirklich wiederholen oder ob in den meisten Fällen mehrere Prüfungen innerhalb derselben Funktion unterschiedliche Kontextinformationen hinzufügen? Der Vorschlag wäre für den ersten Fall sehr sinnvoll, würde aber für den zweiten nicht helfen. Im letzteren Fall verwenden die Leute entweder weiterhin if err != nil oder geben das Hinzufügen von zusätzlichem Kontext auf, verwenden try() und greifen auf das Hinzufügen eines allgemeinen Fehlerkontexts pro Funktion zurück, was meiner Meinung nach schädlich wäre. Mit den kommenden Fehlerwertfunktionen erwarten wir, dass Leute häufiger Fehler mit mehr Informationen umschließen. Wahrscheinlich habe ich den Vorschlag falsch verstanden, aber AFAIU, dies hilft nur dann, die Boilerplate zu reduzieren, wenn alle Fehler einer einzelnen Funktion auf genau eine Weise umschlossen werden müssen, und hilft nicht, wenn eine Funktion fünf Fehler behandelt, die möglicherweise unterschiedlich umschlossen werden müssen. Ich bin mir nicht sicher, wie häufig solche Fälle in freier Wildbahn sind (ziemlich häufig in den meisten meiner Projekte), aber ich mache mir Sorgen, dass try() die Leute dazu ermutigen könnte, gemeinsame Wrapper pro Funktion zu verwenden, selbst wenn es sinnvoll wäre, verschiedene Fehler zu umschließen anders.

Nur ein kurzer Kommentar, der mit Daten eines kleinen Beispielsatzes unterlegt ist:

Wir schlagen eine neue integrierte Funktion namens try vor, die speziell dafür entwickelt wurde, die Boilerplate-if-Anweisungen zu eliminieren, die normalerweise mit der Fehlerbehandlung in Go verbunden sind

Wenn dies das Kernproblem ist, das durch diesen Vorschlag gelöst wird, finde ich, dass dieser „Boilerplate“ nur ~ 1,4 % meines Codes in Dutzenden von öffentlich verfügbaren Open-Source-Projekten mit insgesamt ~ 60.000 SLOC ausmacht.

Neugierig, ob jemand ähnliche Statistiken hat?

Bei einer viel größeren Codebasis wie Go selbst mit einem Gesamtvolumen von etwa ~1,6 Mio. SLOC entspricht dies etwa ~0,5 % der Codebasis mit Zeilen wie if err != nil .

Ist dies wirklich das wirkungsvollste Problem, das mit Go 2 gelöst werden kann?

Vielen Dank @griesemer , dass du dir die Zeit genommen hast, alle Ideen durchzugehen und explizit Gedanken zu liefern. Ich denke, dass es wirklich dazu beiträgt, dass die Gemeinschaft in diesem Prozess gehört wird.

  1. @pierrec schlug vor, dass gofmt try-Ausdrücke geeignet formatieren könnte, um sie besser sichtbar zu machen.
    Alternativ könnte man vorhandenen Code kompakter machen, indem man gofmt erlaubt, if-Anweisungen zu formatieren, die auf Fehler in einer Zeile prüfen (@zeebo).
  1. Die Verwendung gofmt zum Formatieren try -Ausdrücken, sodass sie besonders sichtbar sind, wäre sicherlich eine Option. Aber es würde einige der Vorteile von try wegnehmen, wenn es in einem Ausdruck verwendet wird.

Dies sind wertvolle Gedanken darüber, dass gofmt try formatieren muss, aber ich bin daran interessiert, ob es irgendwelche Gedanken gibt, insbesondere zu gofmt , die die Überprüfung der if -Anweisung zulassen der Fehler ist eine Zeile. Der Vorschlag wurde mit der Formatierung try in einen Topf geworfen, aber ich denke, es ist eine völlig orthogonale Sache. Danke.

@griesemer vielen Dank für die unglaubliche Arbeit, all die Kommentare durchzugehen und die meisten, wenn nicht alle Rückmeldungen zu beantworten 🎉

Eine Sache, die in Ihrem Feedback nicht angesprochen wurde, war die Idee, den Werkzeug-/Überprüfungsteil der Go-Sprache zu verwenden, um die Fehlerbehandlung zu verbessern, anstatt die Go-Syntax zu aktualisieren.

Mit der Landung des neuen LSP ( gopls ) scheint es beispielsweise ein perfekter Ort zu sein, um die Signatur einer Funktion zu analysieren und sich um die Fehlerbehandlungs-Boilerplate für den Entwickler zu kümmern, auch mit ordnungsgemäßer Umhüllung und Überprüfung.

@griesemer Ich bin mir sicher, dass dies nicht gut durchdacht ist, aber ich habe versucht, Ihren Vorschlag näher an etwas zu modifizieren, mit dem ich mich hier wohlfühlen würde: https://www.reddit.com/r/golang/comments/bwvyhe /proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x

@zeebo Es wäre einfach, gofmt if err != nil { return ...., err } in einer einzigen Zeile zu formatieren. Vermutlich wäre es nur für diese spezielle Art von if -Mustern, nicht für alle "kurzen" if -Anweisungen?

In ähnlicher Weise gab es Bedenken, dass try unsichtbar sein könnte, weil es auf derselben Linie wie die Geschäftslogik steht. Wir haben all diese Möglichkeiten:

Aktueller Stil:

a, b, c, ... err := BusinessLogic(...)
if err != nil {
   return ..., err
}

Einzeilig if :

a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }

try in einer separaten Zeile (!):

a, b, c, ... err := BusinessLogic(...)
try(err)

try wie vorgeschlagen:

a, b, c := try(BusinessLogic(...))

Die erste und die letzte Zeile scheinen (für mich) am klarsten zu sein, besonders wenn man sich daran gewöhnt hat, try als das zu erkennen, was es ist. Mit der letzten Zeile wird zwar explizit auf einen Fehler geprüft, aber da es (normalerweise) nicht die Hauptaktion ist, tritt es etwas mehr in den Hintergrund.

@marwan-at-work Ich bin mir nicht sicher, was Sie vorschlagen, dass die Tools für Sie tun. Schlagen Sie vor, dass sie die Fehlerbehandlung irgendwie verstecken?

@Dpinela

@guybrand Das scheint ein ganz anderer Vorschlag zu sein; Ich denke, Sie sollten es als eigene Ausgabe einreichen, damit sich diese auf die Diskussion des Vorschlags von @griesemer konzentrieren kann.

IMO unterscheidet sich mein Vorschlag nur in der Syntax, was bedeutet:

  • Ziele sind in Inhalt und Priorität ähnlich.
  • Die Idee, jeden Fehler in einer eigenen Zeile zu erfassen und dementsprechend (wenn nicht null) die Funktion zu verlassen, während eine Handler-Funktion durchlaufen wird, ist ähnlich (Pseudo-Asm - es ist ein "jnz" und "Call").
  • Dies bedeutet sogar, dass die Anzahl der Zeilen in einem Funktionskörper (ohne die Verzögerung) und ein Fluss genau gleich aussehen würden (und dementsprechend würde AST wahrscheinlich auch gleich ausfallen).

Der Hauptunterschied besteht also darin, ob wir den ursprünglichen Funktionsaufruf mit try(func()) umschließen, das immer die letzte Variable analysiert, um den Aufruf zu jnz, oder ob wir dafür den tatsächlichen Rückgabewert verwenden.

Ich weiß, es sieht anders aus, aber eigentlich sehr ähnlich im Konzept.
Auf der anderen Seite - wenn Sie den üblichen Versuch unternehmen .... in vielen C-ähnlichen Sprachen zu fangen - wäre das eine ganz andere Implementierung, andere Lesbarkeit usw.

Ich denke jedoch ernsthaft darüber nach, einen Vorschlag zu schreiben, danke für die Idee.

@griesemer

Ich bin mir nicht sicher, was Sie vorschlagen, dass die Tools für Sie tun. Schlagen Sie vor, dass sie die Fehlerbehandlung irgendwie verstecken?

Ganz im Gegenteil: Ich schlage vor, dass gopls optional die Boilerplate für die Fehlerbehandlung für Sie schreiben kann.

Wie Sie in Ihrem letzten Kommentar erwähnt haben:

Der Grund für diesen Vorschlag ist, dass die Fehlerbehandlung (insbesondere der zugehörige Boilerplate-Code) von der Go-Community als ein bedeutendes Problem in Go (neben dem Fehlen von Generika) erwähnt wurde

Der Kern des Problems besteht also darin, dass der Programmierer am Ende eine Menge Boilerplate-Code schreibt. Es geht also ums Schreiben, nicht ums Lesen. Daher mein Vorschlag: Lassen Sie den Computer (tooling/gopls) das Schreiben für den Programmierer erledigen, indem Sie die Funktionssignatur analysieren und geeignete Fehlerbehandlungsklauseln platzieren.

Zum Beispiel:

// user begins to write this function: 
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  return bts, nil
}

Dann löst der Benutzer das Tool aus, vielleicht indem er einfach die Datei speichert (ähnlich wie gofmt/goimports normalerweise funktionieren) und gopls würde sich diese Funktion ansehen, ihre Rückgabesignatur analysieren und den Code so erweitern:

// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  return bts, nil
}

Auf diese Weise erhalten wir das Beste aus beiden Welten: Wir erhalten die Lesbarkeit/Eindeutigkeit des aktuellen Fehlerbehandlungssystems, und der Programmierer hat keine Fehlerbehandlungsbausteine ​​geschrieben. Noch besser, der Benutzer kann später die Fehlerbehandlungsblöcke ändern, um ein anderes Verhalten zu erzielen: gopls kann verstehen, dass der Block existiert, und es würde ihn nicht ändern.

Wie würde das Tool wissen, dass ich beabsichtigte, err später in der Funktion zu verarbeiten, anstatt vorzeitig zurückzukehren? Obwohl selten, aber Code habe ich trotzdem geschrieben.

Ich entschuldige mich, falls dies schon einmal erwähnt wurde, aber ich konnte keinen Hinweis darauf finden.

try(DoSomething()) liest sich gut für mich und macht Sinn: Der Code versucht, etwas zu tun. try(err) , OTOH, fühlt sich semantisch etwas daneben: Wie versucht man einen Fehler? Meiner Meinung nach könnte man einen Fehler _testen_ oder _überprüfen_, aber einen _versuchen_ scheint nicht richtig zu sein.

Mir ist klar, dass das Zulassen try(err) aus Konsistenzgründen wichtig ist: Ich nehme an, es wäre seltsam, wenn try(DoSomething()) funktionieren würde, aber err := DoSomething(); try(err) nicht. Trotzdem fühlt es sich so an, als würde try(err) auf der Seite etwas seltsam aussehen. Mir fallen keine anderen integrierten Funktionen ein, die so einfach so seltsam aussehen können.

Ich habe dazu keine konkreten Vorschläge, wollte aber dennoch diese Bemerkung machen.

@Griesemer Danke. Tatsächlich sollte der Vorschlag nur für return sein, aber ich vermute, dass es gut wäre, wenn jede einzelne Aussage eine einzelne Zeile sein könnte. Zum Beispiel in einem Test, den man ohne Änderungen an der Testbibliothek haben könnte

if err != nil { t.Fatal(err) }

Die erste und die letzte Zeile scheinen (für mich) am klarsten zu sein, besonders wenn man sich daran gewöhnt hat, try als das zu erkennen, was es ist. Mit der letzten Zeile wird zwar explizit auf einen Fehler geprüft, aber da es (normalerweise) nicht die Hauptaktion ist, tritt es etwas mehr in den Hintergrund.

Mit der letzten Zeile wird ein Teil der Kosten ausgeblendet. Wenn Sie den Fehler kommentieren möchten, von dem ich glaube, dass die Community lautstark gesagt hat, dass es sich um eine bewährte Vorgehensweise handelt und ermutigt werden sollte, müssten Sie die Funktionssignatur ändern, um die Argumente zu benennen, und hoffen, dass ein einzelnes defer angewendet wird jeder Ausgang im Funktionsrumpf, sonst hat try keinen Wert; vielleicht sogar negativ aufgrund seiner Leichtigkeit.

Ich habe nichts mehr hinzuzufügen, was meines Erachtens nicht schon gesagt wurde.


Ich habe nicht gesehen, wie ich diese Frage aus dem Designdokument beantworten soll. Was macht dieser Code:

func foo() (err error) {
    src := try(getReader())
    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }
        try(err)
        println(n)
    }
    return nil
}

Mein Verständnis ist, dass es entzuckern würde

func foo() (err error) {
    tsrc, te := getReader()
    if err != nil {
        err = te
        return
    }
    src := tsrc

    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }

        terr := err
        if terr != nil {
            err = terr
            return
        }

        println(n)
    }
    return nil
}

die nicht kompiliert werden kann, weil err während einer nackten Rückkehr beschattet wird. Würde das nicht kompilieren? Wenn ja, ist das ein sehr subtiler Fehler und scheint nicht allzu unwahrscheinlich zu sein. Wenn nicht, dann ist mehr los als etwas Zucker.

@marwan-at-work

Wie Sie in Ihrem letzten Kommentar erwähnt haben:

Der Grund für diesen Vorschlag ist, dass die Fehlerbehandlung (insbesondere der zugehörige Boilerplate-Code) von der Go-Community als ein bedeutendes Problem in Go (neben dem Fehlen von Generika) erwähnt wurde

Der Kern des Problems besteht also darin, dass der Programmierer am Ende eine Menge Boilerplate-Code schreibt. Es geht also ums Schreiben, nicht ums Lesen.

Ich denke, es ist eigentlich umgekehrt - für mich ist das größte Ärgernis mit der aktuellen Fehlerbehandlungs-Boilerplate nicht so sehr, dass ich sie tippen muss, sondern wie sie den glücklichen Pfad der Funktion vertikal über den Bildschirm streut, was es schwieriger macht, sie zu verstehen ein Blick. Der Effekt ist besonders ausgeprägt in E/A-lastigem Code, wo normalerweise zwischen zwei Operationen ein Block mit Boilerplates steht. Selbst eine vereinfachte Version von CopyFile benötigt ~20 Zeilen, obwohl sie eigentlich nur fünf Schritte ausführt: Quelle öffnen, Quelle schließen verschieben, Ziel öffnen, Quelle kopieren -> Ziel, Ziel schließen.

Ein weiteres Problem mit der aktuellen Syntax besteht darin, dass Sie, wie ich bereits erwähnt habe, wenn Sie eine Kette von Operationen haben, von denen jede einen Fehler zurückgeben kann, die aktuelle Syntax Sie dazu zwingt, allen Zwischenergebnissen Namen zu geben, selbst wenn Sie dies vorziehen würden etwas anonym lassen. Wenn dies passiert, schadet es auch der Lesbarkeit, weil Sie Gehirnzyklen damit verbringen müssen, diese Namen zu analysieren, obwohl sie nicht sehr informativ sind.

Ich mag try in einer separaten Zeile.
Und ich hoffe, dass es handler func unabhängig spezifizieren kann.

func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    try(err)

    handle := func(err error) error {
        tx.Rollback()
        return err
    }

    var res int64
    _, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(err, handle)

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(err, handle)

    return tx.Commit()
}

@zeebo : Die Beispiele , die ich gegeben habe, sind 1: 1-Übersetzungen. Der erste (traditionelle if ) hat den Fehler nicht behandelt, und die anderen auch nicht. Wenn das erste den Fehler behandelt hat und dies die einzige Stelle wäre, an der ein Fehler in einer Funktion überprüft wird, könnte das erste Beispiel (mit einem if ) die geeignete Wahl zum Schreiben des Codes sein. Wenn es mehrere Fehlerprüfungen gibt, die alle dieselbe Fehlerbehandlung (Wrapping) verwenden, sagen wir, weil sie alle Informationen über die aktuelle Funktion hinzufügen, könnte man eine defer -Anweisung verwenden, um die Fehler alle an einem Ort zu behandeln. Optional könnte man die if 's in try 's umschreiben (oder sie in Ruhe lassen). Wenn mehrere Fehler zu überprüfen sind und sie alle unterschiedlich mit den Fehlern umgehen (was ein Zeichen dafür sein könnte, dass das Anliegen der Funktion zu weit gefasst ist und möglicherweise aufgeteilt werden muss), ist die Verwendung if Weg zu gehen. Ja, es gibt mehrere Möglichkeiten, dasselbe zu tun, und die richtige Wahl hängt sowohl vom Code als auch vom persönlichen Geschmack ab. Während wir in Go nach „one way to do one thing“ streben, ist dies natürlich bereits nicht der Fall, insbesondere für gängige Konstrukte. Wenn zum Beispiel eine if - else - if Sequenz zu lang wird, könnte manchmal eine switch angemessener sein. Manchmal drückt eine Variablendeklaration var x int die Absicht besser aus als x := 0 und so weiter (obwohl nicht jeder darüber glücklich ist).

Zu deiner Frage zum "rewrite": Nein, es gäbe keinen Kompilierungsfehler. Beachten Sie, dass das Umschreiben intern erfolgt (und möglicherweise effizienter ist, als das Codemuster vermuten lässt), und der Compiler sich nicht über eine verdeckte Rückgabe beschweren muss. In Ihrem Beispiel haben Sie eine lokale err -Variable in einem verschachtelten Bereich deklariert. try hätte natürlich immer noch direkten Zugriff auf die Ergebnisvariable err . Die Umschreibung könnte unter der Decke eher so aussehen .

[bearbeitet] PS: Eine bessere Antwort wäre: try ist keine nackte Rückkehr (auch wenn die Umschreibung so aussieht). Schließlich gibt man try explizit ein Argument, das den Fehler enthält (oder ist), der zurückgegeben wird, wenn nicht nil . Der Schattenfehler für nackte Rückgaben ist ein Fehler in der Quelle (nicht in der zugrunde liegenden Übersetzung der Quelle. Der Compiler benötigt den Fehler nicht.

Wenn der endgültige Rückgabetyp der übergeordneten Funktion nicht vom Typ error ist, können wir dann in Panik geraten?

Es wird das Builtin vielseitiger machen (wie z. B. mein Anliegen in #32219 befriedigen)

@pjebs Dies wurde in Betracht gezogen und dagegen entschieden. Bitte lesen Sie das ausführliche Designdokument (das sich ausdrücklich auf Ihr Problem zu diesem Thema bezieht).

Ich möchte auch darauf hinweisen, dass try() als Ausdruck behandelt wird, obwohl es als return-Anweisung funktioniert. Ja, ich weiß, dass try ein eingebautes Makro ist, aber die meisten Benutzer werden dies wie funktionale Programmierung verwenden, denke ich.

func doSomething() (error, error, error, error, error) {
   ...
}
try(try(try(try(try(doSomething)))))

Das Design besagt, dass Sie mit panic untersucht haben, anstatt mit dem Fehler zurückzukehren.

Ich betone einen feinen Unterschied:

Machen Sie genau das, was Ihr aktueller Vorschlag besagt, außer dass Sie die Einschränkung entfernen, dass die übergreifende Funktion einen endgültigen Rückgabetyp vom Typ error haben muss.

Wenn es keinen abschließenden Rückgabewert von error hat => Panik
Bei Verwendung von try für Variablendeklarationen auf Paketebene => Panik (entfernt die Notwendigkeit der MustXXX( ) -Konvention)

Für Einheitentests eine bescheidene Sprachänderung.

@mattn , ich bezweifle sehr, dass eine nennenswerte Anzahl von Leuten solchen Code schreiben wird.

@pjebs , diese Semantik - Panik, wenn in der aktuellen Funktion kein Fehlerergebnis auftritt - ist genau das, was das Designdokument in https://github.com/golang/proposal/blob/master/design/32437-try-builtin bespricht. md#Diskussion.

Bei dem Versuch, try nicht nur innerhalb von Funktionen mit einem Fehlerergebnis nützlich zu machen, hing die Semantik von try außerdem vom Kontext ab: Wenn try auf Paketebene verwendet oder innerhalb einer Funktion ohne ein Fehlerergebnis aufgerufen wurde, try würde in Panik geraten, wenn ein Fehler auftritt. (Nebenbei gesagt, wegen dieser Eigenschaft hieß das eingebaute in diesem Vorschlag eher must als try.) Sich try (oder must) auf diese kontextsensitive Weise zu verhalten, schien natürlich und auch ziemlich nützlich: Es würde die Eliminierung von ermöglichen viele benutzerdefinierte Must-Hilfsfunktionen, die derzeit in Initialisierungsausdrücken für Variablen auf Paketebene verwendet werden. Es würde auch die Möglichkeit eröffnen, try in Unit-Tests über das Testing-Paket zu verwenden.

Die Kontextsensitivität von try wurde jedoch als angespannt angesehen: Beispielsweise könnte sich das Verhalten einer Funktion, die try-Aufrufe enthält, stillschweigend ändern (von möglicherweise panisch zu nicht panisch und umgekehrt), wenn ein Fehlerergebnis zur Signatur hinzugefügt oder daraus entfernt wurde. Dies schien eine zu gefährliche Eigenschaft zu sein. Die offensichtliche Lösung wäre gewesen, die Funktionalität von try in zwei getrennte Funktionen aufzuteilen, must und try (sehr ähnlich zu dem, was in Issue #31442 vorgeschlagen wird). Aber das hätte zwei neue eingebaute Funktionen erfordert, wobei nur try direkt mit dem unmittelbaren Bedarf an besserer Unterstützung bei der Fehlerbehandlung verbunden wäre.

@pjebs Das ist _genau_ das, was wir in einem früheren Vorschlag berücksichtigt haben (siehe detailliertes Dokument, Abschnitt über Design-Iterationen, 4. Absatz):

Bei dem Versuch, try nicht nur innerhalb von Funktionen mit einem Fehlerergebnis nützlich zu machen, hing die Semantik von try außerdem vom Kontext ab: Wenn try auf Paketebene verwendet oder innerhalb einer Funktion ohne ein Fehlerergebnis aufgerufen wurde, try würde in Panik geraten, wenn ein Fehler auftritt. (Nebenbei gesagt, wegen dieser Eigenschaft hieß das eingebaute in diesem Vorschlag eher Muss als Versuch.)

Der (interne) Konsens des Go-Teams war, dass es für try verwirrend wäre, vom Kontext abhängig zu sein und sich so unterschiedlich zu verhalten. Beispielsweise könnte das Hinzufügen eines Fehlerergebnisses zu einer Funktion (oder das Entfernen) das Verhalten der Funktion stillschweigend von Panik zu Nicht-Panik (oder umgekehrt) ändern.

@griesemer Danke für die Klarstellung zum Umschreiben. Ich bin froh, dass es kompiliert wird.

Ich verstehe, dass die Beispiele Übersetzungen waren, die die Fehler nicht kommentierten. Ich habe versucht zu argumentieren, dass try es schwieriger macht, Fehler in gewöhnlichen Situationen gut zu kommentieren, und dass Fehlerkommentare für die Community sehr wichtig sind. Ein großer Teil der bisherigen Kommentare hat Möglichkeiten untersucht, try eine bessere Unterstützung für Anmerkungen hinzuzufügen.

Ich bin nicht der Meinung, dass die Fehler anders behandelt werden müssen, da dies ein Zeichen dafür ist, dass das Anliegen der Funktion zu weit gefasst ist. Ich habe einige Beispiele für behaupteten echten Code aus den Kommentaren übersetzt und sie in einem Dropdown-Menü am Ende meines ursprünglichen Kommentars platziert, und das Beispiel in https://github.com/golang/go/issues/32437#issuecomment - 499007288 Ich denke, demonstriert einen gemeinsamen Fall gut:

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

Der Zweck dieser Funktion besteht darin, eine Vorlage für einige Daten in einer Datei auszuführen. Ich glaube nicht, dass es aufgeteilt werden muss, und es wäre bedauerlich, wenn all diese Fehler nur die Linie erhalten würden, auf der sie durch eine Verzögerung erstellt wurden. Das mag für Entwickler in Ordnung sein, aber es ist viel weniger nützlich für Benutzer.

Ich denke, es ist auch ein kleines Signal dafür, wie subtil die defer wrap(&err, "message: %v", err) Bugs waren und wie sie sogar erfahrenen Go-Programmierern ein Bein stellten.


Um mein Argument zusammenzufassen : Ich denke, dass die Fehleranmerkung wichtiger ist als die ausdrucksbasierte Fehlerprüfung, und wir können einiges an Rauschunterdrückung erreichen, indem wir die Anweisungsbasierte Fehlerprüfung auf eine Zeile statt auf drei beschränken. Danke.

@griesemer Entschuldigung, ich habe einen anderen Abschnitt gelesen, in dem es um Panik ging, und die Diskussion über die Gefahren nicht gesehen.

@zeebo Danke für dieses Beispiel. Es sieht so aus, als wäre die Verwendung einer if -Anweisung in diesem Fall genau die richtige Wahl. Aber auf den Punkt gebracht, kann die Formatierung der ifs in Einzeiler dies ein wenig rationalisieren.

Ich möchte noch einmal die Idee eines Handlers als zweites Argument für try aufbringen, aber mit dem Zusatz, dass das Handler-Argument _erforderlich_, aber nil-fähig ist. Dadurch wird die Behandlung des Fehlers zur Standardeinstellung und nicht zur Ausnahme. In Fällen, in denen Sie den Fehler wirklich unverändert weitergeben möchten, geben Sie dem Handler einfach einen Nullwert an, und try verhält sich genauso wie im ursprünglichen Vorschlag, aber das Argument Null fungiert als visueller Hinweis darauf, dass die Fehler wird nicht behandelt. Es wird während der Codeüberprüfung einfacher zu erkennen sein.

file := try(os.Open("my_file.txt"), nil)

Was soll passieren, wenn der Handler bereitgestellt wird, aber null ist? Sollte es Panik versuchen oder als abwesender Fehlerbehandler behandelt werden?

Wie oben erwähnt, wird sich try gemäß dem ursprünglichen Vorschlag verhalten. Es gäbe keinen abwesenden Fehlerbehandler, nur einen Null-Fehlerbehandler.

Was ist, wenn der Handler mit einem Nicht-Null-Fehler aufgerufen wird und dann ein Null-Ergebnis zurückgibt? Bedeutet dies, dass der Fehler „storniert“ ist? Oder sollte die einschließende Funktion mit einem Nullfehler zurückkehren?

Ich glaube, dass die einschließende Funktion mit einem Nullfehler zurückkehren würde. Es wäre möglicherweise sehr verwirrend, wenn try die Ausführung manchmal fortsetzen könnte, selbst nachdem es einen Nicht-Null-Fehlerwert erhalten hat. Dies würde Handlern ermöglichen, sich unter bestimmten Umständen um den Fehler zu kümmern. Dieses Verhalten könnte beispielsweise in einer Funktion im Stil "Get or Create" nützlich sein.

func getOrCreateObject(obj *object) error {
    defaultObjectHandler := func(err error) error {
        if err == ObjectDoesNotExistErr {
            *obj = object{}
            return nil
        }
        return fmt.Errorf("getting or creating object: %v", err)
    }

    *obj = try(db.getObject(), defaultObjectHandler)
}

Es war auch nicht klar, ob das Zulassen einer optionalen Fehlerbehandlung dazu führen würde, dass Programmierer die ordnungsgemäße Fehlerbehandlung insgesamt ignorieren würden. Es wäre auch einfach, überall eine ordnungsgemäße Fehlerbehandlung durchzuführen, aber ein einziges Auftreten eines Versuchs zu verpassen. Und so weiter.

Ich glaube, dass diese beiden Bedenken gemildert werden, indem der Handler zu einem erforderlichen, nicht brauchbaren Argument gemacht wird. Es erfordert, dass Programmierer eine bewusste, explizite Entscheidung treffen, dass sie ihren Fehler nicht behandeln.

Als Bonus denke ich, dass das Erfordernis des Fehlerbehandlers auch tief verschachtelte try s entmutigt, weil sie weniger kurz sind. Einige mögen dies als Nachteil sehen, aber ich denke, es ist ein Vorteil.

@velovix Ich liebe die Idee, aber warum muss ein Fehlerbehandler erforderlich sein? Kann es nicht standardmäßig nil sein? Warum brauchen wir einen „visuellen Hinweis“?

@griesemer Was wäre, wenn die Idee von @velovix übernommen würde, aber mit builtin , das eine vordefinierte Funktion enthält, die Fehler in Panik umwandelt UND wir die Anforderung entfernen, dass die übergreifende Funktion einen Fehlerrückgabewert hat?

Die Idee ist, wenn die übergreifende Funktion keinen Fehler zurückgibt, ist die Verwendung try ohne Error-Handler ein Kompilierzeitfehler.

Der Fehlerhandler kann auch verwendet werden, um den bald zurückgegebenen Fehler mithilfe verschiedener Bibliotheken usw. an der Fehlerstelle einzuschließen, anstatt ein defer oben, das einen benannten zurückgegebenen Fehler modifiziert.

@pjebs

Warum muss ein Error-Handler benötigt werden? Kann es nicht standardmäßig null sein? Warum brauchen wir einen „visuellen Hinweis“?

Damit soll auf die Bedenken eingegangen werden

  1. Der try -Vorschlag, wie er jetzt vorliegt, könnte Leute davon abhalten, Kontext zu ihren Fehlern zu liefern, weil dies nicht ganz so einfach ist.

Einen Handler an erster Stelle zu haben, erleichtert das Bereitstellen von Kontext, und wenn der Handler ein erforderliches Argument ist, wird eine Nachricht gesendet: Der übliche, empfohlene Fall besteht darin, den Fehler auf irgendeine Weise zu behandeln oder zu kontextualisieren, und nicht einfach den Stapel hochzureichen. Es entspricht der allgemeinen Empfehlung der Go-Community.

  1. Ein Anliegen aus dem ursprünglichen Vorschlagsdokument. Ich habe es in meinem ersten Kommentar zitiert:

Es war auch nicht klar, ob das Zulassen einer optionalen Fehlerbehandlung dazu führen würde, dass Programmierer die ordnungsgemäße Fehlerbehandlung insgesamt ignorieren würden. Es wäre auch einfach, überall eine ordnungsgemäße Fehlerbehandlung durchzuführen, aber ein einziges Auftreten eines Versuchs zu verpassen. Und so weiter.

Ein explizites nil übergeben zu müssen, macht es schwieriger zu vergessen, einen Fehler richtig zu behandeln. Sie müssen sich explizit dafür entscheiden, den Fehler nicht zu behandeln, anstatt dies implizit durch Weglassen eines Arguments zu tun.

Denken Sie weiter über die unter https://github.com/golang/go/issues/32437#issuecomment -498947603 kurz erwähnte bedingte Rückgabe nach.
Es scheint
return if f, err := os.Open("/my/file/path"); err != nil
würde besser mit dem Aussehen von Gos existierendem if konform gehen.

Wenn wir eine Regel für die return if Anweisung hinzufügen, dass
wenn der letzte Bedingungsausdruck (wie err != nil ) nicht vorhanden ist,und die letzte Variable der Deklaration in der Anweisung return if ist vom Typ error ,dann wird der Wert der letzten Variablen automatisch mit nil als implizite Bedingung verglichen.

Dann kann die return if -Anweisung abgekürzt werden zu:
return if f, err := os.Open("my/file/path")

Das kommt dem Signal-Rausch-Verhältnis sehr nahe, das der try bietet.
Wenn wir return if in try ändern, wird es
try f, err := os.Open("my/file/path")
Es ähnelt wieder anderen vorgeschlagenen Variationen des try in diesem Thread, zumindest syntaktisch.
Ich persönlich bevorzuge in diesem Fall immer noch return if gegenüber try , weil es die Austrittspunkte einer Funktion sehr deutlich macht. Zum Beispiel hebe ich beim Debuggen oft das Schlüsselwort return im Editor hervor, um alle Austrittspunkte einer großen Funktion zu identifizieren.

Leider scheint es auch bei der Unannehmlichkeit des Einfügens von Debug-Protokollen nicht genug zu helfen.
Es sei denn, wir erlauben auch einen body Block für return if , wie
Original:

        return if f, err := os.Open("my/path") 

Beim Debuggen:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

Die Bedeutung des Körperblocks von return if ist offensichtlich, nehme ich an. Es wird vor defer und zurück ausgeführt.

Allerdings habe ich keine Beschwerden über den bestehenden Fehlerbehandlungsansatz in Go.
Ich mache mir mehr Sorgen darüber, wie sich das Hinzufügen der neuen Fehlerbehandlung auf die derzeitige Güte von Go auswirken würde.

@velovix Uns gefiel die Idee eines try mit einer expliziten Handler-Funktion als 2. Argument sehr gut. Aber es gab zu viele Fragen, auf die es keine offensichtlichen Antworten gab, wie das Designdokument feststellt. Sie haben einige davon auf eine Weise beantwortet, die Ihnen vernünftig erscheint. Es ist sehr wahrscheinlich (und das war unsere Erfahrung innerhalb des Go-Teams), dass jemand anderes denkt, dass die richtige Antwort eine ganz andere ist. Zum Beispiel geben Sie an, dass das Handler-Argument immer angegeben werden sollte, aber dass es nil sein kann, um es deutlich zu machen, wir kümmern uns nicht um die Behandlung des Fehlers. Was passiert nun, wenn man einen Funktionswert (kein nil -Literal) bereitstellt und dieser Funktionswert (in einer Variablen gespeichert) zufällig null ist? Analog zum expliziten nil ist keine Handhabung erforderlich. Aber andere könnten argumentieren, dass dies ein Fehler im Code ist. Oder alternativ könnte man nullwertige Handler-Argumente zulassen, aber dann könnte eine Funktion in einigen Fällen Fehler inkonsistent behandeln und in anderen nicht, und es ist nicht unbedingt aus dem Code ersichtlich, was man tut, weil es so aussieht, als ob ein Handler immer vorhanden ist . Ein weiteres Argument war, dass es besser ist, eine Fehlerbehandlungsroutine auf oberster Ebene zu deklarieren, da dies sehr deutlich macht, dass die Funktion Fehler behandelt. Daher die defer . Wahrscheinlich gibt es noch mehr.

Es wäre gut, mehr über dieses Anliegen zu erfahren. Der aktuelle Codierungsstil, der if-Anweisungen zum Testen auf Fehler verwendet, ist so explizit wie möglich. Es ist sehr einfach, auf individueller Basis (für jedes if) zusätzliche Informationen zu einem Fehler hinzuzufügen. Oftmals ist es sinnvoll, alle in einer Funktion erkannten Fehler einheitlich zu behandeln, was mit einem defer geschehen kann – dies ist bereits heute möglich. Es ist die Tatsache, dass wir bereits alle Werkzeuge für eine gute Fehlerbehandlung in der Sprache haben, und das Problem, dass ein Handler-Konstrukt nicht orthogonal zum Zurückstellen ist, was uns dazu veranlasste, einen neuen Mechanismus allein zum Erweitern von Fehlern wegzulassen.

@griesemer - IIUC, Sie sagen, dass für callsite-abhängige Fehlerkontexte die aktuelle if-Anweisung in Ordnung ist. Dagegen ist diese neue try -Funktion für die Fälle nützlich, in denen die Behandlung mehrerer Fehler an einer einzigen Stelle sinnvoll ist.

Ich glaube, die Sorge war, dass, obwohl es in einigen Fällen in Ordnung sein kann, einfach ein if err != nil { return err} zu machen, es normalerweise empfohlen wird, den Fehler vor der Rückkehr zu dekorieren. Und dieser Vorschlag scheint sich mit dem vorherigen zu befassen und tut nicht viel für das letztere. Was im Wesentlichen bedeutet, dass die Leute ermutigt werden, ein Muster mit einfacher Rückgabe zu verwenden.

@agnivade Sie haben Recht, dieser Vorschlag hilft genau nicht bei der Fehlerdekoration (sondern um die Verwendung von defer zu empfehlen). Ein Grund dafür ist, dass es dafür bereits Sprachmechanismen gibt. Sobald eine Fehlerdekoration erforderlich ist, insbesondere auf Einzelfehlerbasis, macht die zusätzliche Menge an Quelltext für den Dekorationscode das if im Vergleich weniger belastend. In Fällen, in denen keine Dekoration erforderlich ist oder die Dekoration immer gleich ist, wird die Boilerplate zu einem sichtbaren Ärgernis und lenkt dann vom wichtigen Code ab.

Die Leute werden bereits ermutigt, ein Muster mit einfacher Rückkehr zu verwenden, try oder kein try , es gibt nur weniger zu schreiben. Wenn ich darüber nachdenke, _die einzige Möglichkeit, Fehlerdekoration zu fördern, besteht darin, sie obligatorisch zu machen_, denn egal, welche Sprachunterstützung verfügbar ist, die Dekoration von Fehlern erfordert mehr Arbeit.

Eine Möglichkeit, den Deal zu versüßen, wäre, so etwas wie try (oder eine analoge Abkürzungsnotation) nur zuzulassen, _wenn_ irgendwo ein expliziter (möglicherweise leerer) Handler bereitgestellt wird (beachten Sie, dass der ursprüngliche Entwurfsentwurf keinen solchen hatte Anforderung auch nicht).

Ich bin mir nicht sicher, ob wir so weit gehen wollen. Lassen Sie mich noch einmal sagen, dass eine Menge perfekter Code, sagen wir Interna einer Bibliothek, nicht überall Fehler schmücken muss. Es ist beispielsweise in Ordnung, Fehler einfach zu verbreiten und zu dekorieren, bevor sie die API-Einstiegspunkte verlassen. (Sie überall auszuschmücken führt tatsächlich nur zu übertriebenen Fehlern, die es schwieriger machen, die wichtigen Fehler zu lokalisieren, wenn die wirklichen Übeltäter verborgen sind; ähnlich wie eine übermäßig ausführliche Protokollierung es schwierig machen kann, zu erkennen, was wirklich vor sich geht).

Ich denke, wir können auch eine Fangfunktion hinzufügen, was ein nettes Paar wäre, also:

func a() int {
  x := randInt()
  // let's assume that this is what recruiters should "fix" for us
  // or this happens in 3rd-party package.
  if x % 1337 != 0 {
    panic("not l33t enough")
  }
  return x
}

func b() error {
  // if a() panics, then x = 0, err = error{"not l33t enough"}
  x, err := catch(a())
  if err != nil {
    return err
  }
  sendSomewhereElse(x)
  return nil
}

// which could be simplified even further

func c() error {
  x := try(catch(a()))
  sendSomewhereElse(x)
  return nil
}

In diesem Beispiel wäre catch() recover() eine Panik und return ..., panicValue .
Natürlich haben wir einen offensichtlichen Eckfall, in dem wir eine Funktion haben, die auch einen Fehler zurückgibt. In diesem Fall denke ich, dass es praktisch wäre, den Fehlerwert einfach weiterzugeben.

Im Grunde können Sie also catch() verwenden, um Panikattacken tatsächlich zu beheben und sie in Fehler umzuwandeln.
das sieht für mich ziemlich komisch aus, weil Go eigentlich keine Ausnahmen hat, aber in diesem Fall haben wir ein ziemlich nettes try()-catch()-Muster, das auch nicht Ihre gesamte Codebasis mit so etwas wie Java ( catch(Throwable) im Hauptmenü + throws LiterallyAnything ). Sie können die Panik von jemandem leicht verarbeiten, als wären es übliche Fehler. Ich habe derzeit etwa 6 Millionen LoC in Go in meinem aktuellen Projekt, und ich denke, dies würde die Dinge zumindest für mich vereinfachen.

@griesemer Danke für deine Zusammenfassung der Diskussion.

Mir fällt auf, dass ein Punkt darin fehlt: Einige Leute haben argumentiert, dass wir mit dieser Funktion warten sollten, bis wir Generika haben, die es uns hoffentlich ermöglichen, dieses Problem auf elegantere Weise zu lösen.

Darüber hinaus gefällt mir auch der Vorschlag von @velovix , und obwohl ich zu schätzen weiß, dass dies einige Fragen aufwirft, wie in der Spezifikation beschrieben, denke ich, dass diese leicht auf vernünftige Weise beantwortet werden können, wie es @velovix bereits getan hat.

Zum Beispiel:

  • Was passiert, wenn man einen Funktionswert (kein Null-Literal) bereitstellt und dieser Funktionswert (in einer Variablen gespeichert) zufällig Null ist? => Behandeln Sie den Fehler nicht, Punkt. Dies ist nützlich, wenn die Fehlerbehandlung vom Kontext abhängt und die Handler-Variable abhängig davon gesetzt wird, ob eine Fehlerbehandlung erforderlich ist oder nicht. Das ist kein Bug, sondern ein Feature. :)

  • Ein weiteres Argument war, dass es besser ist, eine Fehlerbehandlungsroutine auf oberster Ebene zu deklarieren, da dies sehr deutlich macht, dass die Funktion Fehler behandelt. => Definieren Sie also den Fehlerbehandler am Anfang der Funktion als benannte Abschlussfunktion und verwenden Sie diese, sodass auch klar ist, dass der Fehler behandelt werden sollte. Dies ist kein ernstes Problem, eher eine Stilanforderung.

Welche anderen Bedenken gab es? Ich bin mir ziemlich sicher, dass sie alle auf vernünftige Weise ähnlich beantwortet werden können.

Schließlich, wie Sie sagen, "eine Möglichkeit, den Deal zu versüßen, wäre, so etwas wie try (oder eine analoge Abkürzungsnotation) nur zuzulassen, wenn irgendwo ein expliziter (möglicherweise leerer) Handler bereitgestellt wird". Ich denke, wenn wir mit diesem Vorschlag fortfahren wollen, sollten wir es tatsächlich "bis hierher" bringen, um eine ordnungsgemäße "explizite ist besser als implizite" Fehlerbehandlung zu fördern.

@griesemer

Was passiert nun, wenn man einen Funktionswert (kein Null-Literal) bereitstellt und dieser Funktionswert (in einer Variablen gespeichert) zufällig Null ist? Analog zum expliziten Nullwert ist keine Behandlung erforderlich. Aber andere könnten argumentieren, dass dies ein Fehler im Code ist.

Theoretisch scheint dies ein potenzieller Fallstrick zu sein, obwohl es mir schwer fällt, mir eine vernünftige Situation vorzustellen, in der ein Handler versehentlich Null sein würde. Ich stelle mir vor, dass Handler am häufigsten entweder aus einer an anderer Stelle definierten Hilfsfunktion oder als in der Funktion selbst definierter Abschluss stammen. Beides wird wahrscheinlich nicht unerwartet null werden. Sie könnten theoretisch ein Szenario haben, in dem Handler-Funktionen als Argumente an andere Funktionen weitergegeben werden, aber in meinen Augen scheint es ziemlich weit hergeholt. Vielleicht gibt es so ein Muster, das ich nicht kenne.

Ein weiteres Argument war, dass es besser ist, eine Fehlerbehandlungsroutine auf oberster Ebene zu deklarieren, da dies sehr deutlich macht, dass die Funktion Fehler behandelt. Daher die defer .

Wie @beoran erwähnte, würde das Definieren des Handlers als Abschluss am oberen Rand der Funktion im Stil sehr ähnlich aussehen, und so gehe ich persönlich davon aus, dass die Leute Handler am häufigsten verwenden würden. Obwohl ich die Klarheit schätze, die durch die Tatsache gewonnen wird, dass alle Funktionen, die Fehler behandeln, defer verwenden, kann es weniger klar werden, wenn eine Funktion in ihrer Fehlerbehandlungsstrategie auf halbem Weg nach unten in der Funktion schwenken muss. Dann gibt es zwei defer zu sehen und der Leser muss darüber nachdenken, wie sie miteinander interagieren werden. Dies ist eine Situation, in der ich glaube, dass ein Handler-Argument sowohl klarer als auch ergonomischer wäre, und ich denke, dass dies ein _relativ_ häufiges Szenario sein wird.

Ist es möglich, dass es ohne Klammern funktioniert?

Dh sowas wie:
a := try func(some)

@Cyberax - Wie bereits oben erwähnt, ist es sehr wichtig, dass Sie das Designdokument sorgfältig lesen, bevor Sie es veröffentlichen. Da es sich um eine stark frequentierte Ausgabe handelt, haben viele Leute abonniert.

Das Dokument behandelt Operatoren vs. Funktionen im Detail.

Ich mag das viel mehr als ich die August-Version mochte.

Ich denke, dass ein Großteil des negativen Feedbacks, das Rücksendungen ohne das Schlüsselwort return nicht direkt widerspricht, in zwei Punkten zusammengefasst werden kann:

  1. Menschen mögen keine benannten Ergebnisparameter, die in den meisten Fällen erforderlich wären
  2. es rät davon ab, detaillierten Kontext zu Fehlern hinzuzufügen

Siehe zum Beispiel:

Die Widerlegung dieser beiden Einwände lautet jeweils:

  1. "wir entschieden, dass [benannte Ergebnisparameter] in Ordnung waren"
  2. "Niemand wird Sie dazu zwingen, try zu verwenden" / es wird nicht für 100 % der Fälle angemessen sein

Ich habe nicht wirklich etwas zu 1 zu sagen (ich fühle mich nicht stark danach). Aber zu 2 möchte ich anmerken, dass der August-Vorschlag dieses Problem nicht hatte, die meisten Gegenvorschläge haben dieses Problem auch nicht.

Insbesondere hatte weder der tryf -Gegenvorschlag (der zweimal unabhängig voneinander in diesem Thread gepostet wurde) noch der try(X, handlefn) -Gegenvorschlag (der Teil der Design-Iterationen war) dieses Problem.

Ich denke, es ist schwer zu argumentieren, dass try , so wie es ist, die Leute davon abhält, Fehler mit relevantem Kontext zu dekorieren, und zu einer einzigen generischen Fehlerdekoration pro Funktion führt.

Aus diesen Gründen denke ich, dass es sich lohnt, dieses Problem anzugehen, und ich möchte eine mögliche Lösung vorschlagen:

  1. Derzeit kann der Parameter von defer nur ein Funktions- oder Methodenaufruf sein. Erlauben Sie defer auch einen Funktionsnamen oder ein Funktionsliteral zu haben, dh
defer func(...) {...}
defer packageName.functionName
  1. Wenn Panic oder Deferreturn auf diese Art von Verzögerung stoßen, rufen sie die Funktion auf und übergeben den Nullwert für alle ihre Parameter

  2. try darf mehr als einen Parameter haben

  3. Wenn try auf den neuen Verzögerungstyp trifft, ruft es die Funktion auf und übergibt einen Zeiger auf den Fehlerwert als ersten Parameter, gefolgt von allen eigenen Parametern von try , außer dem ersten.

Zum Beispiel gegeben:

func errorfn() error {
    return errors.New("an error")
}


func f(fail bool) {
    defer func(err *error, a, b, c int) {
        fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
    }
    if fail {
        try(errorfn, 1, 2, 3)
    }
}

Folgendes wird passieren:

f(false)        // prints "a=0 b=0 c=0"
f(true)         // prints "a=1 b=2 c=3"

Der Code in https://github.com/golang/go/issues/32437#issuecomment -499309304 von @zeebo könnte dann wie folgt umgeschrieben werden:

func (c *Config) Build() error {
    defer func(err *error, msg string, args ...interface{}) {
        if *err == nil || msg == "" {
            return
        }
        *err = errors.WithMessagef(err, msg, args...)
    }
    pkgPath := try(c.load(), "load config dir")

    b := bytes.NewBuffer(nil)
    try(templates.ExecuteTemplate(b, "main", c), "execute main template")

    buf := try(format.Source(b.Bytes()), "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
    // ...
}

Und Definieren von ErrorHandlef als:

func HandleErrorf(err *error, format string, args ...interface{}) {
        if *err != nil && format != "" {
                *err = fmt.Errorf(format + ": %v", append(args, *err)...)
        }
}

würde jedem das begehrte tryf kostenlos geben, ohne Formatzeichenfolgen im fmt -Stil in die Kernsprache zu ziehen.

Diese Funktion ist abwärtskompatibel, da defer keine Funktionsausdrücke als Argument zulässt. Es werden keine neuen Schlüsselwörter eingeführt.
Die Änderungen, die vorgenommen werden müssen, um es zu implementieren, sind zusätzlich zu den in https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md beschriebenen:

  1. dem Parser die neue Art des Zurückstellens beibringen
  2. Ändern Sie den Typprüfer, um zu überprüfen, ob innerhalb einer Funktion alle Verzögerungen, die eine Funktion als Parameter (anstelle eines Aufrufs) haben, auch dieselbe Signatur haben
  3. Ändern Sie den Typprüfer, um zu überprüfen, ob die an try übergebenen Parameter mit der Signatur der an defer übergebenen Funktionen übereinstimmen
  4. Ändern Sie das Backend (?), um den entsprechenden deferproc-Aufruf zu generieren
  5. Ändern Sie die Implementierung von try , um seine Argumente in die Argumente des verzögerten Aufrufs zu kopieren, wenn es auf einen Anruf stößt, der durch die neue Art von Verzögerung verzögert wird.

Nach der Komplexität des check/handle -Entwurfs war ich angenehm überrascht, als ich sah, dass dieser viel einfachere und pragmatischere Vorschlag landete, obwohl ich enttäuscht bin, dass es so viel Widerstand dagegen gegeben hat.

Zugegebenermaßen kommt ein Großteil des Widerstands von Leuten, die mit der gegenwärtigen Ausführlichkeit (eine absolut vernünftige Position) recht zufrieden sind und die vermutlich keinen Vorschlag begrüßen würden, sie zu mildern. Für den Rest von uns denke ich, dass dieser Vorschlag den idealen Punkt trifft, einfach und Go-artig zu sein, nicht zu viel zu versuchen und gut mit den bestehenden Fehlerbehandlungstechniken zu harmonieren, auf die Sie immer zurückgreifen könnten, wenn try hat nicht genau das getan, was Sie wollten.

Zu einigen konkreten Punkten:

  1. Das einzige, was ich an dem Vorschlag nicht mag, ist die Notwendigkeit, einen benannten Fehlerrückgabeparameter zu haben, wenn defer verwendet wird, aber ich kann mir keine andere Lösung vorstellen, die nicht im Widerspruch dazu stehen würde wie der Rest der Sprache funktioniert. Ich denke also, dass wir das einfach akzeptieren müssen, wenn der Vorschlag angenommen wird.

  2. Schade, dass try nicht gut mit dem Testpaket für Funktionen zusammenspielt, die keinen Fehlerwert zurückgeben. Meine eigene bevorzugte Lösung dafür wäre eine zweite eingebaute Funktion (vielleicht ptry oder must ), die immer in Panik geriet, anstatt zurückzukehren, wenn sie auf einen Nicht-Null-Fehler stieß und dies daher sein könnte Wird mit den oben genannten Funktionen verwendet (einschließlich main ). Obwohl diese Idee in der vorliegenden Iteration des Vorschlags abgelehnt wurde, hatte ich den Eindruck, dass es sich um eine „knappe Entscheidung“ handelte und daher für eine erneute Prüfung in Betracht kommen könnte.

  3. Ich denke, es wäre schwierig für die Leute, herauszufinden, was go try(f) oder defer try(f) getan haben, und dass es daher am besten ist, sie einfach ganz zu verbieten.

  4. Ich stimme mit denen überein, die denken, dass die bestehenden Techniken zur Fehlerbehandlung weniger ausführlich aussehen würden, wenn go fmt keine einzeiligen if -Anweisungen neu schreiben würde. Persönlich würde ich eine einfache Regel vorziehen, dass dies für _jede_ einzelne Anweisung if erlaubt wäre, unabhängig davon, ob es um die Fehlerbehandlung geht oder nicht. Tatsächlich konnte ich nie verstehen, warum dies derzeit nicht zulässig ist, wenn einzeilige Funktionen geschrieben werden, bei denen der Körper in derselben Zeile platziert wird, in der die Deklaration zulässig ist.

Bei Dekorationsfehlern

func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}

Dies fühlt sich wesentlich ausführlicher und schmerzhafter an als die bestehenden Paradigmen und nicht so prägnant wie check/handle. Die try()-Variante ohne Umbruch ist prägnanter, aber es fühlt sich an, als würden die Leute am Ende eine Mischung aus try und einfachen Fehlerrückgaben verwenden. Ich bin mir nicht sicher, ob mir die Idee gefällt, Try und einfache Fehlerrückgaben zu mischen, aber ich bin total begeistert davon, Fehler zu dekorieren (und freue mich auf Is/As). Lassen Sie mich denken, dass dies zwar syntaktisch ordentlich ist, aber ich bin mir nicht sicher, ob ich es tatsächlich verwenden möchte. check/handle fühlte etwas, das ich gründlicher umarmen würde.

Ich mag die Einfachheit und den „eine Sache gut machen“-Ansatz sehr. In meinem GoAWK- Interpreter wäre es sehr hilfreich – ich habe ungefähr 100 if err != nil { return nil } -Konstrukte, die es vereinfachen und aufräumen würde, und das in einer ziemlich kleinen Codebasis.

Ich habe die Begründung des Vorschlags gelesen, es zu einem eingebauten und nicht zu einem Schlüsselwort zu machen, und es läuft darauf hinaus, dass der Parser nicht angepasst werden muss. Aber ist das nicht eine relativ kleine Menge Schmerz für Compiler- und Tooling-Autoren, während die zusätzlichen Klammern und die Lesbarkeitsprobleme, die wie eine Funktion aussehen, aber nicht sind, etwas für alle Go-Codierer und Code- Leser müssen aushalten. Meiner Meinung nach ist das Argument (Entschuldigung? :-), dass "aber panic() den Fluss kontrolliert" nicht stichhaltig, da Panik und Erholung von Natur aus außergewöhnlich sind, während try() dies tun wird normale Fehlerbehandlung und Kontrollfluss sein.

Ich würde es auf jeden Fall begrüßen, auch wenn dies so geschehen würde, aber ich würde es stark bevorzugen, wenn der normale Kontrollfluss klar ist, dh über ein Schlüsselwort erfolgt.

Ich bin für diesen Vorschlag. Es vermeidet meinen größten Vorbehalt gegenüber dem vorherigen Vorschlag: die Nicht-Orthogonalität von handle in Bezug auf defer .

Ich möchte zwei Aspekte erwähnen, die meiner Meinung nach oben nicht hervorgehoben wurden.

Erstens, obwohl dieser Vorschlag es nicht einfach macht, kontextspezifischen Fehlertext zu einem Fehler hinzuzufügen, _macht_ er es einfach, Stack-Frame-Fehlerverfolgungsinformationen zu einem Fehler hinzuzufügen: https://play.golang.org/p /YL1MoqR08E6

Zweitens ist try wohl eine faire Lösung für die meisten Probleme, die https://github.com/golang/go/issues/19642 zugrunde liegen. Um ein Beispiel für dieses Problem zu nehmen, könnten Sie try verwenden, um zu vermeiden, dass jedes Mal alle Rückgabewerte ausgegeben werden. Dies ist möglicherweise auch nützlich, wenn By-Value-Strukturtypen mit langen Namen zurückgegeben werden.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    i := f.cached.locations[xx+0]
    j := f.cached.locations[xx+1]
    if j < i {
        try(errInvalidGlyphDataLength)
    }
    if j-i > maxGlyphDataLength {
        try(errUnsupportedGlyphDataLength)
    }
    buf, err = b.view(&f.src, int(i), int(j-i))
    return buf, i, j - i, err
}

Ich finde diesen Vorschlag auch gut.

Und ich habe eine Bitte.

Wie make können wir try erlauben, eine variable Anzahl von Parametern anzunehmen

  • Versuch (f):
    wie oben.
    ein Rückgabefehlerwert ist obligatorisch (als letzter Rückgabeparameter).
    HÄUFIGSTES NUTZUNGSMODELL
  • try(f, doPanic bool):
    wie oben, aber wenn doPanic, dann Panik(err) statt Rückkehr.
    In diesem Modus ist ein Rückgabefehlerwert nicht erforderlich.
  • try(f, fn):
    wie oben, aber rufen Sie vor der Rückkehr fn(err) auf.
    In diesem Modus ist ein Rückgabefehlerwert nicht erforderlich.

Auf diese Weise ist es ein Built-in, das alle Anwendungsfälle verarbeiten kann, während es immer noch explizit ist. Seine Vorteile:

  • immer explizit - keine Notwendigkeit, abzuleiten, ob in Panik geraten oder ein Fehler gesetzt und zurückgekehrt werden soll
  • unterstützt kontextspezifische Handler (aber keine Handler-Kette)
  • unterstützt Anwendungsfälle, in denen es keine Fehlerrückgabevariable gibt
  • unterstützt must(...)-Semantik

Während wiederholtes if err !=nil { return ... err } sicherlich ein hässliches Stottern ist, bin ich damit einverstanden
die denken, dass der try()-Vorschlag sehr schlecht lesbar und etwas unausgesprochen ist.
Die Verwendung benannter Rückgaben ist ebenfalls problematisch.

Wenn diese Art des Aufräumens erforderlich ist, warum nicht try(err) als syntaktischer Zucker für
if err !=nil { return err } :

file, err := os.Open("file.go")
try(err)

zum

file, err := os.Open("file.go")
if err != nil {
   return err
}

Und wenn es mehr als einen Rückgabewert gibt, könnte try(err) return t1, ... tn, err sein
wobei t1, ... tn die Nullwerte der anderen Rückgabewerte sind.

Dieser Vorschlag kann die Notwendigkeit von benannten Rückgabewerten vermeiden und sein,
meiner Meinung nach verständlicher und besser lesbar.

Noch besser wäre meiner Meinung nach:

file, try(err) := os.Open("file.go")

Oder auch

file, err? := os.Open("file.go")

Letzteres ist abwärtskompatibel (? ist derzeit in Bezeichnern nicht zulässig).

(Dieser Vorschlag bezieht sich auf https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. Aber die Beispiele für wiederkehrende Themen scheinen anders zu sein, da dies zu einem Zeitpunkt war, als ein explizites Handle noch diskutiert wurde, anstatt es zu verlassen das zu einer Verschiebung.)

Danke an das go-Team für diesen sorgfältigen, interessanten Vorschlag.

@rogpeppe kommentieren, wenn try den Stack-Frame automatisch hinzufügt, nicht ich, ich bin damit einverstanden, dass ich das Hinzufügen von Kontext entmutige.

@aarzilli - Ist also nach Ihrem Vorschlag eine Verzögerungsklausel jedes Mal obligatorisch, wenn wir tryf zusätzliche Parameter geben?

Was passiert, wenn ich es tue

try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)

und keine defer-Funktion schreiben?

@Agnivade

Was passiert, wenn ich (...) eine Verzögerungsfunktion schreibe und nicht?

Typprüfungsfehler.

Meiner Meinung nach ist die Verwendung try , um das Ausschreiben aller Rückgabewerte zu vermeiden, eigentlich nur ein weiterer Schlag dagegen.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    //...

Ich verstehe den Wunsch, return nil, 0, 0, ErrNotFound nicht ausschreiben zu müssen, aber ich würde das viel lieber auf andere Weise lösen.

Das Wort try bedeutet nicht „Rückkehr“. Und so wird es hier verwendet. Eigentlich würde ich es vorziehen, wenn sich der Vorschlag dahingehend ändert, dass try nicht direkt einen error annehmen kann, weil ich nie möchte, dass jemand solchen Code schreibt ^^ . Es liest sich falsch . Wenn Sie diesen Code einem Neuling zeigen würden, hätte er keine Ahnung, was dieser Versuch bewirkte.

Wenn wir eine Möglichkeit suchen, einfach nur Standardwerte und einen Fehlerwert zurückzugeben, lösen wir das separat. Vielleicht ein anderes eingebautes wie

return default(ErrNotFound)

Zumindest liest sich das mit einer Art Logik.

Aber missbrauchen wir try nicht, um ein anderes Problem zu lösen.

@natefinch Wenn das eingebaute try check wie im ursprünglichen Vorschlag check heißt, wäre es check(err) , was sich wesentlich besser liest, imo.

Abgesehen davon weiß ich nicht, ob es wirklich ein Missbrauch ist, try(err) zu schreiben. Es fällt sauber aus der Definition heraus. Das bedeutet aber andererseits auch, dass dies legal ist:

a, b := try(1, f(), err)

Ich denke, mein Hauptproblem mit try ist, dass es wirklich nur ein panic ist, das nur eine Ebene höher geht ... außer dass es im Gegensatz zu Panik ein Ausdruck ist, keine Aussage, also kannst du dich verstecken es mitten in einer Aussage irgendwo. Das macht es fast schlimmer als Panik.

@natefinch Wenn Sie es sich wie eine Panik vorstellen, die eine Ebene höher geht und dann andere Dinge tut, scheint es ziemlich chaotisch zu sein. Allerdings stelle ich mir das anders vor. Funktionen, die Fehler in Go zurückgeben, geben effektiv ein Ergebnis zurück, um sich locker an die Terminologie von Rust zu lehnen. try ist ein Dienstprogramm, das das Ergebnis entpackt und entweder ein "Fehlerergebnis" zurückgibt, wenn error != nil , oder den T-Teil des Ergebnisses entpackt, wenn error == nil .

Natürlich haben wir in Go eigentlich keine Ergebnisobjekte, aber es ist praktisch das gleiche Muster und try scheint eine natürliche Codierung dieses Musters zu sein. Ich glaube, dass jede Lösung für dieses Problem einige Aspekte der Fehlerbehandlung kodifizieren muss, und try s Lösung erscheint mir vernünftig. Ich selbst und andere schlagen vor, die Möglichkeiten von try etwas zu erweitern, um sie besser an bestehende Go-Fehlerbehandlungsmuster anzupassen, aber das zugrunde liegende Konzept bleibt dasselbe.

@ugorji Die von dir vorgeschlagene try(f, bool) -Variante klingt wie die must aus #32219.

@ugorji Die von dir vorgeschlagene try(f, bool) -Variante klingt wie die must aus #32219.

Ja, so ist es. Ich hatte einfach das Gefühl, dass alle 3 Fälle mit einer einzigen eingebauten Funktion behandelt werden könnten und alle Anwendungsfälle elegant erfüllen.

Da try() bereits magisch ist und den Fehlerrückgabewert kennt, könnte es erweitert werden, um auch einen Zeiger auf diesen Wert zurückzugeben, wenn es in der Nullargumentform aufgerufen wird? Das würde die Notwendigkeit benannter Rückgaben beseitigen, und ich glaube, es hilft, visuell zu korrelieren, woher der Fehler in defer-Anweisungen erwartet wird. Zum Beispiel:

func foo() error {
  defer fmt.HandleErrorf(try(), "important foo context info")
  try(bar())
  try(baz())
  try(etc())
}

@ugorji
Ich denke, der boolesche Wert auf try(f, bool) würde es schwer lesbar und leicht zu übersehen machen. Ich mag Ihren Vorschlag, aber für den Panikfall denke ich, dass dies für Benutzer weggelassen werden könnte, um dies in den Handler aus Ihrem dritten Aufzählungszeichen zu schreiben, z. B. try(f(), func(err error) { panic('at the disco'); }) , dies macht es für Benutzer deutlicher als ein verstecktes try(f(), true) das ist leicht zu übersehen, und ich denke nicht, dass die eingebauten Funktionen Panik auslösen sollten.

@ugorji
Ich denke, der boolesche Wert auf try(f, bool) würde es schwer lesbar und leicht zu übersehen machen. Ich mag Ihren Vorschlag, aber für den Panikfall denke ich, dass dies für Benutzer weggelassen werden könnte, um dies in den Handler aus Ihrem dritten Aufzählungszeichen zu schreiben, z. B. try(f(), func(err error) { panic('at the disco'); }) , dies macht es für Benutzer deutlicher als ein verstecktes try(f(), true) das ist leicht zu übersehen, und ich denke nicht, dass die eingebauten Funktionen Panik auslösen sollten.

Bei weiterem Nachdenken neige ich dazu, Ihrer Position und Ihrer Argumentation zuzustimmen, und es sieht immer noch elegant aus wie ein Einzeiler.

@patrick-nyt ist noch ein weiterer Befürworter der _Zuweisungssyntax_, um einen Nulltest auszulösen, in https://github.com/golang/go/issues/32437#issuecomment -499533464

Dieses Konzept erscheint in 13 separaten Antworten auf den Check/Handle-Vorschlag
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

f, ?return := os.Open(...)
f, ?panic  := os.Open(...)

Warum? Weil es sich wie Go 1 liest, während try() und check dies nicht tun.

Ein Einwand gegen try scheint zu sein, dass es sich um einen Ausdruck handelt. Nehmen wir stattdessen an, dass es eine unäre Postfix-Anweisung ? gibt, die bedeutet, wenn nicht nil zurückgeben. Hier ist das Standardcodebeispiel (unter der Annahme, dass mein vorgeschlagenes zurückgestelltes Paket hinzugefügt wird):

func CopyFile(src, dst string) error {
    var err error // Don't need a named return because err is explicitly named
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r, err := os.Open(src)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    w, err := os.Create(dst)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    _, err = io.Copy(w, r)

    return err
}

Das pgStore-Beispiel:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    err?

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    // tricky bit: this would not change the value of err 
    // but the deferred.Cond would still be triggered by err being set before
    deferred.Format(err, "insert table")?

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    deferred.Format(err, "insert table2")?

    return tx.Commit()
}

Ich mag das von @jargv :

Da try() bereits magisch ist und den Fehlerrückgabewert kennt, könnte es erweitert werden, um auch einen Zeiger auf diesen Wert zurückzugeben, wenn es in der Nullargumentform aufgerufen wird? Das würde die Notwendigkeit benannter Rücksendungen beseitigen

Aber anstatt den Namen try basierend auf der Anzahl der Argumente zu überladen, könnte meiner Meinung nach eine andere Magie eingebaut sein, sagen wir reterr oder so.

Ich habe einige sehr häufig verwendete Pakete durchgesehen und nach Go-Code gesucht, der unter Fehlerbehandlung "leidet", aber vor dem Schreiben gut durchdacht worden sein muss, um herauszufinden, welche "Magie" das vorgeschlagene try() bewirken würde.
Wenn ich den Vorschlag nicht missverstanden habe, würden derzeit viele davon (z. B. nicht sehr einfache Fehlerbehandlung) nicht viel gewinnen oder müssten beim "alten" Fehlerbehandlungsstil bleiben.
Beispiel von net/http/request.go:

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`

trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
    defer func() {
        trace.WroteRequest(httptrace.WroteRequestInfo{
            Err: err,
        })
    }()
}

// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
    if r.URL == nil {
        return errMissingHost
    }
    host = cleanHost(r.URL.Host)
}

// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)

ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
    ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
    // CONNECT requests normally give just the host and port, not a full URL.
    ruri = host
    if r.URL.Opaque != "" {
        ruri = r.URL.Opaque
    }
}
if stringContainsCTLByte(ruri) {
    return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).

// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
    bw = bufio.NewWriter(w)
    w = bw
}

_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
    return err
}

// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
    return err
}
if trace != nil && trace.WroteHeaderField != nil {
    trace.WroteHeaderField("Host", []string{host})
}

// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
    userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
    _, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
    if err != nil {
        return err
    }
    if trace != nil && trace.WroteHeaderField != nil {
        trace.WroteHeaderField("User-Agent", []string{userAgent})
    }
}

// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
    return err
}
err = tw.writeHeader(w, trace)
if err != nil {
    return err
}

err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
    return err
}

if extraHeaders != nil {
    err = extraHeaders.write(w, trace)
    if err != nil {
        return err
    }
}

_, err = io.WriteString(w, "\r\n")
if err != nil {
    return err
}

if trace != nil && trace.WroteHeaders != nil {
    trace.WroteHeaders()
}

// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
    if bw, ok := w.(*bufio.Writer); ok {
        err = bw.Flush()
        if err != nil {
            return err
        }
    }
    if trace != nil && trace.Wait100Continue != nil {
        trace.Wait100Continue()
    }
    if !waitForContinue() {
        r.closeBody()
        return nil
    }
}

if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
    if err := bw.Flush(); err != nil {
        return err
    }
}

// Write body and trailer
err = tw.writeBody(w)
if err != nil {
    if tw.bodyReadError == err {
        err = requestBodyReadError{err}
    }
    return err
}

if bw != nil {
    return bw.Flush()
}
return nil

}
`

oder wie in einem gründlichen Test wie pprof/profile/profile_test.go verwendet:
`
func checkAggregation(prof *Profile, a *aggTest) error {
// Überprüfen Sie, ob die Gesamtzahl der Stichproben für die Zeilen beibehalten wurde.
gesamt := int64(0)

samples := make(map[string]bool)
for _, sample := range prof.Sample {
    tb := locationHash(sample)
    samples[tb] = true
    total += sample.Value[0]
}

if total != totalSamples {
    return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}

// Check the number of unique sample locations
if a.rows != len(samples) {
    return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}

// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
    if m.HasFunctions != a.function {
        return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
    }
    if m.HasFilenames != a.fileline {
        return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
    }
    if m.HasLineNumbers != a.fileline {
        return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
    }
    if m.HasInlineFrames != a.inlineFrame {
        return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
    }
}

// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
    if !a.inlineFrame && len(l.Line) > 1 {
        return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
    }

    for _, ln := range l.Line {
        if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
            return fmt.Errorf("found line %s:%d on location %d, want :0",
                ln.Function.Filename, ln.Line, l.ID)
        }
        if !a.function && (ln.Function.Name != "") {
            return fmt.Errorf(`found file %s location %d, want ""`,
                ln.Function.Name, l.ID)
        }
    }
}

return nil

}
`
Dies sind zwei Beispiele, die mir einfallen, in denen man sagen würde: "Ich hätte gerne eine bessere Fehlerbehandlungsoption"

Kann jemand demonstrieren, wie sich diese mit try() verbessern würden?

Ich bin überwiegend für diesen Vorschlag.

Mein Hauptanliegen, das ich mit vielen Kommentatoren teile, betrifft benannte Ergebnisparameter. Der aktuelle Vorschlag ermutigt sicherlich zu einer viel stärkeren Verwendung von benannten Ergebnisparametern, und ich denke, das wäre ein Fehler. Ich glaube nicht, dass dies einfach eine Frage des Stils ist, wie der Vorschlag besagt: Benannte Ergebnisse sind ein subtiles Merkmal der Sprache, das den Code in vielen Fällen fehleranfälliger oder weniger klar macht. Nach ~8 Jahren des Lesens und Schreibens von Go-Code verwende ich wirklich nur benannte Ergebnisparameter für zwei Zwecke:

  • Dokumentation der Ergebnisparameter
  • Manipulieren eines Ergebniswerts (normalerweise ein error ) innerhalb einer Zurückstellung

Um dieses Problem aus einer neuen Richtung anzugreifen, hier ist eine Idee, die meiner Meinung nach nicht eng mit irgendetwas übereinstimmt, das im Designdokument oder in diesem Kommentarthread zu diesem Thema besprochen wurde. Nennen wir es "Error-Defers":

Ermöglicht die Verwendung von defer zum Aufrufen von Funktionen mit einem impliziten Fehlerparameter.

Also, wenn Sie eine Funktion haben

func f(err error, t1 T1, t2 T2, ..., tn Tn) error

Dann wird in einer Funktion g , in der der letzte Ergebnisparameter den Typ error hat (dh jede Funktion, in der try verwendet werden kann), ein Aufruf von f können wie folgt verschoben werden:

func g() (R0, R0, ..., error) {
    defer f(t0, t1, ..., tn) // err is implicit
}

Die Semantik von error-defer ist:

  1. Der verzögerte Aufruf von f wird mit dem letzten Ergebnisparameter von g als erstem Eingabeparameter von f aufgerufen
  2. f wird nur aufgerufen, wenn dieser Fehler nicht null ist
  3. Das Ergebnis von f wird dem letzten Ergebnisparameter von g zugewiesen

Um also ein Beispiel aus dem alten Dokument zum Fehlerbehandlungsdesign zu verwenden, könnten wir dies tun, indem wir error-defer and try verwenden

func printSum(a, b string) error {
    defer func(err error) error {
        return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
    }()
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

So würde HandleErrorf funktionieren:

func printSum(a, b string) error {
    defer handleErrorf("printSum(%q + %q)", a, b)
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

func handleErrorf(err error, format string, args ...interface{}) error {
    return fmt.Errorf(format+": %v", append(args, err)...)
}

Ein Eckfall, der ausgearbeitet werden müsste, ist der Umgang mit Fällen, in denen nicht eindeutig ist, welche Form der Verzögerung wir verwenden. Ich denke, das passiert nur mit (sehr ungewöhnlichen) Funktionen mit Signaturen wie dieser:

func(error, ...error) error

Es scheint vernünftig zu sagen, dass dieser Fall auf die nicht fehlerverzögernde Weise behandelt wird (und dies die Abwärtskompatibilität bewahrt).


Wenn man in den letzten Tagen über diese Idee nachdenkt, ist sie ein bisschen magisch, aber die Vermeidung von benannten Ergebnisparametern ist ein großer Vorteil zu ihren Gunsten. Da try zu einer stärkeren Verwendung von defer zur Fehlermanipulation anregt, macht es Sinn, dass defer erweitert werden könnte, um es besser für diesen Zweck geeignet zu machen. Außerdem gibt es eine gewisse Symmetrie zwischen try und error-defer.

Schließlich sind Error-Defer heute sogar ohne try nützlich, da sie die Verwendung von benannten Ergebnisparametern zur Manipulation von Fehlerrückgaben ersetzen. Hier ist zum Beispiel eine bearbeitete Version von echtem Code:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
    files := make([]*file, len(keys))

    defer func() {
        if err != nil {
            // Return any successfully retrieved files.
            for _, f := range files {
                if f != nil {
                    c.put(f)
                }
            }
        }
    }()

    // ...
}

Mit error-defer wird dies zu:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
    files := make([]*file, len(keys))

    defer func(err error) error {
        // Return any successfully retrieved files.
        for _, f := range files {
            if f != nil {
                c.put(f)
            }
        }
        return err
    }()

    // ...
}

@beoran In Bezug auf Ihren Kommentar , dass wir auf Generika warten sollten. Generika helfen hier nicht - lesen Sie bitte die FAQ .

In Bezug auf Ihre Vorschläge zum 2-Argument-Standardverhalten von @velovix von try : Wie ich bereits sagte, ist Ihre Vorstellung davon, was die offensichtlich vernünftige Wahl ist, der Alptraum von jemand anderem.

Darf ich vorschlagen, dass wir diese Diskussion fortsetzen, sobald sich ein breiter Konsens entwickelt hat, dass try mit einer expliziten Fehlerbehandlungsroutine eine bessere Idee ist als das aktuelle minimale try . An dieser Stelle ist es sinnvoll, die Feinheiten eines solchen Designs zu diskutieren.

(Eigentlich mag ich es, einen Handler zu haben. Das ist einer unserer früheren Vorschläge. Und wenn wir try so übernehmen, wie es ist, können wir uns immer noch auf ein try mit einem Handler in einem Stürmer zubewegen -kompatibler Weg - zumindest wenn der Handler optional ist. Aber gehen wir einen Schritt nach dem anderen vor.)

@aarzilli Danke für deinen Vorschlag .

Solange Dekorationsfehler optional sind, werden die Leute dazu neigen, es nicht zu tun (es ist immerhin zusätzliche Arbeit). Siehe auch meinen Kommentar hier .

Ich glaube also nicht, dass das vorgeschlagene try die Leute davon abhält, Fehler zu dekorieren (sie sind aus dem oben genannten Grund bereits mit dem if entmutigt); es ist, dass try es nicht _ermutigt_.

(Eine Möglichkeit, dies zu fördern, besteht darin, es in try : Man kann try nur verwenden, wenn man auch den Fehler schmückt oder ausdrücklich ablehnt.)

Aber zurück zu Ihren Vorschlägen: Ich glaube, Sie führen hier noch viel mehr Maschinen ein. Die Semantik von defer zu ändern, nur damit es für try besser funktioniert, ist nichts, was wir in Betracht ziehen möchten, es sei denn, diese defer -Änderungen sind auf allgemeinere Weise von Vorteil. Außerdem bindet Ihr Vorschlag defer mit try zusammen und macht somit diese beiden Mechanismen weniger orthogonal; etwas, das wir vermeiden möchten.

Aber was noch wichtiger ist, ich bezweifle, dass Sie alle zwingen wollen, defer zu schreiben, nur damit sie try verwenden können. Aber ohne dies zu tun, sind wir wieder am Anfang: Die Leute werden dazu neigen, Fehler nicht zu schmücken.

(Ich mag es übrigens, einen Handler zu haben. Das ist einer unserer früheren Vorschläge. Und wenn wir try so übernehmen, wie es ist, können wir immer noch auf vorwärtskompatible Weise zu einem try mit einem Handler übergehen - zumindest wenn der Handler es ist optional. Aber gehen wir einen Schritt nach dem anderen vor.)

Sicher, vielleicht ist ein mehrstufiger Ansatz der richtige Weg. Wenn wir in Zukunft ein optionales Handler-Argument hinzufügen, könnten Tools erstellt werden, die den Autor vor einem nicht behandelten try im gleichen Sinne wie das errcheck -Tool warnen. Unabhängig davon freue ich mich über Ihr Feedback!

@alanfo Vielen Dank für Ihr positives Feedback .

Zu den von Ihnen angesprochenen Punkten:

1) Wenn das einzige Problem mit try die Tatsache ist, dass man eine Fehlerrückgabe benennen muss, damit wir einen Fehler über defer schmücken können, denke ich, dass wir gut sind. Wenn sich herausstellt, dass die Benennung des Ergebnisses ein echtes Problem darstellt, könnten wir es ansprechen. Ein einfacher Mechanismus, den ich mir vorstellen kann, wäre eine vordeklarierte Variable, die ein Alias ​​für ein Fehlerergebnis ist (stellen Sie sich vor, sie enthält den Fehler, der das letzte try ausgelöst hat). Vielleicht gibt es bessere Ideen. Wir haben dies nicht vorgeschlagen, weil es bereits einen Mechanismus in der Sprache gibt, der das Ergebnis benennen soll.
2) try und Testen: Dies kann angegangen und zum Laufen gebracht werden. Siehe ausführliches Dokument.
3) Dies wird explizit in der ausführlichen Dok. angesprochen.
4) Bestätigt.

@benhoyt Vielen Dank für Ihr positives Feedback .

Wenn das Hauptargument gegen diesen Vorschlag die Tatsache ist, dass try eingebaut ist, sind wir in einer großartigen Lage. Die Verwendung eines integrierten ist einfach eine pragmatische Lösung für das Abwärtskompatibilitätsproblem (es verursacht zufällig keine zusätzliche Arbeit für den Parser und die Tools usw. - aber das ist nur ein netter Nebeneffekt, nicht der Hauptgrund). Es gibt auch einige Vorteile, Klammern schreiben zu müssen, dies wird im Design-Dokument (Abschnitt über Eigenschaften des vorgeschlagenen Designs) ausführlich besprochen.

Alles in allem sollten wir das Schlüsselwort try in Betracht ziehen, wenn die Verwendung eines integrierten Schlüsselworts der Showstopper ist. Es ist jedoch nicht abwärtskompatibel mit vorhandenem Code, da das Schlüsselwort mit vorhandenen Bezeichnern in Konflikt geraten kann.

(Um vollständig zu sein, gibt es auch die Option eines Operators wie ? , der abwärtskompatibel wäre. Für eine Sprache wie Go scheint es mir jedoch nicht die beste Wahl zu sein. Aber noch einmal, Wenn das alles ist, was es braucht, um try schmackhaft zu machen, sollten wir es vielleicht in Betracht ziehen.)

@ugorji Danke für dein positives Feedback .

try könnte um ein zusätzliches Argument erweitert werden. Unsere Präferenz wäre, nur eine Funktion mit der Signatur func (error) error zu nehmen. Wenn Sie in Panik geraten möchten, ist es einfach, eine einzeilige Hilfsfunktion bereitzustellen:

func doPanic(err error) error { panic(err) }

Es ist besser, das Design von try einfach zu halten.

@patrick-nyt Was Sie vorschlagen :

file, err := os.Open("file.go")
try(err)

wird mit dem aktuellen Vorschlag möglich sein.

@dpinela , @ugorji Bitte lesen Sie auch das Design-Dokument zum Thema must vs. try . Es ist besser, try so einfach wie möglich zu halten. must ist ein häufiges „Muster“ in Initialisierungsausdrücken, aber es besteht keine dringende Notwendigkeit, das zu „korrigieren“.

@jargv Danke für deinen Vorschlag . Das ist eine interessante Idee (siehe auch meinen Kommentar hier zu diesem Thema). Zusammenfassen:

  • try(x) funktioniert wie vorgeschlagen
  • try() gibt ein *error zurück, das auf das Fehlerergebnis zeigt

Dies wäre in der Tat ein weiterer Weg, um zum Ergebnis zu gelangen, ohne es benennen zu müssen.

@cespare Der Vorschlag von @jargv sieht für mich viel einfacher aus als das, was Sie vorschlagen . Es löst das gleiche Problem des Zugriffs auf den Ergebnisfehler. Was denken Sie?

Gemäß https://github.com/golang/go/issues/32437#issuecomment -499320588:

func doPanic(err Fehler) Fehler { Panik(err) }

Ich gehe davon aus, dass diese Funktion ziemlich häufig sein wird. Könnte dies in "Builtin" (oder irgendwo anders in einem Standardpaket, zB errors ) vordefiniert sein?

Schade, dass Sie Generika nicht mächtig genug erwarten, um sie umzusetzen
ausprobieren, ich hätte eigentlich gehofft, dass es geht.

Ja, dieser Vorschlag könnte ein erster Schritt sein, obwohl ich darin keinen großen Nutzen sehe
es selbst, wie es jetzt steht.

Zugegeben, diese Ausgabe konzentriert sich vielleicht zu sehr auf detaillierte Alternativen,
aber es zeigt sich, dass viele Teilnehmer damit nicht ganz zufrieden sind
es. Was zu fehlen scheint, ist ein breiter Konsens über diesen Vorschlag...

Op vr 7. Juni 2019 01:04 schreef pj [email protected] :

Asper #32437 (Kommentar)
https://github.com/golang/go/issues/32437#issuecomment-499320588 :

func doPanic(err Fehler) Fehler { Panik(err) }

Ich gehe davon aus, dass diese Funktion ziemlich häufig sein wird. Könnte dies vordefiniert sein
in "eingebaut"?


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issuecomment-49969 ,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.

@pjebs , ich habe die entsprechende Funktion Dutzende Male geschrieben. Ich nenne es normalerweise „orDie“ oder „check“. Es ist so einfach, dass es nicht wirklich notwendig ist, es in die Standardbibliothek aufzunehmen. Außerdem möchten verschiedene Personen möglicherweise eine Protokollierung oder was auch immer vor der Beendigung.

@beoran Vielleicht könnten Sie die Verbindung zwischen Generika und Fehlerbehandlung erweitern. Wenn ich an sie denke, scheinen sie zwei verschiedene Dinge zu sein. Generika sind kein Allheilmittel, das alle Probleme mit der Sprache lösen kann. Es ist die Fähigkeit, eine einzelne Funktion zu schreiben, die auf mehreren Typen ausgeführt werden kann.

Dieser spezifische Fehlerbehandlungsvorschlag versucht, Boilerplate zu reduzieren, indem eine vordeklarierte Funktion try eingeführt wird, die die Flusskontrolle unter bestimmten Umständen ändert. Generika werden niemals den Kontrollfluss verändern. Also ich sehe den Zusammenhang nicht wirklich.

Meine erste Reaktion darauf war ein 👎, da ich mir vorgestellt hatte, dass die Behandlung mehrerer fehleranfälliger Aufrufe innerhalb einer Funktion die defer -Fehlerbehandlung verwirrend machen würde. Nachdem ich den ganzen Vorschlag durchgelesen habe, habe ich meine Reaktion auf ein ❤️ und 👍 umgedreht, da ich erfahren habe, dass dies immer noch mit relativ geringer Komplexität erreicht werden kann.

@carlmjohnson Ja, es ist einfach, aber ...

Ich habe die äquivalente Funktion dutzende Male geschrieben.

Die Vorteile einer vordefinierten Funktion sind:

  1. Wir können es einzeilig machen
  2. Wir müssen die err => panic-Funktion nicht in jedem Paket, das wir verwenden, neu deklarieren oder einen gemeinsamen Speicherort dafür pflegen. Da es wahrscheinlich jedem in der Go-Community gemeinsam ist, ist das "Standardpaket" _ der _ übliche Speicherort dafür.

@griesemer Mit der Error-Handler-Variante des ursprünglichen Versuchsvorschlags ist die Anforderung der übergreifenden Funktion zum Zurückgeben von Fehlern jetzt nicht mehr erforderlich.

Als ich mich zum ersten Mal danach erkundigte, wurde ich darauf hingewiesen, dass der Vorschlag es zwar berücksichtigt, aber (aus gutem Grund) für zu gefährlich hielt. Aber wenn wir try() ohne eine Fehlerbehandlungsroutine in einem Szenario verwenden, in dem die übergreifende Funktion keinen Fehler zurückgibt, werden durch die Umwandlung in einen Kompilierzeitfehler die im Vorschlag diskutierten Bedenken gemildert

@pjebs Die Anforderung der übergreifenden Funktion, einen Fehler zurückzugeben, war im ursprünglichen Design nicht erforderlich, _wenn_ ein Fehlerhandler bereitgestellt wurde. Aber es ist nur eine weitere Komplikation von try . Es ist _viel_ besser, es einfach zu halten. Stattdessen wäre es klarer, eine separate must -Funktion zu haben, die bei einem Fehler immer in Panik gerät (aber ansonsten wie try ist). Dann ist es offensichtlich, was im Code passiert und man muss nicht auf den Kontext schauen.

Die Hauptattraktion eines solchen must wäre, dass es mit Unit-Tests verwendet werden könnte; besonders wenn das testing -Paket geeignet angepasst wurde, um sich von den durch must verursachten Paniken zu erholen und sie auf nette Weise als Testfehler zu melden. Aber warum noch einen weiteren neuen Sprachmechanismus hinzufügen, wenn wir das Testpaket einfach so anpassen können, dass es auch Testfunktionen der Form TestXxx(t *testing.T) error akzeptiert? Wenn sie einen Fehler zurückgeben, was doch ganz natürlich erscheint (vielleicht hätten wir das von Anfang an tun sollen), dann wird try gut funktionieren. Lokale Tests erfordern etwas mehr Arbeit, sind aber wahrscheinlich machbar.

Die andere relativ häufige Verwendung für must ist in globalen Initialisierungsausdrücken ( must(regexp.Compile... usw.). If wäre ein "nice to have", aber das hebt es nicht unbedingt auf das Niveau, das für ein neues Sprachfeature erforderlich ist.

@griesemer Angesichts der Tatsache, dass must vage mit try verwandt ist, und angesichts der Tatsache, dass die Dynamik dahin geht, dass try implementiert wird, denken Sie nicht, dass es gut ist, must in Betracht zu ziehen

Die Chancen stehen gut, dass es, wenn es in dieser Runde nicht diskutiert wird, einfach nicht implementiert/ernsthaft in Betracht gezogen wird, zumindest für mehr als 3 Jahre (oder vielleicht überhaupt). Die Diskussionsüberschneidung wäre auch gut, anstatt bei Null anzufangen und Diskussionen zu recyceln.

Viele Leute haben gesagt, dass must try passt.

@pjebs Es sieht ganz sicher nicht so aus, als gäbe es im Moment eine „Dynamik zur Implementierung try “ … – Und wir haben das auch erst vor zwei Tagen gepostet. Es ist auch nichts entschieden. Geben wir dem etwas Zeit.

Es ist uns nicht entgangen, dass must gut zu try , aber das ist nicht dasselbe, als würde es Teil der Sprache werden. Wir haben erst begonnen, diesen Raum mit einer größeren Gruppe von Menschen zu erkunden. Wir wissen wirklich noch nicht, was dafür oder dagegen sprechen könnte. Danke.

Nachdem ich Stunden damit verbracht hatte, alle Kommentare und das detaillierte Designdokument zu lesen, wollte ich diesem Vorschlag meine Ansichten hinzufügen.

Ich werde mein Bestes tun, um die Bitte von @ianlancetaylor zu respektieren, vorherige Punkte nicht nur zu wiederholen, sondern stattdessen neue Kommentare zur Diskussion hinzuzufügen. Ich glaube jedoch nicht, dass ich die neuen Kommentare machen kann, ohne mich auf frühere Kommentare zu beziehen.

Bedenken

Unglückliche Überladung von defer

Die Vorliebe, die offensichtliche und unkomplizierte Natur von defer als alarmierend zu überladen. Wenn ich defer closeFile(f) schreibe, ist das für mich direkt und offensichtlich, was passiert und warum; am Ende der Funktion, die aufgerufen wird. Und während die Verwendung defer für panic() und recover() weniger offensichtlich ist, verwende ich es selten, wenn überhaupt, und sehe es fast nie, wenn ich den Code anderer lese.

Spoo defer zu überladen, um auch Fehler zu behandeln, ist nicht offensichtlich und verwirrend. Warum das Schlüsselwort defer ? Bedeutet defer nicht _"Später erledigen"_ statt _"Vielleicht später?"_

Außerdem gibt es die Bedenken des Go-Teams bezüglich der Leistung von defer . Angesichts dessen erscheint es doppelt unglücklich, dass defer für den _„heißen Pfad“_-Codefluss in Betracht gezogen wird.

Keine Statistiken, die einen signifikanten Anwendungsfall bestätigen

Wie von @prologic erwähnt, basiert dieser try() -Vorschlag auf einem großen Prozentsatz von Code, der diesen Anwendungsfall verwenden würde, oder basiert er stattdessen auf dem Versuch, diejenigen zu besänftigen, die sich über die Go-Fehlerbehandlung beschwert haben?

Ich wünschte, ich wüsste, wie ich Ihnen Statistiken aus meiner Codebasis geben kann, ohne jede Datei erschöpfend zu überprüfen und Notizen zu machen. Ich weiß nicht, wie @prologic in der Lage war, froh zu sein, dass er es getan hat.

Aber anekdotisch wäre ich überrascht, wenn try() 5 % meiner Anwendungsfälle ansprechen würde, und ich würde vermuten, dass es weniger als 1 % ansprechen würde. Wissen Sie sicher, dass andere sehr unterschiedliche Ergebnisse haben? Haben Sie eine Teilmenge der Standardbibliothek genommen und versucht zu sehen, wie sie angewendet werden würde?

Denn ohne bekannte Statistiken, dass dies für eine große Menge Code in freier Wildbahn angemessen ist, muss ich fragen, ob diese neue komplizierte Änderung der Sprache, die von jedem verlangen wird, die neuen Konzepte zu lernen, wirklich eine überzeugende Anzahl von Anwendungsfällen anspricht?

Erleichtert Entwicklern das Ignorieren von Fehlern

Dies ist eine totale Wiederholung dessen, was andere kommentiert haben, aber was im Grunde try() liefert, ist in vielerlei Hinsicht analog dazu, das Folgende einfach als idomatischen Code zu umarmen, und dies ist Code, der niemals seinen Weg in irgendeinen Code finden wird - Respektieren von Entwicklerschiffen:

f, _ := os.Open(filename)

Ich weiß, dass ich in meinem eigenen Code besser sein kann, aber ich weiß auch, dass viele von uns auf die Größe anderer Go-Entwickler angewiesen sind, die einige enorm nützliche Pakete veröffentlichen, aber nach dem, was ich in _"Other People's Code(tm)"_ gesehen habe Best Practices bei der Fehlerbehandlung werden oft ignoriert.

Also im Ernst, wollen wir es Entwicklern wirklich erleichtern, Fehler zu ignorieren und ihnen erlauben, GitHub mit nicht robusten Paketen zu verschmutzen?

Kann (meistens) bereits try() im Userland implementieren

Sofern ich den Vorschlag nicht falsch verstehe – was ich wahrscheinlich tue – ist hier try() im Go Playground implementiert in userland , allerdings mit nur einem (1) Rückgabewert und Rückgabe einer Schnittstelle anstelle des erwarteten Typs:

package main

import (
    "errors"
    "fmt"
    "strings"
)
func main() {
    defer func() {
        r := recover()
        if r != nil && strings.HasPrefix(r.(string),"TRY:") {
            fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
        }
    }()
    n := try(badjuju()).(int)
    fmt.Printf("Just chillin %dx!",n)   
}
func badjuju() (int,error) {
    return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0]
}

Der Benutzer könnte also try2() , try3() usw. hinzufügen, je nachdem, wie viele Rückgabewerte er zurückgeben muss.

Aber Go würde nur eine (1) einfache _ und doch universelle _ Sprachfunktion benötigen, um Benutzern, die try() wollen, zu ermöglichen, ihre eigene Unterstützung zu rollen, wenn auch eine, die immer noch eine explizite Typenzusicherung erfordert. Fügen Sie eine _(vollständig abwärtskompatibel)_-Fähigkeit für ein Go func hinzu, um eine unterschiedliche Anzahl von Rückgabewerten zurückzugeben, z.

func try(args ...interface{}) ...interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0:len(args)-2]
}

Und wenn Sie sich zuerst mit Generika befassen, wären die Typzusicherungen nicht einmal erforderlich _(obwohl ich denke, dass die Anwendungsfälle für Generika reduziert werden sollten, indem Sie integrierte Funktionen hinzufügen, um die Anwendungsfälle von Generika zu adressieren, anstatt den verwirrenden Semantik- und Syntaxsalat von Generika hinzuzufügen von Java et al.)_

Mangelnde Offensichtlichkeit

Beim Studium des Codes des Vorschlags finde ich, dass das Verhalten nicht offensichtlich und etwas schwer zu begründen ist.

Wenn ich sehe, dass try() einen Ausdruck umschließt, was passiert, wenn ein Fehler zurückgegeben wird?

Wird der Fehler einfach ignoriert? Oder springt es zum ersten oder letzten defer , und wenn ja, setzt es automatisch eine Variable namens err innerhalb des Abschlusses, oder übergibt es als Parameter _(I sehe keinen Parameter?)_. Und wenn kein automatischer Fehlername, wie benenne ich ihn? Und bedeutet das, dass ich meine eigene Variable err in meiner Funktion nicht deklarieren kann, um Konflikte zu vermeiden?

Und wird es alle defer s anrufen? In umgekehrter Reihenfolge oder in normaler Reihenfolge?

Oder wird es sowohl von der Schließung als auch von func zurückkehren, wo der Fehler zurückgegeben wurde? _(Etwas, an das ich nie gedacht hätte, wenn ich hier nicht Worte gelesen hätte, die das implizieren.)_

Nachdem ich den Vorschlag und alle bisherigen Kommentare gelesen habe, kenne ich die Antworten auf die obigen Fragen ehrlich gesagt immer noch nicht. Ist das die Art von Feature, die wir einer Sprache hinzufügen möchten, deren Befürworter sich dafür einsetzen, _"Captain Obvious"_ zu sein?

Mangelnde Kontrolle

Bei Verwendung defer scheint es, als wäre die einzige Kontrolle, die Entwicklern gewährt würde, die Verzweigung zu _(dem neuesten?)_ defer . Aber meiner Erfahrung nach ist es mit allen Methoden, die über ein triviales func hinausgehen, normalerweise komplizierter.

Oft habe ich es als hilfreich empfunden, Aspekte der Fehlerbehandlung innerhalb eines func – oder sogar über package hinweg – zu teilen, aber dann auch eine spezifischere Behandlung zu haben, die von einem oder mehreren anderen Paketen geteilt wird.

Zum Beispiel kann ich fünf (5) func Aufrufe aufrufen, die ein error() aus einem anderen func zurückgeben; nennen wir sie A() , B() , C() , D() und E() . Möglicherweise brauche ich C() , um eine eigene Fehlerbehandlung zu haben, A() , B() , D() und E() , um einige Fehlerbehandlungen zu teilen. und B() und E() , um eine spezifische Behandlung zu haben.

Aber ich glaube nicht, dass dies mit diesem Vorschlag möglich wäre. Zumindest nicht leicht.

Ironischerweise verfügt Go jedoch bereits über Sprachfunktionen, die ein hohes Maß an Flexibilität ermöglichen, die nicht auf eine kleine Menge von Anwendungsfällen beschränkt sein muss; func s und Schließungen. Daher meine rhetorische Frage:

_ "Warum können wir nicht einfach geringfügige Verbesserungen zur bestehenden Sprache hinzufügen, um diese Anwendungsfälle zu adressieren, und müssen keine neuen integrierten Funktionen hinzufügen oder verwirrende Semantik akzeptieren?" _

Es handelt sich um eine rhetorische Frage, da ich beabsichtige, als Alternative einen Vorschlag vorzulegen, den ich mir während des Studiums dieses Vorschlags und unter Berücksichtigung aller seiner Nachteile ausgedacht habe.

Aber ich schweife ab, das kommt später, und in diesem Kommentar geht es darum, warum der aktuelle Vorschlag überdacht werden muss.

Fehlende angegebene Unterstützung für break

Dies mag sich anfühlen, als käme es aus dem linken Feld, da die meisten Leute frühe Rückgaben für die Fehlerbehandlung verwenden, aber ich habe festgestellt, dass es vorzuziehen ist, break für die Fehlerbehandlung zu verwenden, die den größten Teil oder die gesamte Funktion vor return umschließt

Ich habe diesen Ansatz eine Zeit lang verwendet, und seine Vorteile allein durch die Erleichterung des Refactorings machen ihn dem frühen return vorzuziehen, aber er hat mehrere andere Vorteile, darunter einen einzigen Austrittspunkt und die Möglichkeit, einen Abschnitt einer Funktion vorzeitig zu beenden, aber immer noch zu sein in der Lage, eine Bereinigung auszuführen _(was wahrscheinlich der Grund ist, warum ich defer so selten verwende, was ich in Bezug auf den Programmablauf schwerer zu begründen finde.)_

Um break anstelle einer vorzeitigen Rückkehr zu verwenden, verwenden Sie eine for range "1" {...} -Schleife, um einen Block für die Unterbrechung zum Verlassen von _ zu erstellen (ich erstelle tatsächlich ein Paket namens only , das nur eine Konstante enthält namens Once mit einem Wert von "1" ):_

func (me *Config) WriteFile() (err error) {
    for range only.Once {
        var j []byte
        j, err = json.MarshalIndent(me, "", "    ")
        if err != nil {
            err = fmt.Errorf("unable to marshal config; %s", 
                err.Error(),
            )
            break
        }
        err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to make directory'%s'; %s", 
                me.GetDir(), 
                err.Error(),
            )
            break
        }
        err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to write to config file '%s'; %s", 
                me.GetFilepath(), 
                err.Error(),
            )
            break
        }
    }
    return err
}

Ich habe vor, in naher Zukunft ausführlich über das Muster zu bloggen und die verschiedenen Gründe zu erörtern, warum ich festgestellt habe, dass es besser funktioniert als frühe Rückgaben.

Aber ich schweife ab. Mein Grund, warum ich es hier aufführe, ist, dass ich für Go eine Fehlerbehandlung implementieren müsste, die frühe return s annimmt und die Verwendung break für die Fehlerbehandlung ignoriert

Meine Meinung err == nil ist problematisch

Als weiteren Exkurs möchte ich meine Bedenken bezüglich der idiomatischen Fehlerbehandlung in Go ansprechen. Obwohl ich ein großer Anhänger der Go-Philosophie bin, Fehler zu behandeln, wenn sie auftreten, im Gegensatz zur Ausnahmebehandlung, halte ich die Verwendung von nil , um anzuzeigen, dass kein Fehler vorliegt, für problematisch, da ich häufig feststellen möchte, dass ich eine Erfolgsmeldung zurückgeben möchte eine Routine – zur Verwendung in API-Antworten – und geben nicht nur dann einen Nicht-Null-Wert zurück, wenn ein Fehler auftritt.

Für Go 2 würde ich wirklich gerne sehen, dass Go einen neuen eingebauten Typ status und drei eingebaute Funktionen iserror() , iswarning() , issuccess() hinzufügt. status könnte error implementieren – was viel Abwärtskompatibilität ermöglicht und ein nil -Wert, der an issuccess() übergeben wird, würde true zurückgeben – aber status hätte einen zusätzlichen internen Status für den Fehlerlevel, so dass das Testen des Fehlerlevels immer mit einer der eingebauten Funktionen durchgeführt würde und idealerweise nie mit einem nil -Check. Das würde stattdessen so etwas wie den folgenden Ansatz ermöglichen:

func (me *Config) WriteFile() (sts status) {
    for range only.Once {
        var j []byte
        j, sts = json.MarshalIndent(me, "", "    ")
        if iserror(sts) {
            sts.AddMessage("unable to marshal config")
            break
        }
        sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to make directory'%s'", me.GetDir())
            break
        }
        sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to write to config file '%s'", 
                me.GetFilepath(), 
            )
            break
        }
        sts = fmt.Status("config file written")
    }
    return sts
}

Ich verwende bereits einen Userland-Ansatz in einem Pre-Beta-Level-Paket, das derzeit intern verwendet wird und ähnlich dem oben genannten für die Fehlerbehandlung ist. Ehrlich gesagt verbringe ich viel weniger Zeit damit, darüber nachzudenken, wie Code strukturiert werden soll, wenn ich diesen Ansatz verwende, als wenn ich versuchte, der idiomatischen Go-Fehlerbehandlung zu folgen.

Wenn Sie der Meinung sind, dass es möglich ist, idiomatischen Go-Code für diesen Ansatz weiterzuentwickeln, berücksichtigen Sie dies bitte bei der Implementierung der Fehlerbehandlung, einschließlich bei der Erwägung dieses try() -Vorschlags.

_"Nicht für alle"_ Begründung

Eine der wichtigsten Antworten des Go-Teams war _„Auch dieser Vorschlag versucht nicht, alle Fehlerbehandlungssituationen zu lösen.“_
Und das ist wahrscheinlich die beunruhigendste Sorge aus Governance-Perspektive.

Behandelt diese neue erschwerende Änderung der Sprache, die von jedem verlangen wird, die neuen Konzepte zu lernen, wirklich eine überzeugende Anzahl von Anwendungsfällen?

Und ist das nicht die gleiche Begründung, mit der Mitglieder des Kernteams zahlreiche Feature-Anfragen aus der Community abgelehnt haben? Das Folgende ist ein direktes Zitat aus einem Kommentar eines Mitglieds des Go-Teams in einer archetypischen Antwort auf eine Funktionsanfrage, die vor etwa 2 Jahren eingereicht wurde _(Ich nenne die Person oder die spezifische Funktionsanfrage nicht, da diese Diskussion nicht möglich sein sollte die Menschen, sondern über die Sprache):_

_"Ein neues Sprachfeature braucht überzeugende Anwendungsfälle. Alle Sprachfeatures sind nützlich, sonst würde sie niemand vorschlagen; die Frage ist: Sind sie nützlich genug, um die Komplexität der Sprache zu rechtfertigen und von jedem zu verlangen, die neuen Konzepte zu lernen? Was ist der überzeugende Nutzen Fälle hier? Wie werden die Menschen diese nutzen? Würden die Menschen zum Beispiel erwarten, in der Lage zu sein, ... und wenn ja, wie würden sie das tun? Bringt dieser Vorschlag mehr als nur die Möglichkeit, ...?"_
— Ein Kernmitglied des Go-Teams

Ehrlich gesagt, als ich diese Antworten gesehen habe, habe ich eines von zwei Gefühlen gespürt:

  1. Empörung, wenn es ein Merkmal ist, dem ich zustimme, oder
  2. Elation, wenn es sich um eine Funktion handelt, mit der ich nicht einverstanden bin.

Aber in jedem Fall waren/sind meine Gefühle irrelevant; Ich verstehe und stimme zu, dass ein Teil des Grundes, warum Go die Sprache ist, in der sich so viele von uns entwickeln, in diesem eifersüchtigen Schutz der Reinheit der Sprache liegt.

Und deshalb beunruhigt mich dieser Vorschlag so, weil das Go-Kernteam diesen Vorschlag auf der gleichen Ebene zu bearbeiten scheint wie jemand, der dogmatisch ein esoterisches Feature will, das die Go-Community auf keinen Fall jemals tolerieren wird.

_(Und ich hoffe wirklich, dass das Team den Boten nicht erschießt und dies als konstruktive Kritik von jemandem auffasst, der sehen möchte, dass Go weiterhin das Beste ist, was es für uns alle sein kann, da ich von ihnen als "Persona non grata" betrachtet werden müsste das Kernteam.)_

Wenn das Erfordernis einer überzeugenden Reihe realer Anwendungsfälle die Messlatte für alle von der Community erstellten Feature-Vorschläge ist, sollte es nicht auch die gleiche Messlatte für _ alle _ Feature-Vorschläge sein?

Verschachtelung von try()

Auch dies wurde von einigen behandelt, aber ich möchte einen Vergleich zwischen try() und der fortgesetzten Forderung nach ternären Operatoren ziehen. Zitat aus den Kommentaren eines anderen Go-Teammitglieds vor etwa 18 Monaten:

_"Beim "Programmieren im großen Stil" (große Codebasen mit großen Teams über lange Zeiträume) wird der Code viel öfter gelesen als geschrieben, also optimieren wir die Lesbarkeit, nicht die Beschreibbarkeit."_

Einer der _hauptsächlichen_ Gründe dafür , ternäre Operatoren nicht hinzuzufügen, ist, dass sie schwer zu lesen und/oder leicht falsch zu lesen sind, wenn sie verschachtelt sind. Das Gleiche kann jedoch auch für verschachtelte try() -Anweisungen wie try(try(try(to()).parse().this)).easily()) gelten.

Weitere Argumente gegen ternäre Operatoren waren, dass sie _„Ausdrücke“_ sind, mit dem Argument, dass verschachtelte Ausdrücke die Komplexität erhöhen können. Aber erzeugt try() nicht auch einen verschachtelbaren Ausdruck?

Nun hat jemand hier gesagt _"Ich denke, Beispiele wie [verschachtelte try() s] sind unrealistisch"_ und diese Aussage wurde nicht in Frage gestellt.

Aber wenn die Leute als Postulat akzeptieren, dass Entwickler try() nicht verschachteln, warum wird dann ternären Operatoren nicht die gleiche Ehrerbietung entgegengebracht, wenn Leute sagen _"Ich denke, tief verschachtelte ternäre Operatoren sind unrealistisch?"_

Fazit für diesen Punkt, ich denke, wenn das Argument gegen ternäre Operatoren wirklich gültig ist, dann sollten sie auch als gültige Argumente gegen diesen try() -Vorschlag betrachtet werden.

Zusammenfassend

Zum Zeitpunkt des Verfassens dieses Artikels stimmen 58% nach unten und 42% nach oben. Ich denke, dies allein sollte ausreichen, um darauf hinzuweisen, dass dieser Vorschlag so spaltend ist, dass es an der Zeit ist, zu diesem Thema zum Reißbrett zurückzukehren.

fwiw

PS Um es augenzwinkernder auszudrücken, ich denke, wir sollten der umschriebenen Weisheit von Yoda folgen:

_"Es gibt kein try() . Nur do() ."_

@ianlancetaylor

@beoran Vielleicht könnten Sie die Verbindung zwischen Generika und Fehlerbehandlung erweitern.

Ich spreche nicht für @beoran , aber in meinem Kommentar von vor ein paar Minuten werden Sie sehen, dass wir unsere eigenen try() bauen könnten, wenn wir Generika _(plus variadische Rückgabeparameter)_ hätten.

Allerdings – und ich werde wiederholen, was ich oben über Generika gesagt habe, hier, wo es einfacher zu sehen sein wird:

_" Ich denke, die Anwendungsfälle für Generika sollten reduziert werden, indem eingebaute Funktionen hinzugefügt werden, um die Anwendungsfälle von Generika zu adressieren, anstatt den verwirrenden Semantik- und Syntaxsalat von Generika von Java et al. hinzuzufügen.)"_

@ianlancetaylor

Bei dem Versuch, eine Antwort auf Ihre Frage zu formulieren, habe ich versucht, die try -Funktion in Go so zu implementieren, wie sie ist, und zu meiner Freude ist es tatsächlich bereits möglich, etwas ganz Ähnliches zu emulieren:

func try(v interface{}, err error) interface{} {
   if err != nil { 
     panic(err)
   }
   return v
}

Sehen Sie hier, wie es verwendet werden kann: https://play.golang.org/p/Kq9Q0hZHlXL

Die Nachteile dieses Ansatzes sind:

  1. Eine verzögerte Rettung ist erforderlich, aber mit try wie in diesem Vorschlag ist auch ein verzögerter Handler erforderlich, wenn wir eine ordnungsgemäße Fehlerbehandlung durchführen möchten. Also ich finde das ist kein gravierender Nachteil. Es könnte sogar noch besser sein, wenn Go eine Art von super(arg1, ..., argn) eingebaut hätte, die bewirkt, dass der Aufrufer des Aufrufers, eine Ebene höher in der Aufrufliste, mit den angegebenen Argumenten arg1,...argn zurückkehrt, eine Art Super-Rückgabe wenn du willst.
  2. Dieses von mir implementierte try kann nur mit einer Funktion arbeiten, die ein einzelnes Ergebnis und einen Fehler zurückgibt.
  3. Sie müssen die zurückgegebenen leeren Schnittstellenergebnisse eingeben.

Generika mit ausreichender Leistung könnten Problem 2 und 3 lösen, sodass nur 1 übrigbleibt, das durch Hinzufügen eines super() gelöst werden könnte. Mit diesen beiden Funktionen könnten wir so etwas bekommen wie:

func (T ... interface{})try(T, err error) super {
   if err != nil { 
      super(err)
   }
  super(T...)
}

Und dann wäre die verzögerte Rettung nicht mehr nötig. Dieser Vorteil wäre auch dann verfügbar, wenn Go keine Generika hinzugefügt werden.

Tatsächlich ist diese Idee eines eingebauten super() so mächtig und interessant, dass ich einen separaten Vorschlag dafür posten könnte.

@beoran Gut zu sehen, dass wir bei der Implementierung try() im Userland unabhängig voneinander auf genau die gleichen Einschränkungen gestoßen sind, mit Ausnahme des Super-Teils, den ich nicht aufgenommen habe, weil ich in einem alternativen Vorschlag über etwas Ähnliches sprechen wollte. :-)

Ich mag den Vorschlag, aber die Tatsache, dass Sie explizit angeben mussten, dass defer try(...) und go try(...) nicht erlaubt sind, ließ mich denken, dass etwas nicht ganz richtig war .... Orthogonalität ist ein guter Designleitfaden. Beim weiteren Lesen und Sehen von Dingen wie
x = try(foo(...)) y = try(bar(...))
Ich frage mich, ob vielleicht try ein Kontext sein muss! Erwägen:
try ( x = foo(...) y = bar(...) )
Hier geben foo() und bar() zwei Werte zurück, von denen der zweite error ist. Try-Semantik spielt nur für Aufrufe innerhalb des try -Blocks eine Rolle, bei denen der zurückgegebene Fehlerwert eliminiert (kein Empfänger) und nicht ignoriert wird (Empfänger ist _ ). Sie können sogar einige Fehler zwischen foo - und bar -Aufrufen behandeln.

Zusammenfassung:
a) Das Problem, try für go und defer nicht zuzulassen, verschwindet aufgrund der Syntax.
b) Fehlerbehandlung mehrerer Funktionen kann ausgeklammert werden.
c) seine magische Natur lässt sich besser als spezielle Syntax als als Funktionsaufruf ausdrücken.

Wenn try ein Kontext ist, dann haben wir gerade try/catch-Blöcke erstellt, die wir ausdrücklich zu vermeiden versuchen (und das aus gutem Grund).

Es gibt keinen Haken. Es würde genau derselbe Code generiert werden wie beim aktuellen Vorschlag
x = try(foo(...)) y = try(bar(...))
Dies ist nur eine andere Syntax, keine Semantik.
````

Ich denke, ich hatte ein paar Annahmen darüber getroffen, die ich nicht hätte tun sollen, obwohl es immer noch ein paar Nachteile gibt.

Was ist, wenn foo oder bar keinen Fehler zurückgeben, können sie auch in den try-Kontext gestellt werden? Wenn nicht, scheint es ziemlich hässlich zu sein, zwischen Fehler- und Nicht-Fehler-Funktionen zu wechseln, und wenn dies möglich ist, greifen wir auf die Probleme von Try-Blöcken in älteren Sprachen zurück.

Die zweite Sache ist, dass die keyword ( ... ) -Syntax normalerweise bedeutet, dass Sie das Schlüsselwort jeder Zeile voranstellen. Also für import, var, const, etc: jede Zeile beginnt mit dem Schlüsselwort. Eine Ausnahme von dieser Regel zu machen, scheint keine gute Entscheidung zu sein

Anstatt eine Funktion zu verwenden, wäre es einfach idiomatischer, eine spezielle Kennung zu verwenden?

Wir haben bereits den leeren Bezeichner _ , der Werte ignoriert.
Wir könnten so etwas wie # haben, das nur in Funktionen verwendet werden kann, deren letzter zurückgegebener Wert vom Typ error ist.

func foo() (error) {
    f, # := os.Open()
    defer f.Close()
    _, # = f.WriteString("foo")
    return nil
}

Wenn # ein Fehler zugewiesen wird, kehrt die Funktion sofort mit dem empfangenen Fehler zurück. Die Werte der anderen Variablen wären:

  • wenn sie nicht als Nullwert bezeichnet werden
  • andernfalls der den benannten Variablen zugewiesene Wert

@deanveloper , die Blocksemantik try spielt nur für Funktionen eine Rolle, die einen Fehlerwert zurückgeben und bei denen der Fehlerwert nicht zugewiesen wird. So könnte das letzte Beispiel des vorliegenden Vorschlags auch geschrieben werden als
try(x = foo(...)) try(y = bar(...))
Das Einfügen beider Anweisungen in denselben Block ähnelt dem, was wir für wiederholte import -, const - und var -Anweisungen tun.

Wenn Sie nun z
try( x = foo(...)) go zee(...) defer fum() y = bar(...) )
Das ist gleichbedeutend mit dem Schreiben
try(x = foo(...)) go zee(...) defer fum() try(y = bar(...))
Wenn Sie all dies in einem Versuchsblock ausklammern, wird es weniger beschäftigt.

Erwägen
try(x = foo())
Wenn foo() keinen Fehlerwert zurückgibt, ist dies äquivalent zu
x = foo()

Erwägen
try(f, _ := os.open(filename))
Da der zurückgegebene Fehlerwert ignoriert wird, ist dies gleichbedeutend mit just
f, _ := os.open(filename)

Erwägen
try(f, err := os.open(filename))
Da der zurückgegebene Fehlerwert nicht ignoriert wird, ist dies äquivalent zu
f, err := os.open(filename) if err != nil { return ..., err }
Wie derzeit im Vorschlag angegeben.

Und es entrümpelt auch schön verschachtelte Versuche!

Hier ist ein Link zu dem alternativen Vorschlag, den ich oben erwähnt habe:

Es fordert das Hinzufügen von zwei (2) kleinen, aber universellen Sprachfunktionen, um dieselben Anwendungsfälle wie try() zu adressieren

  1. Möglichkeit zum Aufrufen eines func /closure in einer Zuweisungsanweisung.
  2. Fähigkeit, break , continue oder return mehr als eine Ebene zu erreichen.

Mit diesen beiden Funktionen wäre es keine _"Magie"_ und ich glaube, ihre Verwendung würde Go-Code erzeugen, der leichter zu verstehen ist und mehr mit dem idiomatischen Go-Code übereinstimmt, mit dem wir alle vertraut sind.

Ich habe den Vorschlag gelesen und mag wirklich, wohin der Versuch geht.

In Anbetracht dessen, wie weit verbreitet try sein wird, frage ich mich, ob es einfacher wäre, es zu einem standardmäßigeren Verhalten zu machen.

Betrachten Sie Karten. Dies gilt:

v := m[key]

wie ist das:

v, ok := m[key]

Was ist, wenn wir Fehler genau so behandeln, wie try es vorschlägt, aber die eingebaute entfernen. Also wenn wir anfangen mit:

v, err := fn()

Anstatt zu schreiben:

v := try(fn())

Wir könnten stattdessen schreiben:

v := fn()

Wenn der err-Wert nicht erfasst wird, wird er genauso behandelt wie try. Es wäre etwas gewöhnungsbedürftig, aber es fühlt sich sehr ähnlich an wie v, ok := m[key] und v, ok := x.(string) . Grundsätzlich führt jeder unbehandelte Fehler dazu, dass die Funktion zurückkehrt und der Fehlerwert gesetzt wird.

Um zu den Schlussfolgerungen und Implementierungsanforderungen der Designdokumente zurückzukehren:

• Die Sprachsyntax wird beibehalten und es werden keine neuen Schlüsselwörter eingeführt
• Es ist weiterhin syntaktischer Zucker wie try und hoffentlich einfach zu erklären.
• Erfordert keine neue Syntax
• Es sollte vollständig abwärtskompatibel sein.

Ich stelle mir vor, dass dies fast die gleichen Implementierungsanforderungen wie try hätte, da der Hauptunterschied eher darin besteht, dass der syntaktische Zucker nicht im eingebauten Zustand ausgelöst wird, jetzt ist es das Fehlen des err-Felds.

Wenn wir also das Beispiel CopyFile aus dem Vorschlag zusammen mit defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) verwenden, erhalten wir:

func CopyFile(src, dst string) (err error) {
        defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

        r := os.Open(src)
        defer r.Close()

        w := os.Create(dst)
        defer func() {
                err := w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        io.Copy(w, r)
        w.Close()
        return nil
}

@savaki Ich mag das und habe darüber nachgedacht, was nötig wäre, um Go dazu zu bringen, die Fehlerbehandlung umzudrehen, indem es standardmäßig immer Fehler behandelt und den Programmierer angeben lässt, wann dies nicht der Fall sein soll (indem er den Fehler in einer Variablen erfasst), aber völlig fehlt Bezeichner würde es schwierig machen, dem Code zu folgen, da man nicht alle Rückkehrpunkte sehen könnte. Möglicherweise könnte eine Konvention zum Benennen von Funktionen, die einen Fehler zurückgeben könnten, anders funktionieren (wie das Großschreiben öffentlicher Bezeichner). Wenn eine Funktion einen Fehler zurückgibt, muss sie immer mit beispielsweise ? enden. Dann könnte Go den Fehler immer implizit behandeln und ihn automatisch an die aufrufende Funktion zurückgeben, genau wie try. Dies macht es sehr ähnlich zu einigen Vorschlägen, die vorschlagen, einen ? Bezeichner anstelle von try zu verwenden, aber ein wichtiger Unterschied besteht darin, dass hier ? Teil des Namens der Funktion und kein zusätzlicher Bezeichner wäre. Tatsächlich würde eine Funktion, die error als letzten Rückgabewert zurückgibt, nicht einmal kompiliert werden, wenn nicht ? angehängt wäre. Natürlich ist ? willkürlich und könnte durch alles andere ersetzt werden, was die Absicht deutlicher macht. operation?() wäre gleichbedeutend mit try(someFunc()) , aber ? wäre Teil des Funktionsnamens und sein einziger Zweck wäre, anzuzeigen, dass die Funktion genau wie die Großschreibung einen Fehler zurückgeben kann der erste Buchstabe einer Variablen.

Dies ist oberflächlich betrachtet sehr ähnlich zu anderen Vorschlägen, die try durch ? ersetzen sollen, aber ein entscheidender Unterschied besteht darin, dass die Fehlerbehandlung implizit (automatisch) und stattdessen das Ignorieren (oder Umbrechen) von Fehlern explizit gemacht wird sowieso eine Art Best Practice. Das offensichtlichste Problem dabei ist natürlich, dass es nicht abwärtskompatibel ist, und ich bin mir sicher, dass es noch viele mehr gibt.

Abgesehen davon wäre ich sehr daran interessiert zu sehen, wie Go die Fehlerbehandlung zum standardmäßigen/impliziten Fall machen kann, indem es sie automatisiert und den Programmierer etwas zusätzlichen Code schreiben lässt, um die Behandlung zu ignorieren/zu überschreiben. Die Herausforderung besteht meines Erachtens darin, alle Rückkehrpunkte in diesem Fall offensichtlich zu machen, da Fehler ohne sie eher zu Ausnahmen werden, in dem Sinne, dass sie von überall her kommen können, da der Ablauf des Programms sie nicht offensichtlich machen würde. Man könnte sagen, dass Fehler implizit mit einem visuellen Indikator gemacht werden, als würde man try implementieren und errcheck zu einem Compiler-Fehler machen.

könnten wir so etwas wie C++-Ausnahmen mit Dekoratoren für alte Funktionen machen?

func some_old_test() (int, error){
    return 0, errors.New("err1")
}
func some_new_test() (int){
        if true {
             return 1
        }
    throw errors.New("err2")
}
func throw_res(int, e error) int {
    if e != nil {
        throw e
    }
    return int
}
func main() {
    fmt.Println("Hello, playground")
    try{
        i := throw_res(some_old_test())
        fmt.Println("i=", i + some_new_test())
    } catch(err io.Error) {
        return err
    } catch(err error) {
        fmt.Println("unknown err", err)
    }
}

@owais Ich dachte, die Semantik wäre genau die gleiche wie try, also müssten Sie zumindest den Fehlertyp deklarieren. Also wenn wir anfangen mit:

func foo() error {
  _, err := fn() 
  if err != nil {
    return err
  }

  return nil
} 

Wenn ich den Versuchsvorschlag verstehe, mache einfach Folgendes:

func foo() error {
  _  := fn() 
  return nil
} 

würde nicht kompilieren. Ein netter Vorteil ist, dass es der Kompilierung die Möglichkeit gibt, dem Benutzer mitzuteilen, was fehlt. Etwas mit dem Effekt, dass die Verwendung der impliziten Fehlerbehandlung erfordert, dass der Fehlerrückgabetyp benannt wird, err.

Das würde dann funktionieren:

func foo() (err error) {
  _  := fn() 
  return nil
} 

Warum behandeln Sie nicht einfach den Fall eines Fehlers, der keiner Variablen zugewiesen ist?

  • Benötigte Rückgaben mit Namen entfernen, der Compiler kann dies alles selbst tun.
  • ermöglicht das Hinzufügen von Kontext.
  • behandelt den allgemeinen Anwendungsfall.
  • abwärtskompatibel
  • interagiert nicht seltsam mit Verzögerungen, Schleifen oder Schaltern.

implizite Rückgabe für den Fall if err != nil, Compiler kann lokale Variablennamen für Rückgaben generieren, falls erforderlich, auf die der Programmierer nicht zugreifen kann.
persönlich mag ich diesen speziellen Fall aus Sicht der Code-Lesbarkeit nicht

f := os.Open("foo.txt")

Bevorzugen Sie eine explizite Rückkehr, folgt der Code mehr gelesen als geschrieben Mantra

f := os.Open("foo.txt") else return

interessanterweise könnten wir beide Formen akzeptieren und gofmt automatisch die else-Rückgabe hinzufügen lassen.

Hinzufügen von Kontext, auch lokale Benennung der Variablen. return wird explizit, weil wir Kontext hinzufügen möchten.

f := os.Open("foo.txt") else err {
  return errors.Wrap(err, "some context")
}

Hinzufügen von Kontext mit mehreren Rückgabewerten

f := os.Open("foo.txt") else err {
  return i, j, errors.Wrap(err, "some context")
}

Verschachtelte Funktionen erfordern, dass die äußeren Funktionen alle Ergebnisse in derselben Reihenfolge verarbeiten
abzüglich des letzten Fehlers.

bits := ioutil.ReadAll(os.Open("foo")) else err {
  // either error ends up here.
  return i, j, errors.Wrap(err, "some context")
}

Compiler verweigert die Kompilierung aufgrund eines fehlenden Fehlerrückgabewerts in der Funktion

func foo(s string) int {
   i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
   return i * 2
}

lässt sich problemlos kompilieren, da Fehler explizit ignoriert werden.

func foo(s string) int {
   i, _ := strconv.Atoi(s)
   return i * 2
} 

Compiler ist zufrieden. er ignoriert den Fehler wie bisher, da keine Zuweisung oder sonst ein Suffix auftritt.

func foo() error {
  return errors.New("whoops")
}

func bar() {
  foo()
}

Innerhalb einer Schleife können Sie Continue verwenden.

for _, s := range []string{"1","2","3","4","5","6"} {
  i := strconv.Atoi(s) else continue
}

Bearbeiten: ; durch else ersetzt

@savaki Ich glaube, ich habe Ihren ursprünglichen Kommentar verstanden, und ich mag die Idee, dass Go standardmäßig Fehler behandelt, aber ich glaube nicht, dass es ohne das Hinzufügen einiger zusätzlicher Syntaxänderungen möglich ist, und sobald wir das tun, wird es dem aktuellen Vorschlag auffallend ähnlich.

Der größte Nachteil Ihres Vorschlags besteht darin, dass nicht alle Punkte offengelegt werden, von denen eine Funktion zurückkehren kann, im Gegensatz zum aktuellen if err != nil {return err} oder der in diesem Vorschlag eingeführten try-Funktion. Auch wenn es unter der Haube genauso funktionieren würde, würde der Code optisch ganz anders aussehen. Beim Lesen von Code gäbe es keine Möglichkeit zu wissen, welche Funktionsaufrufe einen Fehler zurückgeben könnten. Das wäre meiner Meinung nach eine schlimmere Erfahrung als Ausnahmen.

Möglicherweise könnte die Fehlerbehandlung implizit gemacht werden, wenn der Compiler eine semantische Konvention für Funktionen erzwingt, die Fehler zurückgeben könnten. Als müssten sie mit einem bestimmten Satz oder Zeichen beginnen oder enden. Das würde alle Rückgabepunkte sehr offensichtlich machen, und ich denke, es wäre besser als die manuelle Fehlerbehandlung, aber ich bin mir nicht sicher, wie viel besser es ist, wenn man bedenkt, dass es bereits Flusenprüfungen gibt, die Last schreien, wenn sie einen Fehler entdecken, der ignoriert wird. Es wäre sehr interessant zu sehen, ob der Compiler erzwingen kann, dass Funktionen auf eine bestimmte Weise benannt werden, je nachdem, ob sie mögliche Fehler zurückgeben könnten.

Der Hauptnachteil dieses Ansatzes besteht darin, dass der Fehlerergebnisparameter benannt werden muss, was möglicherweise zu weniger hübschen APIs führt (siehe aber die FAQs zu diesem Thema). Wir glauben, dass wir uns daran gewöhnen werden, wenn sich dieser Stil etabliert hat.

Ich bin mir nicht sicher, ob so etwas schon einmal vorgeschlagen wurde, kann es hier oder im Vorschlag nicht finden. Haben Sie eine andere eingebaute Funktion in Betracht gezogen, die einen Zeiger auf den Fehlerrückgabewert der aktuellen Funktion zurückgibt?
z.B:

func example() error {
        var err *error = funcerror() // always return a non-nil pointer
        fmt.Print(*err) // always nil if the return parameters are not named and not in a defer

        defer func() {
                err := funcerror()
                fmt.Print(*err) // "x"
        }

        return errors.New("x")
}
func exampleNamed() (err error) {
        funcErr := funcerror()
        fmt.Print(*funcErr) // "nil"

        err = errors.New("x")
        fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called

        *funcErr = errors.New("y")
        fmt.Print(err) // "y", unfortunate side effect?

        defer func() {
                funcErr := funcerror()
                fmt.Print(*funcErr) // "z"
                fmt.Print(err) // "z"
        }

        return errors.New("z")
}

Verwendung mit Versuch:

func CopyFile(src, dst string) (error) {
        defer func() {
                err := funcerror()
                if *err != nil {
                        *err = fmt.Errorf("copy %s %s: %v", src, dst, err)
                }
        }()
        // one liner alternative
        // defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)

        r := try(os.Open(src))
        defer r.Close()

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                err := funcerror()
                if *err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try(io.Copy(w, r))
        try(w.Close())
        return nil
}

Alternativ könnte funcerror (der Name ist in Arbeit :D ) nil zurückgeben, wenn er nicht innerhalb von defer aufgerufen wird.

Eine andere Alternative ist, dass funcerror eine "Errorer"-Schnittstelle zurückgibt, um sie schreibgeschützt zu machen:

type interface Errorer() {
        Error() error
}

@savaki Ich mag deinen Vorschlag, try() wegzulassen und es eher wie das Testen einer Karte oder einer Typenzusicherung zu machen. Das fühlt sich viel mehr _"Go-like"_ an.

Es gibt jedoch immer noch ein eklatantes Problem, das ich sehe, und das ist Ihr Vorschlag, der davon ausgeht, dass alle Fehler, die diesen Ansatz verwenden, ein return auslösen und die Funktion verlassen. Was nicht in Betracht gezogen wird, ist die Ausgabe von break aus den aktuellen for oder continue für die aktuellen for .

Frühe return s sind ein Vorschlaghammer, wenn oft ein Skalpell die bessere Wahl ist.

Ich behaupte also, dass break und continue gültige Strategien zur Fehlerbehandlung sein sollten, und derzeit setzt Ihr Vorschlag nur return $ voraus, während try() dies voraussetzt oder einen Fehler aufruft Handler, der selbst nur return kann, nicht break oder continue .

Sieht aus wie Savaki und ich hatte ähnliche Ideen, ich habe nur die Blocksemantik für den Umgang mit dem Fehler hinzugefügt, falls gewünscht. Zum Beispiel das Hinzufügen von Comtext, für Schleifen, in denen Sie kurzschließen möchten usw

@mikeschinkel siehe meine Erweiterung, er und ich hatten ähnliche Ideen, ich habe sie nur mit einer optionalen Blockanweisung erweitert

@james-lawrence

@mikeckinkel siehe meine Erweiterung, er und ich hatten ähnliche Ideen, ich habe sie nur mit einer optionalen Blockanweisung erweitert

Nimm dein Beispiel:

f := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}

Im Vergleich zu dem, was wir heute tun:

f,err := os.Open("foo.txt"); 
if err != nil {
  return errors.Wrap(err, "some context")
}

Ist mir definitiv vorzuziehen. Außer es hat ein paar Probleme:

  1. err scheint _"magisch"_ deklariert zu sein. Magie sollte minimiert werden, oder? Erklären wir es also:
f, err := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}
  1. Aber das funktioniert immer noch nicht, weil Go weder nil -Werte als false noch Zeigerwerte als true interpretiert, also müsste es so sein:
f, err := os.Open("foo.txt"); err != nil {
  return errors.Wrap(err, "some context")
}

Und was das funktioniert, es fühlt sich nach genauso viel Arbeit und viel Syntax in einer Zeile an, also könnte ich der Übersichtlichkeit halber weiterhin auf die alte Art und Weise vorgehen.

Aber was wäre, wenn Go zwei (2) Builtins hinzufügen würde; iserror() und error() ? Dann könnten wir das tun, was sich für mich nicht so schlimm anfühlt:

f := os.Open("foo.txt"); iserror() {
  return errors.Wrap(error(), "some context")
}

Oder besser _(etwas wie):_

f := os.Open("foo.txt"); iserror() {
  return error().Extend("some context")
}

Was denken Sie und andere?

Überprüfen Sie nebenbei die Schreibweise meines Benutzernamens. Ich wäre nicht über Ihre Erwähnung informiert worden, wenn ich nicht sowieso aufgepasst hätte ...

@mikeschinkel Entschuldigung für den Namen, den ich auf meinem Telefon hatte, und Github hat keine Autosuggestion gemacht.

err scheint "magisch" deklariert zu sein. Magie sollte minimiert werden, oder? Erklären wir es also:

meh, die ganze Idee, automatisch eine Rückgabe einzufügen, ist magisch. Das ist kaum das Magischste, was in diesem ganzen Vorschlag vor sich geht. Außerdem würde ich argumentieren, dass der Irrtum erklärt wurde; nur am Ende innerhalb des Kontexts eines bereichsbezogenen Blocks, um zu verhindern, dass er den übergeordneten Bereich verschmutzt, während weiterhin all die guten Dinge beibehalten werden, die wir normalerweise mit if-Anweisungen erhalten.

Ich bin im Allgemeinen ziemlich zufrieden mit der Fehlerbehandlung von go mit den bevorstehenden Ergänzungen des Fehlerpakets. Ich sehe nichts in diesem Vorschlag als super hilfreich. Ich versuche nur, die natürlichste Passform für den Golang anzubieten, wenn wir fest entschlossen sind, dies zu tun.

_"Die ganze Idee des automatischen Einfügens einer Rückgabe ist magisch."_

Da wirst du von mir keine Argumente bekommen.

_"Das ist kaum das Magischste, was in diesem ganzen Vorschlag vor sich geht."_

Ich glaube, ich habe versucht zu argumentieren, dass _"jede Magie problematisch ist."_

_"Außerdem würde ich argumentieren, dass der Fehler deklariert wurde; nur am Ende innerhalb des Kontexts eines Bereichsblocks ..."_

Wenn ich es also err2 nennen wollte, würde das auch funktionieren?

f := os.Open("foo.txt"); err2 {
  return errors.Wrap(err, "some context")
}

Ich gehe also davon aus, dass Sie auch eine Sonderfallbehandlung von err / err2 nach dem Semikolon vorschlagen, dh dass angenommen wird, dass es entweder nil ist oder nicht nil statt bool wie beim Überprüfen einer Karte?

if _,ok := m[a]; !ok {
   print("there is no 'a' in 'm'")
}

Ich bin im Allgemeinen ziemlich zufrieden mit der Fehlerbehandlung von go mit den bevorstehenden Ergänzungen des Fehlerpakets.

Auch ich bin mit der Fehlerbehandlung zufrieden, wenn sie mit break und continue _ kombiniert wird (aber nicht return .)_

So wie es ist, sehe ich diesen try() -Vorschlag eher als schädlich denn als hilfreich an und würde lieber nichts als diese Umsetzung als vorgeschlagen sehen. #jmtcw.

@beoran @mikeschinkel Ich habe vorhin angedeutet, dass wir diese Version von try nicht mit Generika implementieren könnten, weil sie den Kontrollfluss verändert. Wenn ich richtig lese, schlagen Sie beide vor, dass wir Generika verwenden könnten, um try zu implementieren, indem Sie panic aufrufen. Aber diese Version von try macht ausdrücklich nicht panic . Wir können also keine Generika verwenden, um diese Version von try zu implementieren.

Ja, wir könnten Generika verwenden (eine Version von Generika, die erheblich leistungsfähiger ist als die im Designentwurf unter https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md), um eine Funktion zu schreiben die bei Fehlern in Panik gerät. Aber bei Fehlern in Panik zu geraten, ist nicht die Art von Fehlerbehandlung, die Go-Programmierer heute schreiben, und es scheint mir keine gute Idee zu sein.

Die besondere Behandlung von @mikeschinkel wäre, dass der Block nur ausgeführt wird, wenn ein Fehler auftritt.
```
f := os.Open('foo'); err { return err } // err wäre hier immer nicht-nil.

@ianlancetaylor

_"Ja, wir könnten Generika verwenden ... Aber bei Fehlern in Panik zu geraten, ist nicht die Art der Fehlerbehandlung, die Go-Programmierer heute schreiben, und es scheint mir keine gute Idee zu sein."_

Ich stimme Ihnen diesbezüglich tatsächlich stark zu, daher scheint es, dass Sie die Absicht meines Kommentars falsch interpretiert haben. Ich habe keineswegs vorgeschlagen, dass das Go-Team eine Fehlerbehandlung implementiert, die panic() verwendet – natürlich nicht.

Stattdessen habe ich versucht, Ihren Hinweisen aus vielen Ihrer früheren Kommentare zu anderen Themen zu folgen und vorgeschlagen, dass wir keine Änderungen an Go vornehmen, die nicht absolut notwendig sind, da sie stattdessen im Userland möglich sind . Also _wenn_ Generika angesprochen würden _dann_ könnten Leute, die try() wollen, es tatsächlich selbst implementieren, wenn auch unter Nutzung von panic() . Und das wäre eine Funktion weniger, die das Team für Go hinzufügen und dokumentieren müsste.

Was ich nicht tat – und vielleicht war das nicht klar – war, dafür zu plädieren, dass die Leute tatsächlich panic() verwenden, um try() $ zu implementieren, nur dass sie es könnten, wenn sie es wirklich wollten, und sie die Funktionen von hätten Generika.

Klärt das auf?

Für mich ist das Aufrufen panic , wie auch immer das gemacht wird, ganz anders als dieser Vorschlag für try . Obwohl ich denke, dass ich verstehe, was Sie sagen, stimme ich nicht zu, dass sie gleichwertig sind. Selbst wenn wir Generika hätten, die stark genug wären, um eine Version von try zu implementieren, die in Panik gerät, würde meiner Meinung nach immer noch ein vernünftiger Wunsch nach der in diesem Vorschlag vorgestellten Version von try bestehen.

@ianlancetaylor Bestätigt. Auch hier habe ich nach einem Grund gesucht, warum try() nicht hinzugefügt werden muss, anstatt einen Weg zu finden, es hinzuzufügen. Wie ich oben sagte, hätte ich viel lieber nichts Neues für die Fehlerbehandlung als try() , wie hier vorgeschlagen.

Ich persönlich mochte den früheren check -Vorschlag mehr als diesen, basierend auf rein visuellen Aspekten; check hatte die gleiche Kraft wie dieses try() , aber bar(check foo()) ist für mich besser lesbar als bar(try(foo())) (ich brauchte nur eine Sekunde, um die Klammern zu zählen!).

Was noch wichtiger ist, mein größter Kritikpunkt an handle / check war, dass es nicht möglich war, einzelne Schecks auf unterschiedliche Weise zu verpacken – und jetzt hat dieser try() -Vorschlag den gleichen Fehler, beim Aufrufen kniffliger, selten verwendeter, für Neulinge verwirrender Funktionen von Verzögerungen und benannten Rückgaben. Und bei handle hatten wir zumindest die Möglichkeit, Scopes zu verwenden, um Handle-Blöcke zu definieren, bei defer ist selbst das nicht möglich.

Soweit es mich betrifft, verliert dieser Vorschlag in jeder Hinsicht gegenüber dem früheren handle / check Vorschlag.

Hier ist ein weiteres Problem bei der Verwendung von Verzögerungen für die Fehlerbehandlung.

try ist ein kontrolliertes/beabsichtigtes Verlassen einer Funktion. Verzögerungen werden immer ausgeführt, einschließlich unkontrollierter/unbeabsichtigter Beendigungen von Funktionen. Diese Diskrepanz könnte Verwirrung stiften. Hier ist ein imaginäres Szenario:

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

Denken Sie daran, dass sich net/http von einer Panik erholt, und stellen Sie sich vor, Sie müssten ein Produktionsproblem um die Panik herum debuggen. Sie würden sich Ihre Instrumentierung ansehen und einen Anstieg der DB-Aufruffehler aufgrund der recordMetric -Aufrufe feststellen. Dies könnte das wahre Problem verschleiern, nämlich die Panik in der folgenden Zeile.

Ich bin mir nicht sicher, wie ernst dies in der Praxis ist, aber es ist (leider) vielleicht ein weiterer Grund zu der Annahme, dass defer kein idealer Mechanismus für die Fehlerbehandlung ist.

Hier ist eine Änderung, die bei einigen der geäußerten Bedenken hilfreich sein kann: Behandle try wie goto statt wie return . Lass mich ausreden. :)

try wäre stattdessen syntaktischer Zucker für:

t1, … tn, te := f()  // t1, … tn, te are local (invisible) temporaries
if te != nil {
        err = te   // assign te to the error result parameter
        goto error // goto "error" label
}
x1, … xn = t1, … tn  // assignment only if there was no error

Leistungen:

  • defer ist nicht erforderlich, um Fehler zu dekorieren. (Named Returns sind jedoch weiterhin erforderlich.)
  • Das Vorhandensein des Labels error: ist ein visueller Hinweis darauf, dass sich irgendwo in der Funktion ein try befindet.

Dies bietet auch einen Mechanismus zum Hinzufügen von Handlern, der die Handler-als-Funktion-Probleme umgeht: Verwenden Sie Labels als Handler. try(fn(), wrap) wäre goto wrap statt goto error . Der Compiler kann bestätigen, dass wrap: in der Funktion vorhanden ist. Beachten Sie, dass Handler auch beim Debuggen hilfreich sind: Sie können den Handler hinzufügen/ändern, um einen Debugging-Pfad bereitzustellen.

Beispielcode:

func CopyFile(src, dst string) (err error) {
    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    try(io.Copy(w, r), copyfail)
    try(w.Close())
    return nil

error:
    return fmt.Errorf("copy %s %s: %v", src, dst, err)

copyfail:
    recordMetric("copy failure") // count incidents of this failure
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

Andere Kommentare:

  • Wir könnten verlangen, dass jedem Label, das als Ziel von try verwendet wird, eine abschließende Anweisung vorangestellt wird. In der Praxis würde dies sie zum Ende der Funktion zwingen und könnte einigen Spaghetti-Code verhindern. Andererseits könnte es einige sinnvolle, hilfreiche Verwendungen verhindern.
  • try könnte verwendet werden, um eine Schleife zu erstellen. Ich denke, das fällt unter das Motto "wenn es weh tut, tu es nicht", aber ich bin mir nicht sicher.
  • Dazu müsste https://github.com/golang/go/issues/26058 repariert werden.

Kredit: Ich glaube, eine Variante dieser Idee wurde erstmals von @griesemer persönlich auf der GopherCon im letzten Jahr vorgeschlagen.

@josharian Das Nachdenken über die Interaktion mit panic ist hier wichtig, und ich bin froh, dass Sie es angesprochen haben, aber Ihr Beispiel erscheint mir seltsam. Im folgenden Code ergibt es für mich keinen Sinn, dass die Zurückstellung immer eine "db call failed" -Metrik aufzeichnet. Es wäre eine falsche Metrik, wenn someHTTPHandlerGuts erfolgreich ist und nil zurückgibt. Das defer wird in allen Exit-Fällen ausgeführt, nicht nur in Fehler- oder Panikfällen, sodass der Code falsch zu sein scheint, selbst wenn keine Panik vorliegt.

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

@josharian Ja, das ist mehr oder weniger genau die Version, die wir letztes Jahr besprochen haben (außer dass wir check statt try verwendet haben). Ich denke, es wäre entscheidend, dass man nicht "zurück" in den Rest des Funktionskörpers springen könnte, sobald wir beim Label error sind. Das würde sicherstellen, dass das goto etwas "strukturiert" ist (kein Spaghetti-Code möglich). Eine Bedenken, die geäußert wurden, war, dass die Fehlerbehandlungsmarke (das error: )-Label immer am Ende der Funktion landen würde (andernfalls müsste man sie irgendwie umgehen). Ich persönlich mag den Code zur Fehlerbehandlung (am Ende), aber andere waren der Meinung, dass er gleich zu Beginn sichtbar sein sollte.

@mikeshenkel Ich sehe die Rückkehr aus einer Schleife eher als Plus denn als Negativ. Ich vermute, dass dies Entwickler ermutigen würde, entweder eine separate Funktion zu verwenden, um den Inhalt einer Schleife zu verarbeiten, oder ausdrücklich err zu verwenden, wie wir es derzeit tun. Beides scheint mir ein gutes Ergebnis zu sein.

Aus meiner Sicht habe ich nicht das Gefühl, dass diese Try-Syntax jeden Anwendungsfall behandeln muss, so wie ich nicht das Gefühl habe, dass ich die verwenden muss

V, ok:= m[Taste]

Formular aus dem Lesen von einer Karte

Sie könnten vermeiden, dass die goto-Labels Handler zum Ende der Funktion zwingen, indem Sie den Vorschlag handle / check in vereinfachter Form wiederbeleben. Was wäre, wenn wir die handle err { ... } -Syntax verwenden, aber keine Handler verketten lassen, sondern nur der letzte verwendet wird. Es vereinfacht diesen Vorschlag sehr und ist der Goto-Idee sehr ähnlich, außer dass es die Handhabung näher an den Verwendungsort bringt.

func CopyFile(src, dst string) (err error) {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “check” fails
        }
    }()

    {
        // handlers are scoped, after this scope the original handle is used again.
        // as an alternative, we could have repeated the first handle after the io.Copy,
        // or come up with a syntax to name the handlers, though that's often not useful.
        handle err {
            recordMetric("copy failure") // count incidents of this failure
            return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
        check io.Copy(w, r)
    }
    check w.Close()
    return nil
}

Als Bonus hat dies einen zukünftigen Weg, Handler verketten zu lassen, da alle bestehenden Verwendungen eine Rendite haben würden.

@josharian @griesemer Wenn Sie benannte Handler einführen (die viele Antworten zum Überprüfen/Handhaben angefordert haben, siehe wiederkehrende Themen ), gibt es Syntaxoptionen, die try(f(), err) vorzuziehen sind:

try.err f()
try?err f()
try#err f()

?err    f() // because 'try' is redundant
?return f() // no handler
?panic  f() // no handler

(?err f()).method()

f?err() // lead with function name, instead of error handling
f?err().method()

file, ?err := os.Open(...) // many check/handle responses also requested this style

Eines der Dinge, die ich an Go am meisten mag, ist, dass seine Syntax relativ frei von Satzzeichen ist und ohne größere Probleme laut vorgelesen werden kann. Ich würde es wirklich hassen, wenn Go als $#@!perl enden würde.

Für mich hat das "Ausprobieren" einer integrierten Funktion und das Aktivieren von Ketten zwei Probleme:

  • Es ist nicht konsistent mit dem Rest des Kontrollflusses in go (z. B. Schlüsselwörter for/if/return/etc).
  • Es macht Code weniger lesbar.

Ich würde es vorziehen, es eine Aussage ohne Klammern zu machen. Die Beispiele in dem Vorschlag würden mehrere Zeilen erfordern, würden aber lesbarer werden (dh einzelne "try"-Instanzen wären schwerer zu übersehen). Ja, es würde externe Parser beschädigen, aber ich ziehe es vor, die Konsistenz zu bewahren.

Der ternäre Operator ist ein weiterer Ort, an dem go nichts hat und mehr Tastenanschläge erfordert, aber gleichzeitig die Lesbarkeit / Wartbarkeit verbessert. Das Hinzufügen von "versuchen" in dieser eingeschränkteren Form wird Ausdrucksstärke und Lesbarkeit besser ausbalancieren, IMO.

FWIW, panic beeinflusst den Kontrollfluss und hat Klammern, aber go und defer beeinflussen auch den Fluss und nicht. Ich neige dazu zu denken, dass try defer insofern ähnlicher ist, als es eine ungewöhnliche Flussoperation ist und es gut ist, try (try os.Open(file)).Read(buf) schwieriger zu machen, weil wir Einzeiler entmutigen wollen egal, aber egal. Beides ist in Ordnung.

Vorschlag, den jeder für einen impliziten Namen für eine endgültige Fehlerrückgabevariable hassen wird: $err . Es ist besser als try() IMO. :-)

@griesemer

_"Persönlich mag ich den Code zur Fehlerbehandlung (am Ende)"_

+1 dazu!

Ich finde, dass die Fehlerbehandlung, die _bevor_ der Fehler auftritt, viel schwieriger zu begründen ist als die Fehlerbehandlung, die _nach_ dem Fehler auftritt. Wenn ich mental zurückspringen und mich zwingen muss, dem logischen Fluss zu folgen, fühlt es sich an, als wäre ich zurück im Jahr 1980, als ich Basic mit GOTOs schrieb.

Lassen Sie mich noch einen weiteren möglichen Weg zur Behandlung von Fehlern vorschlagen, wobei ich wieder CopyFile() als Beispiel verwende:

func CopyFile(src, dst string) (err error) {

    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    defer w.Close()

    io.Copy(w, r)
    w.Close()

    for err := error {
        switch err.Source() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

Die erforderlichen Sprachänderungen wären:

  1. Ein for error{} -Konstrukt zulassen, ähnlich wie for range{} , aber nur bei einem Fehler eingegeben und nur einmal ausgeführt.

  2. Erlauben Sie das Weglassen der Erfassung von Rückgabewerten, die <object>.Error() string implementieren, aber nur, wenn ein for error{} Konstrukt innerhalb desselben func existiert.

  3. Bewirken, dass die Programmsteuerung zur ersten Zeile des Konstrukts for error{} springt , wenn ein func einen _"Fehler"_ in seinem letzten Rückgabewert zurückgibt.

  4. Bei der Rückgabe eines _"Fehlers"_ würde Go eine Referenz auf die Funktion zuweisen, die den Fehler zurückgegeben hat, der durch <error>.Source() abrufbar sein sollte

Was ist ein _"Fehler"_?

Derzeit wird ein _"Fehler"_ als jedes Objekt definiert, das Error() string implementiert und natürlich nicht nil ist.

Es besteht jedoch häufig die Notwendigkeit, Fehler _sogar bei Erfolg_ zu erweitern, um die Rückgabe von Werten zu ermöglichen, die für Erfolgsergebnisse einer RESTful-API erforderlich sind. Daher würde ich das Go-Team bitten, nicht automatisch davon auszugehen, dass err!=nil _"Fehler"_ bedeutet, sondern stattdessen zu prüfen, ob ein Fehlerobjekt ein IsError() implementiert und ob IsError() true . nil ist, ein _"Fehler"_ ist.

_(Ich spreche nicht unbedingt von Code in der Standardbibliothek, sondern in erster Linie, wenn Sie Ihren Kontrollfluss so wählen, dass er bei einem err!=nil "Fehler"_ verzweigt in Bezug auf Rückgabewerte in unseren Funktionen tun können.)_

Übrigens, jedem zu ermöglichen, auf die gleiche Weise auf einen _"Fehler"_ zu testen, könnte wahrscheinlich am einfachsten durch Hinzufügen einer neuen eingebauten iserror() -Funktion erfolgen :

type ErrorIser interface {
    IsError() bool
}
func iserror(err error) bool {
    if err == nil { 
        return false
    }
    if _,ok := err.(ErrorIser); !ok {
        return true
    }
    return err.IsError()
}

Ein Nebennutzen des Zulassens der Nichterfassung von _"Fehlern"_

Beachten Sie, dass das Nicht-Erfassen des letzten _"Fehlers"_ von func -Aufrufen ein späteres Refactoring ermöglichen würde, um Fehler von func s zurückzugeben, die ursprünglich keine Fehler zurückgeben mussten. Und es würde dieses Refactoring ermöglichen , ohne bestehenden Code zu beschädigen , der diese Form der Fehlerbehebung verwendet und die besagten func s aufruft.

Für mich ist diese Entscheidung _"Soll ich einen Fehler zurückgeben oder auf die Fehlerbehandlung verzichten, um den Aufruf zu vereinfachen?"_ eines meiner größten Dilemmas beim Schreiben von Go-Code. Das Nicht-Erfassen von _"Fehlern"_ oben zuzulassen, würde dieses Dilemma so gut wie beseitigen.

Eigentlich habe ich vor etwa einem halben Jahr versucht, diese Idee als Go-Übersetzer umzusetzen. Ich bin mir nicht sicher, ob diese Funktion als integriertes Go hinzugefügt werden sollte, aber lassen Sie mich Ihre Erfahrung teilen (obwohl ich nicht sicher bin, ob sie nützlich ist).

https://github.com/rhysd/trygo

Ich habe die erweiterte Sprache TryGo genannt und den TryGo to Go-Übersetzer implementiert.

Mit dem Übersetzer, dem Code

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd := try(os.Getwd())

    try(os.Mkdir(filepath.Join(cwd, subdir)))

    p := filepath.Join(cwd, subdir, filename)
    f := try(os.Create(p))
    defer f.Close()

    try(f.Write(content))

    fmt.Println("Created:", p)
    return nil
}

übersetzen kann

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd, _err0 := os.Getwd()
    if _err0 != nil {
        return _err0
    }

    if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
        return _err1
    }

    p := filepath.Join(cwd, subdir, filename)
    f, _err2 := os.Create(p)
    if _err2 != nil {
        return _err2
    }
    defer f.Close()

    if _, _err3 := f.Write(content); _err3 != nil {
        return _err3
    }

    fmt.Println("Created:", p)
    return nil
}

Für die Einschränkung der Sprache konnte ich den generischen try() -Aufruf nicht implementieren. Es ist beschränkt auf

  • RHS der Definitionserklärung
  • RHS der Abtretungserklärung
  • Anruferklärung

aber ich könnte das mit meinem kleinen Projekt versuchen. Meine Erfahrung war

  • es funktioniert im Grunde gut und spart mehrere Zeilen
  • Der benannte Rückgabewert ist für err eigentlich unbrauchbar, da der Rückgabewert seiner Funktion sowohl von der Zuweisung als auch von der Sonderfunktion try() bestimmt wird. sehr verwirrend
  • dieser try() -Funktion fehlte die oben beschriebene Funktion „Umbruchfehler“.

_"Beides scheint mir ein gutes Ergebnis zu sein."_

Hier müssen wir uns einigen, nicht zuzustimmen.

_"diese Try-Syntax (muss nicht) jeden Anwendungsfall abdecken"_

Dieses Meme ist wahrscheinlich das beunruhigendste. Zumindest wenn man bedenkt, wie widerstandsfähig das Go-Team / die Go-Community gegenüber Änderungen in der Vergangenheit war, die nicht allgemein anwendbar sind.

Wenn wir diese Rechtfertigung hier zulassen, warum können wir dann nicht auf frühere Vorschläge zurückgreifen, die abgelehnt wurden, weil sie nicht allgemein anwendbar waren?

Und sind wir jetzt bereit, für Änderungen in Go zu argumentieren, die nur für ausgewählte Grenzfälle nützlich sind?

Meiner Meinung nach wird es langfristig keine guten Ergebnisse bringen, diesen Präzedenzfall zu schaffen ...

_"@mikeschenkel"_

PS Ich habe Ihre Nachricht wegen eines Rechtschreibfehlers zuerst nicht gesehen. _(das beleidigt mich nicht, ich werde nur nicht benachrichtigt, wenn mein Benutzername falsch geschrieben ist...)_

Ich schätze die Verpflichtung zur Abwärtskompatibilität, die Sie dazu motiviert, try zu einem eingebauten und nicht zu einem Schlüsselwort zu machen, aber nachdem Sie mit der völligen _Seltsamkeit_ gerungen haben, eine häufig verwendete Funktion zu haben, die den Kontrollfluss ändern kann ( panic und recover extrem selten sind), frage ich mich: Hat jemand eine groß angelegte Analyse der Häufigkeit von try als Kennung in Open-Source-Codebasen durchgeführt? Ich war neugierig und skeptisch, also habe ich eine vorläufige Suche nach Folgendem durchgeführt:

In den 11.108.770 signifikanten Go-Linien, die in diesen Depots leben, gab es nur 63 Instanzen von try , die als Kennung verwendet wurden. Natürlich ist mir klar, dass diese Codebasen (obwohl sie groß, weit verbreitet und an sich wichtig sind) nur einen Bruchteil des Go-Codes da draußen darstellen, und dass wir außerdem keine Möglichkeit haben, private Codebasen direkt zu analysieren, aber es ist sicher ein interessantes Ergebnis.

Da try außerdem wie jedes Schlüsselwort in Kleinbuchstaben geschrieben ist, werden Sie es niemals in der öffentlichen API eines Pakets finden. Schlüsselwortergänzungen wirken sich nur auf Paketinterna aus.

Dies ist alles ein Vorwort zu ein paar Ideen, die ich in die Mischung einbringen wollte, die von try als Schlüsselwort profitieren würden.

Ich würde folgende Konstruktionen vorschlagen.

1) Kein Handler

// The existing proposal, but as a keyword rather than builtin.  When an error is 
// "caught", the function returns all zero values plus the error.  Nothing 
// particularly new here.
func doSomething() (int, error) {
    try SomeFunc()
    a, b := try AnotherFunc()

    // ...

    return 123, nil
}

2) Handler

Beachten Sie, dass Fehlerbehandlungsroutinen einfache Codeblöcke sind, die eingebettet werden sollen, und keine Funktionen. Mehr dazu weiter unten.

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

Vorgeschlagene Beschränkungen:

  • Sie können nur try eine Funktion aufrufen. Nein try err .
  • Wenn Sie keinen Handler angeben, können Sie innerhalb einer Funktion, die einen Fehler als Rückgabewert ganz rechts zurückgibt, nur try ausgeben. Das Verhalten try ändert sich je nach Kontext nicht. Es gerät nie in Panik (wie viel früher im Thread besprochen).
  • Es gibt keinerlei "Handler-Kette". Handler sind nur inlineable Codeblöcke.

Leistungen:

  • Die Syntax try / else könnte trivial in das bestehende „compound if“ desugered werden:
    go a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "error in doSomething:") }
    wird
    go if a, b, err := SomeFunc(); err != nil { return 0, errors.Wrap(err, "error in doSomething:") }
    In meinen Augen erschienen zusammengesetzte ifs aus einem sehr einfachen Grund immer eher verwirrend als hilfreich: Bedingungen treten im Allgemeinen _nach_ einer Operation auf und haben etwas mit der Verarbeitung ihrer Ergebnisse zu tun. Wenn die Operation in die bedingte Anweisung eingeklemmt ist, ist es einfach weniger offensichtlich, dass sie stattfindet. Das Auge wird abgelenkt. Außerdem ist der Geltungsbereich der definierten Variablen nicht so offensichtlich, als wenn sie ganz links in einer Zeile stehen.
  • Error-Handler sind absichtlich nicht als Funktionen definiert (noch mit irgendetwas, das einer funktionsähnlichen Semantik ähnelt). Dies bewirkt mehrere Dinge für uns:

    • Der Compiler kann einen benannten Handler einfach überall dort einfügen, wo auf ihn verwiesen wird. Es ist viel mehr wie ein einfaches Makro/Codegen-Template als ein Funktionsaufruf. Die Laufzeit muss nicht einmal wissen, dass Handler existieren.

    • Wir sind nicht darauf beschränkt, was wir innerhalb eines Handlers tun können. Wir umgehen die Kritik von check / handle , dass „dieses Fehlerbehandlungs-Framework nur für Rettungsaktionen gut ist“. Wir umgehen auch die Kritik an der "Händlerkette". Jeder beliebige Code kann in einen dieser Handler eingefügt werden, und es wird kein anderer Kontrollfluss impliziert.

    • Wir müssen return im Handler nicht kapern, um super return zu bedeuten. Das Entführen eines Schlüsselworts ist äußerst verwirrend. return bedeutet einfach nur return , und es besteht keine wirkliche Notwendigkeit für super return .

    • defer muss nicht als Fehlerbehandlungsmechanismus dienen. Wir können es weiterhin hauptsächlich als eine Möglichkeit betrachten, Ressourcen zu bereinigen usw.

  • Zum Hinzufügen von Kontext zu Fehlern:

    • Das Hinzufügen von Kontext mit Handlern ist extrem einfach und sieht bestehenden if err != nil -Blöcken sehr ähnlich

    • Auch wenn das Konstrukt „try without handler“ nicht direkt dazu auffordert, Kontext hinzuzufügen, ist es sehr einfach, es in das Handler-Formular umzugestalten. Seine beabsichtigte Verwendung wäre hauptsächlich während der Entwicklung, und es wäre äußerst einfach, einen go vet -Check zu schreiben, um nicht behandelte Fehler hervorzuheben.

Entschuldigen Sie, wenn diese Ideen anderen Vorschlägen sehr ähnlich sind – ich habe versucht, mit ihnen allen Schritt zu halten, aber möglicherweise einen guten Deal verpasst.

@brynbellomy Danke für die Keyword-Analyse – das sind sehr hilfreiche Informationen. Es scheint, dass try als Schlüsselwort in Ordnung sein könnte. (Sie sagen, dass APIs nicht betroffen sind – das stimmt, aber try wird möglicherweise immer noch als Parametername oder ähnliches angezeigt – daher muss die Dokumentation möglicherweise geändert werden. Aber ich stimme zu, dass dies keine Auswirkungen auf Clients dieser Pakete hätte.)

Zu Ihrem Vorschlag: Es würde auch ohne benannte Handler gut gehen, oder? (Das würde den Vorschlag ohne Leistungsverlust vereinfachen. Man könnte einfach eine lokale Funktion aus dem eingebetteten Handler aufrufen.)

Zu Ihrem Vorschlag: Es würde auch ohne benannte Handler gut gehen, oder? (Das würde den Vorschlag ohne Leistungsverlust vereinfachen. Man könnte einfach eine lokale Funktion aus dem eingebetteten Handler aufrufen.)

@griesemer In der Tat – ich fühlte mich ziemlich lauwarm, diese einzubeziehen. Sicherlich mehr Go-ish ohne.

Auf der anderen Seite scheinen die Leute die Möglichkeit zu haben, eine Einzeiler-Fehlerbehandlung durchzuführen, einschließlich Einzeiler, die return . Ein typischer Fall wäre log, dann return . Wenn wir in der else -Klausel eine lokale Funktion berappen, verlieren wir das wahrscheinlich:

a, b := try SomeFunc() else err {
    someLocalFunc(err)
    return 0, err
}

(Ich bevorzuge dies jedoch immer noch gegenüber zusammengesetzten ifs)

Sie könnten jedoch immer noch einzeilige Rückgaben erhalten, die einen Fehlerkontext hinzufügen, indem Sie eine einfache gofmt -Optimierung implementieren, die weiter oben im Thread besprochen wurde:

a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

Ist das neue Schlüsselwort im obigen Vorschlag erforderlich? Warum nicht:

SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

@griesemer Wenn Handler wieder auf dem Tisch sind, schlage ich vor, dass Sie ein neues Thema zur Diskussion von try/handle oder try/_label_ erstellen. Dieser Vorschlag verzichtet ausdrücklich auf Handler, und es gibt unzählige Möglichkeiten, sie zu definieren und aufzurufen.

Jeder, der Handler vorschlägt, sollte zuerst das Check/Handle-Feedback-Wiki lesen. Die Chancen stehen gut, dass alles, was Sie sich erträumen, dort bereits beschrieben ist :-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

@smonkewitz nein, ein neues Schlüsselwort ist in dieser Version nicht erforderlich, da es an Zuweisungsanweisungen gebunden ist, die bisher in verschiedenen Syntaxzuckern mehrfach erwähnt wurden.

https://github.com/golang/go/issues/32437#issuecomment -499808741
https://github.com/golang/go/issues/32437#issuecomment -499852124
https://github.com/golang/go/issues/32437#issuecomment -500095505

@ianlancetaylor hat das Go-Team diese besondere Art der Fehlerbehandlung schon in Betracht gezogen? Es ist nicht so einfach zu implementieren wie der vorgeschlagene eingebaute Versuch, fühlt sich aber idiomatischer an. ~unnötige Aussage, sorry.~

Ich möchte etwas wiederholen, was @deanveloper und einige andere gesagt haben, aber mit meiner eigenen Betonung. In https://github.com/golang/go/issues/32437#issuecomment -498939499 sagte @deanveloper:

try ist eine bedingte Rückgabe. Sowohl Kontrollfluss als auch Rückgaben werden in Go auf Sockeln gehalten. Der gesamte Kontrollfluss innerhalb einer Funktion ist eingerückt und alle Rückgaben beginnen mit return . Das Mischen dieser beiden Konzepte zu einem leicht zu übersehenden Funktionsaufruf fühlt sich einfach ein bisschen daneben an.

Darüber hinaus ist try in diesem Vorschlag eine Funktion, die Werte zurückgibt, sodass sie als Teil eines größeren Ausdrucks verwendet werden kann.

Einige haben argumentiert, dass panic bereits den Präzedenzfall für eine eingebaute Funktion geschaffen hat, die den Kontrollfluss ändert, aber ich denke, dass panic aus zwei Gründen grundlegend anders ist:

  1. Panik ist nicht bedingt; es bricht immer die aufrufende Funktion ab.
  2. Panic gibt keine Werte zurück und kann daher nur als eigenständige Anweisung erscheinen, was seine Sichtbarkeit erhöht.

Versuchen Sie es dagegen:

  1. Bedingt ist; es kann von der aufrufenden Funktion zurückkehren oder nicht.
  2. Gibt Werte zurück und kann in einem zusammengesetzten Ausdruck erscheinen, möglicherweise mehrmals, in einer einzelnen Zeile, möglicherweise über den rechten Rand meines Editorfensters hinaus.

Aus diesen Gründen denke ich, dass sich try mehr als nur ein bisschen anfühlt, ich denke, es schadet der Lesbarkeit des Codes grundlegend.

Wenn wir heute zum ersten Mal auf einen Go-Code stoßen, können wir ihn schnell überfliegen, um die möglichen Austrittspunkte und Kontrollflusspunkte zu finden. Ich glaube, das ist eine sehr wertvolle Eigenschaft des Go-Codes. Mit try wird es zu einfach, Code zu schreiben, dem diese Eigenschaft fehlt.

Ich gebe zu, dass es wahrscheinlich ist, dass Go-Entwickler, die Wert auf Lesbarkeit des Codes legen, sich auf Verwendungsausdrücke für try konvergieren würden, die diese Fallstricke bei der Lesbarkeit vermeiden. Ich hoffe, dass dies passieren würde, da die Lesbarkeit des Codes für viele Go-Entwickler ein zentraler Wert zu sein scheint. Aber es ist für mich nicht offensichtlich, dass try bestehenden Code-Idiomen genügend Wert hinzufügt, um das Gewicht zu tragen, der Sprache ein neues Konzept hinzuzufügen, das jeder lernen kann, und das kann so leicht die Lesbarkeit beeinträchtigen.

````
wenn es != "pleite" {
nicht reparieren
}

@ChrisHines Zu Ihrem Punkt (der an anderer Stelle in diesem Thread wiederholt wird) fügen wir eine weitere Einschränkung hinzu:

  • jede try -Anweisung (auch solche ohne Handler) muss in einer eigenen Zeile stehen.

Sie würden immer noch von einer großen Reduzierung des visuellen Rauschens profitieren. Dann haben Sie garantierte Rückgaben, die mit return kommentiert sind, und bedingte Rückgaben, die mit try kommentiert sind, und diese Schlüsselwörter stehen immer am Anfang einer Zeile (oder im schlimmsten Fall direkt nach einer Variablenzuweisung).

Also nichts von diesem Unsinn:

try EmitEvent(try (try DecodeMsg(m)).SaveToDB())

sondern das:

dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)

was sich immer noch klarer anfühlt als das:

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Eine Sache, die ich an diesem Design mag, ist, dass es unmöglich ist, Fehler stillschweigend zu ignorieren, ohne dennoch zu kommentieren, dass einer auftreten könnte . Während Sie jetzt manchmal x, _ := SomeFunc() sehen (was ist der ignorierte Rückgabewert? ein Fehler? etwas anderes?), müssen Sie jetzt klar kommentieren:

x := try SomeFunc() else err {}

Seit meinem vorherigen Beitrag zur Unterstützung des Vorschlags habe ich zwei Ideen gesehen, die von @jagv (parameterlose try gibt *error zurück) und von @josharian (gekennzeichnet als Fehlerbehandler) gepostet wurden, an die ich glaube leicht modifizierte Form würde den Vorschlag erheblich aufwerten.

Wenn ich diese Ideen mit einer weiteren kombiniere, die ich selbst hatte, hätten wir vier Versionen von try :

  1. Versuchen()
  2. versuchen (Parameter)
  3. try(params, label)
  4. Versuch (Parameter, Panik)

1 würde einfach einen Zeiger auf den Fehlerrückgabeparameter (ERP) zurückgeben oder nil, wenn es keinen gibt (nur #4). Dies würde eine Alternative zu einem benannten ERP darstellen, ohne dass eine weitere Integration hinzugefügt werden müsste.

2 würde genau so funktionieren, wie es derzeit vorgesehen ist. Ein Nicht-Null-Fehler würde sofort zurückgegeben, könnte aber durch eine defer -Anweisung ergänzt werden.

3 würde wie von @josharian vorgeschlagen funktionieren, dh bei einem Nicht-Null-Fehler würde der Code zum Label verzweigen. Es gäbe jedoch kein Standard-Error-Handler-Label, da dieser Fall nun zu #2 degenerieren würde.

Es scheint mir, dass dies normalerweise eine bessere Möglichkeit ist, Fehler zu dekorieren (oder sie lokal zu behandeln und dann nil zurückzugeben) als defer , da es einfacher und schneller ist. Wem es nicht gefiel, konnte immer noch #2 verwenden.

Es wäre eine bewährte Methode, das Label/den Code zur Fehlerbehandlung am Ende der Funktion zu platzieren und nicht in den Rest des Funktionskörpers zurückzuspringen. Ich denke jedoch nicht, dass der Compiler sie erzwingen sollte, da es gelegentliche Gelegenheiten geben kann, in denen sie nützlich sind, und die Erzwingung in jedem Fall schwierig sein könnte.

Also würde normales Label- und goto -Verhalten gelten, sofern (wie @josharian sagte) #26058 zuerst behoben wird, aber ich denke, es sollte trotzdem behoben werden.

Der Name des Labels darf nicht panic lauten, da dies zu Konflikten mit #4 führen würde.

4 würde panic sofort zurückgeben oder verzweigen. Wenn dies die einzige Version von try wäre, die in einer bestimmten Funktion verwendet wird, wäre folglich kein ERP erforderlich.

Ich habe dies hinzugefügt, damit das Testpaket so funktionieren kann, wie es jetzt funktioniert, ohne dass weitere eingebaute oder andere Änderungen erforderlich sind. Es könnte jedoch auch in anderen _fatalen_ Szenarien nützlich sein.

Dies muss eine separate Version von try sein, da die Alternative zum Verzweigen zu einem Fehlerbehandler und dann in Panik geraten immer noch ein ERP erfordern würde.

Eine der stärksten Reaktionen auf den ursprünglichen Vorschlag war die Besorgnis, dass der normale Fluss, in den eine Funktion zurückkehrt, nicht mehr leicht sichtbar ist.

Zum Beispiel drückte @deanveloper diese Besorgnis sehr gut in https://github.com/golang/go/issues/32437#issuecomment -498932961 aus, was meiner Meinung nach der am höchsten bewertete Kommentar hier ist.

@dominikh schrieb in https://github.com/golang/go/issues/32437#issuecomment -499067357:

In gofmt-codiertem Code stimmt ein return immer mit /^\t*return / überein – es ist ein sehr triviales Muster, das ohne Hilfe mit dem Auge zu erkennen ist. try hingegen kann überall im Code vorkommen, beliebig tief in Funktionsaufrufen verschachtelt. Selbst noch so viel Training wird uns nicht in die Lage versetzen, den gesamten Kontrollfluss in einer Funktion ohne Werkzeugunterstützung sofort zu erkennen.

Um dabei zu helfen, schlug @brynbellomy gestern vor:

Jede try-Anweisung (auch solche ohne Handler) muss in einer eigenen Zeile stehen.

Weiterführend könnte das try der Anfang der Zeile sein, sogar für eine Zuweisung.

Es könnte also sein:

try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)

anstelle des Folgenden (aus dem Beispiel von @brynbellomy ):

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Das scheint, es würde auch ohne Editor- oder IDE-Unterstützung ein gutes Maß an Sichtbarkeit bewahren und gleichzeitig die Boilerplate reduzieren.

Das könnte mit dem derzeit vorgeschlagenen verzögerungsbasierten Ansatz funktionieren, der auf benannten Ergebnisparametern beruht, oder es könnte mit der Angabe normaler Handlerfunktionen funktionieren. (Das Angeben von Handler-Funktionen, ohne dass benannte Rückgabewerte erforderlich sind, scheint mir besser zu sein, als dass benannte Rückgabewerte erforderlich sind, aber das ist ein separater Punkt).

Der Vorschlag enthält dieses Beispiel:

info := try(try(os.Open(file)).Stat())    // proposed try built-in

Das könnte stattdessen sein:

try f := os.Open(file)
try info := f.Stat()

Das ist immer noch eine Reduzierung der Textbausteine ​​im Vergleich zu dem, was jemand heute schreiben könnte, wenn auch nicht ganz so kurz wie die vorgeschlagene Syntax. Vielleicht wäre das kurz genug?

@elagergren-spideroak lieferte dieses Beispiel:

try(try(try(to()).parse().this)).easily())

Ich denke, das hat nicht übereinstimmende Klammern, was vielleicht ein absichtlicher Punkt oder ein subtiler Humor ist, also bin ich mir nicht sicher, ob dieses Beispiel beabsichtigt, 2 try oder 3 try zu haben. In jedem Fall wäre es vielleicht besser, dies auf 2-3 Zeilen zu verteilen, die mit try beginnen.

@thepudds , darauf wollte ich in meinem früheren Kommentar hinaus. Außer dem gegebenen

try f := os.Open(file)
try info := f.Stat()

Es liegt auf der Hand, sich try als Try-Block vorzustellen, in dem mehr als ein Satz in Klammern gesetzt werden kann. So kann das obige werden

try (
    f := os.Open(file)
    into := f.Stat()
)

Wenn der Compiler damit umzugehen weiß, funktioniert das Gleiche auch beim Verschachteln. So jetzt kann das obige werden

try info := os.Open(file).Stat()

Aus Funktionssignaturen weiß der Compiler, dass Open einen Fehlerwert zurückgeben kann, und da es sich in einem try-Block befindet, muss es eine Fehlerbehandlung generieren und dann Stat() für den primär zurückgegebenen Wert aufrufen und so weiter.

Als Nächstes lassen Sie Anweisungen zu, bei denen entweder kein Fehlerwert generiert oder lokal behandelt wird. So kann man jetzt sagen

try (
    f := os.Open(file)
    debug("f: %v\n", f) // debug returns nothing
    into := f.Stat()
)

Dies ermöglicht die Entwicklung von Code, ohne Try-Blöcke neu anordnen zu müssen. Aber aus irgendeinem seltsamen Grund scheinen die Leute zu denken, dass die Fehlerbehandlung explizit angegeben werden muss! Sie wollen

try(try(try(to()).parse()).this)).easily())

Während es mir vollkommen gut geht

try to().parse().this().easily()

Obwohl in beiden Fällen genau derselbe Fehlerprüfcode generiert werden kann. Meiner Ansicht nach können Sie bei Bedarf immer speziellen Code für die Fehlerbehandlung schreiben. try (oder wie auch immer Sie es nennen möchten) entrümpelt einfach die standardmäßige Fehlerbehandlung (die darin besteht, sie dem Aufrufer zu zeigen).

Ein weiterer Vorteil besteht darin, dass der Compiler, wenn er die Standardfehlerbehandlung generiert, weitere identifizierende Informationen hinzufügen kann, damit Sie wissen, welche der vier oben genannten Funktionen fehlgeschlagen sind.

Ich war etwas besorgt über die Lesbarkeit von Programmen, in denen try in anderen Ausdrücken vorkommt. Also habe ich grep "return .*err$" in der Standardbibliothek ausgeführt und angefangen, Blöcke nach dem Zufallsprinzip zu lesen. Es gibt 7214 Ergebnisse, ich habe nur ein paar hundert gelesen.

Das erste, was zu beachten ist, ist, dass dort, wo try zutrifft, fast alle diese Blöcke ein wenig besser lesbar werden.

Die zweite Sache ist, dass nur sehr wenige davon, weniger als 1 von 10, try in einen anderen Ausdruck einfügen würden. Der typische Fall sind Anweisungen der Form x := try(...) oder ^try(...)$ .

Hier sind einige Beispiele, bei denen try in einem anderen Ausdruck erscheinen würde:

Text/Vorlage

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

wird:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        return !try(le(arg1, arg2)), nil
}

Text/Vorlage

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

wird

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

(Dies ist das fragwürdigste Beispiel, das ich gesehen habe)

regulärer Ausdruck/Syntax:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

wird

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        c, t = try(nextRune(t))
        p.literal(c)
        ...
}

Dies ist kein Beispiel für try in einem anderen Ausdruck, aber ich möchte es hervorheben, weil es die Lesbarkeit verbessert. Hier ist viel einfacher zu sehen, dass die Werte von c und t außerhalb des Geltungsbereichs der if-Anweisung leben.

net/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

wird:

        req.Header = Header(try(tp.ReadMIMEHeader())

Datenbank/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

wird

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

Datenbank/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

wird

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }

net/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

wird

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                return try(p.t.dialclientconn(addr, singleuse))
        }

net/http

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

wird

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

(Diese hier gefällt mir sehr gut.)

net/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

wird

        hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder

}

(Auch schön.)

Netto :

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

wird

        if ctrlFn != nil {
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
        }

vielleicht ist das zu viel, und stattdessen sollte es sein:

        if ctrlFn != nil {
                c := try(newRawConn(fd))
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
        }

Insgesamt genieße ich die Wirkung von try auf den Code der Standardbibliothek, den ich durchgelesen habe.

Ein letzter Punkt: Zu sehen, dass try über die wenigen Beispiele im Vorschlag hinaus zum Lesen von Code angewendet wurden, war aufschlussreich. Ich denke, es ist eine Überlegung wert, ein Tool zu schreiben, um Code automatisch zu konvertieren, um try zu verwenden (wo es die Semantik des Programms nicht ändert). Es wäre interessant, ein Beispiel der Diffs zu lesen, das gegen beliebte Pakete auf Github produziert wird, um zu sehen, ob das, was ich in der Standardbibliothek gefunden habe, Bestand hat. Die Ergebnisse eines solchen Programms könnten zusätzliche Einblicke in die Wirkung des Vorschlags geben.

@crawshaw danke dafür, es war toll, es in Aktion zu sehen. Aber als ich es in Aktion sah, nahm ich die Argumente gegen die Inline-Fehlerbehandlung, die ich bisher zurückgewiesen hatte, ernster.

Da dies in so unmittelbarer Nähe zu @thepudds interessantem Vorschlag war, try zu einer Aussage zu machen, habe ich alle Beispiele mit dieser Syntax neu geschrieben und fand sie viel klarer als entweder den Ausdruck - try oder den Status quo, ohne dass zu viele zusätzliche Zeilen erforderlich sind:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}
func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}
        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)
        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

Dieser wäre wohl besser mit einem Ausdruck - try , wenn es mehrere Felder gäbe, die try -ed werden müssten, aber ich bevorzuge immer noch das Gleichgewicht dieses Kompromisses

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

Dies ist im Grunde der schlimmste Fall und sieht gut aus:

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

Ich habe mit mir selbst darüber diskutiert, ob if try legal sein würde oder sollte, aber ich konnte keine vernünftige Erklärung finden, warum es nicht sein sollte, und es funktioniert hier ganz gut:

func (f *http2Framer) endWrite() error {
        ...
        if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}
        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

Wenn ich mir die Beispiele von @crawshaw ansehe , fühle ich mich nur noch sicherer, dass der Kontrollfluss oft kryptisch genug gemacht wird, um noch vorsichtiger mit dem Design umzugehen. Selbst eine kleine Menge an Komplexität zu erzählen, wird schwierig zu lesen und leicht zu verpfuschen. Ich bin froh, dass Optionen in Betracht gezogen werden, aber die Verkomplizierung des Kontrollflusses in einer so zurückhaltenden Sprache scheint außergewöhnlich untypisch.

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Außerdem "versucht" try nichts. Es ist ein "Schutzrelais". Wenn die grundlegende Semantik des Vorschlags nicht stimmt, überrascht es mich nicht, dass der resultierende Code ebenfalls problematisch ist.

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

Wenn Sie eine try-Anweisung machen, können Sie mit einem Flag angeben, welcher Rückgabewert und welche Aktion ausgeführt wird:

try c, @      := newRawConn(fd) // return
try c, <strong i="6">@panic</strong> := newRawConn(fd) // panic
try c, <strong i="7">@hname</strong> := newRawConn(fd) // invoke named handler
try c, <strong i="8">@_</strong>     := newRawConn(fd) // ignore, or invoke "ignored" handler if defined

Sie benötigen immer noch eine Unterausdruckssyntax (Russ hat angegeben, dass dies eine Voraussetzung ist), zumindest für Panik- und Ignorieren-Aktionen.

Zunächst begrüße ich @crawshaw dafür, dass er sich die Zeit genommen hat, sich ungefähr 200 echte Beispiele anzusehen, und sich die Zeit für seinen durchdachten Artikel oben genommen hat.

Zweitens @jimmyfrasche bezüglich Ihrer Antwort hier zum http2Framer -Beispiel:


Ich habe mit mir selbst darüber diskutiert, ob if try legal sein würde oder sollte, aber ich konnte keine vernünftige Erklärung finden, warum es nicht sein sollte, und hier funktioniert es ganz gut:

```
func (f *http2Framer) endWrite() Fehler {
...
if try n := fwWrite(f.wbuf); n != len(f.wbuf) {
gib io.ErrShortWrite zurück
}
Null zurückgeben
}

At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.

That `http2Framer` example could instead be:

func (f *http2Framer) endWrite() Fehler {
...
try n := fwWrite(f.wbuf)
if n != len(f.wbuf) {
gib io.ErrShortWrite zurück
}
Null zurückgeben
}
`` That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the versuchen`.

@deanveloper schrieb oben in https://github.com/golang/go/issues/32437#issuecomment -498932961:

Die Rückkehr von einer Veranstaltung schien eine „heilige“ Sache gewesen zu sein

Dieses spezifische http2Framer -Beispiel ist am Ende nicht so kurz, wie es möglicherweise sein könnte. Es gilt jedoch, von einer "heiligeren" Funktion zurückzukehren, wenn try das erste Ding in einer Zeile sein muss.

@crawshaw erwähnt:

Die zweite Sache ist, dass nur sehr wenige von ihnen, weniger als 1 von 10, try in einen anderen Ausdruck einfügen würden. Der typische Fall sind Anweisungen der Form x := try(...) oder ^try(...)$.

Vielleicht ist es in Ordnung, diesen 1 von 10 Beispielen nur teilweise mit einer eingeschränkteren Form von try zu helfen, insbesondere wenn der typische Fall aus diesen Beispielen mit der gleichen Zeilenzahl endet, selbst wenn try ist muss das Erste in einer Linie sein?

@jimmyfrasche

@crawshaw danke dafür, es war toll, es in Aktion zu sehen. Aber als ich es in Aktion sah, nahm ich die Argumente gegen die Inline-Fehlerbehandlung, die ich bisher zurückgewiesen hatte, ernster.

Da dies in so unmittelbarer Nähe zu @thepudds interessantem Vorschlag war, try zu einer Aussage zu machen, habe ich alle Beispiele mit dieser Syntax neu geschrieben und fand sie viel klarer als entweder den Ausdruck - try oder den Status quo, ohne dass zu viele zusätzliche Zeilen erforderlich sind:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

Ihr erstes Beispiel veranschaulicht gut, warum ich den Ausdruck try stark bevorzuge. In Ihrer Version muss ich das Ergebnis des Aufrufs von le in eine Variable einfügen, aber diese Variable hat keine semantische Bedeutung, die der Begriff le nicht bereits impliziert. Ich kann ihm also keinen Namen geben, der nicht bedeutungslos (wie x ) oder überflüssig (wie lessOrEqual ) ist. Mit expression- try wird keine Zwischenvariable benötigt, sodass dieses Problem gar nicht erst auftritt.

Ich möchte lieber keine geistige Anstrengung aufwenden, um Namen für Dinge zu erfinden, die besser anonym bleiben.

Ich freue mich, meine Unterstützung hinter die letzten Posts zu werfen, in denen try (das Schlüsselwort) an den Anfang der Zeile verschoben wurde. Es sollte wirklich den gleichen visuellen Raum wie return .

Betreff: @jimmyfrasches Vorschlag, try in zusammengesetzten if -Anweisungen zuzulassen, das ist genau das, was meiner Meinung nach viele hier aus mehreren Gründen zu vermeiden versuchen:

  • Es vereint zwei sehr unterschiedliche Kontrollflussmechanismen in einer einzigen Zeile
  • Der try -Ausdruck wird tatsächlich zuerst ausgewertet und kann dazu führen, dass die Funktion zurückkehrt, aber er erscheint nach if
  • Sie kehren mit völlig anderen Fehlern zurück, von denen wir einen nicht im Code sehen und einen, den wir sehen
  • es macht es weniger offensichtlich, dass das try tatsächlich unbehandelt ist, da der Block einem Handler-Block sehr ähnlich sieht (obwohl er ein völlig anderes Problem behandelt).

Man könnte diese Situation aus einem etwas anderen Blickwinkel angehen, der es bevorzugt, die Leute dazu zu drängen, mit try s umzugehen. Wie wäre es, wenn Sie der Syntax try / else erlauben, nachfolgende Bedingungen zu enthalten (was ein häufiges Muster bei vielen E/A-Funktionen ist, die sowohl err als auch n zurückgeben

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf) else err {
                return errors.Wrap(err, "error writing:")
        } else if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

In dem Fall, in dem Sie den von .Write zurückgegebenen Fehler nicht behandeln, haben Sie immer noch eine klare Anmerkung, dass .Write einen Fehler verursachen könnte (wie von @thepudds hervorgehoben):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Ich schließe mich der Antwort von @daved an. Meiner Meinung nach wurde jedes Beispiel, das @crawshaw hervorgehoben hat, durch try weniger klar und fehleranfälliger.

Ich freue mich, meine Unterstützung hinter die letzten Posts zu werfen, in denen try (das Schlüsselwort) an den Anfang der Zeile verschoben wurde. Es sollte wirklich den gleichen visuellen Raum wie return .

Angesichts der beiden Optionen für diesen Punkt und unter der Annahme, dass eine ausgewählt wurde, und damit eine Art Präzedenzfall für zukünftige potenzielle Funktionen:

EIN.)

try f := os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

B.)

f := try os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

Welche der beiden bieten mehr Flexibilität für die zukünftige Verwendung neuer Keywords? _(Ich kenne die Antwort darauf nicht, da ich die dunkle Kunst des Schreibens von Compilern nicht gemeistert habe.)_ Wäre ein Ansatz einschränkender als ein anderer?

@davecheney @daved @crawshaw
Ich würde den Daves in diesem Punkt eher zustimmen: In den Beispielen von @crawshaw gibt es viele try -Anweisungen, die tief in Zeilen eingebettet sind, in denen eine Menge anderer Dinge vor sich gehen. Wirklich schwer zu erkennende Austrittspunkte. Außerdem scheinen die try -Parens in einigen Beispielen die Dinge ziemlich unübersichtlich zu machen.

Es ist sehr nützlich, einen Haufen stdlib-Code zu sehen, der so transformiert wurde, also habe ich die gleichen Beispiele genommen, sie aber gemäß dem alternativen Vorschlag neu geschrieben, der restriktiver ist:

  • try als Schlüsselwort
  • nur ein try pro Zeile
  • try muss am Anfang einer Zeile stehen

Hoffentlich hilft uns das beim Vergleich. Ich persönlich finde, dass diese Beispiele viel prägnanter aussehen als ihre Originale, ohne jedoch den Kontrollfluss zu verdecken. try bleibt überall dort, wo es verwendet wird, gut sichtbar.

Text/Vorlage

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

wird:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

Text/Vorlage

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

wird

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

regulärer Ausdruck/Syntax:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

wird

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}

net/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

wird:

        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)

Datenbank/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

wird

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

Datenbank/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

wird

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

net/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

wird

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

net/http
Dieser erspart uns eigentlich keine Zeilen, aber ich finde ihn viel klarer, weil if err == nil eine relativ ungewöhnliche Konstruktion ist.

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

wird

func (f *http2Framer) endWrite() error {
        ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

net/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

wird

        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}

Netz:

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

wird

        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

@james-lawrence Als Antwort auf https://github.com/golang/go/issues/32437#issuecomment -500116099 : Ich erinnere mich nicht, dass Ideen wie ein optionales , err ernsthaft in Betracht gezogen wurden, nein. Ich persönlich halte das für eine schlechte Idee, weil es bedeutet, dass, wenn sich eine Funktion ändert, um einen nachgestellten error -Parameter hinzuzufügen, vorhandener Code weiter kompiliert wird, sich aber ganz anders verhält.

Die Verwendung von defer zur Behandlung der Fehler ist sehr sinnvoll, führt jedoch dazu, dass der Fehler benannt werden muss und eine neue Art von if err != nil -Boilerplate entsteht.

Externe Handler müssen dies tun:

func handler(err *error) {
  if *err != nil {
    *err = handle(*err)
  }
} 

die verwendet wird wie

defer handler(&err)

Externe Handler müssen nur einmal geschrieben werden, aber es müssten zwei Versionen vieler Fehlerbehandlungsfunktionen vorhanden sein: diejenige, die zurückgestellt werden soll, und diejenige, die auf normale Weise verwendet werden soll.

Interne Handler müssen dies tun:

defer func() {
  if err != nil {
    err = handle(err)
  }
}()

In beiden Fällen muss der Fehler der äußeren Funktion benannt werden, damit darauf zugegriffen werden kann.

Wie ich bereits erwähnt habe, kann dies in einer einzigen Funktion abstrahiert werden:

func catch(err *error, handle func(error) error) {
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

Das widerspricht @griesemers Besorgnis über die Mehrdeutigkeit von nil -Handler-Funktionen und hat seine eigenen defer - und func(err error) error -Boilerplates, zusätzlich zu der Notwendigkeit, err zu benennen

Wenn try als Schlüsselwort endet, kann es sinnvoll sein, auch ein catch -Schlüsselwort zu haben, das unten beschrieben wird.

Syntaktisch wäre es ähnlich wie handle :

catch err {
  return handleThe(err)
}

Semantisch wäre es Zucker für den obigen internen Handler-Code:

defer func() {
  if err != nil {
    err = handleThe(err)
  }
}()

Da es etwas magisch ist, könnte es den Fehler der äußeren Funktion erfassen, selbst wenn er nicht benannt wurde. (Das err nach catch ist eher wie ein Parametername für den Block catch ).

catch hätte die gleiche Einschränkung wie try , dass es sich in einer Funktion befinden muss, die eine endgültige Fehlerrückgabe hat, da beide Zucker sind, die sich darauf verlassen.

Das ist bei weitem nicht so leistungsfähig wie der ursprüngliche handle -Vorschlag, aber es würde die Anforderung vermeiden, einen Fehler zu benennen, um ihn zu behandeln, und es würde die oben besprochene neue Textbausteine ​​für interne Handler entfernen, während es einfach genug wäre, dies nicht zu tun erfordern separate Versionen von Funktionen für externe Handler.

Eine komplizierte Fehlerbehandlung kann erfordern, dass catch nicht verwendet wird, genauso wie es möglicherweise erforderlich ist, try nicht zu verwenden.

Da es sich bei beiden um Zucker handelt, ist es nicht erforderlich, catch mit try zu verwenden. Die catch -Handler werden immer dann ausgeführt, wenn die Funktion einen Nicht- nil -Fehler zurückgibt, was beispielsweise das Einfügen einer schnellen Protokollierung ermöglicht:

catch err {
  log.Print(err)
  return err
}

oder packen Sie einfach alle zurückgegebenen Fehler ein:

catch err {
  return fmt.Errorf("foo: %w", err)
}

@ianlancetaylor

_" Ich denke, es ist eine schlechte Idee, weil es bedeutet, dass, wenn sich eine Funktion ändert, um einen nachgestellten error -Parameter hinzuzufügen, vorhandener Code weiter kompiliert wird, sich aber ganz anders verhält."_

Dies ist wahrscheinlich die richtige Betrachtungsweise, wenn Sie sowohl den Upstream- als auch den Downstream-Code steuern können, sodass Sie dies tun können, wenn Sie eine Funktionssignatur ändern müssen, um auch einen Fehler zurückzugeben.

Aber ich möchte Sie bitten, darüber nachzudenken, was passiert, wenn jemand weder vor- noch nachgelagert seine eigenen Pakete kontrolliert? Und auch die Anwendungsfälle zu berücksichtigen, in denen Fehler hinzugefügt werden könnten, und was passiert, wenn Fehler hinzugefügt werden müssen, Sie aber keine Änderung des nachgelagerten Codes erzwingen können?

Können Sie sich ein Beispiel vorstellen, bei dem jemand die Signatur ändern würde, um einen Rückgabewert hinzuzufügen? Für mich fallen sie typischerweise in die Kategorie _"Ich wusste nicht, dass ein Fehler auftreten würde"_ oder _"Ich fühle mich faul und möchte mich nicht anstrengen, weil der Fehler wahrscheinlich nicht passieren wird." _

In beiden Fällen kann ich eine Fehlerrückgabe hinzufügen, da sich herausstellt, dass ein Fehler behandelt werden muss. Was kann ich in diesem Fall tun, wenn ich die Signatur nicht ändern kann, weil ich die Kompatibilität für andere Entwickler, die meine Pakete verwenden, nicht beeinträchtigen möchte? Ich vermute, dass der Fehler in den allermeisten Fällen auftritt und dass der Code, der die Funktion aufgerufen hat, die den Fehler nicht zurückgibt, sich _wie auch immer_ ganz anders verhält.

Eigentlich mache ich letzteres selten, aber zu häufig mache ich ersteres. Aber ich habe bemerkt, dass Pakete von Drittanbietern häufig Erfassungsfehler ignorieren, wo sie sein sollten, und ich weiß das, weil, wenn ich ihren Code in GoLand aufrufe, jedes Mal leuchtend orange markiert wird. Ich würde gerne Pull-Requests einreichen, um den Paketen, die ich häufig verwende, eine Fehlerbehandlung hinzuzufügen, aber wenn ich das tue, werden sie von den meisten nicht akzeptiert, weil ich ihre Code-Signaturen brechen würde.

Da keine abwärtskompatible Methode zum Hinzufügen von Fehlern angeboten wird, die von Funktionen zurückgegeben werden sollen, können Entwickler, die Code verteilen und darauf achten, dass nichts für ihre Benutzer beschädigt wird, ihre Pakete nicht so weiterentwickeln, dass sie die Fehlerbehandlung enthalten, wie sie sollten.


Anstatt das Problem darin zu sehen, dass sich der Code anders verhält, sollten Sie das Problem vielleicht als eine technische Herausforderung betrachten, wie der Nachteil einer Methode minimiert werden kann, die einen Fehler nicht aktiv erfasst. Das hätte einen breiteren und längerfristigen Wert.

Ziehen Sie beispielsweise in Betracht, einen Paketfehlerhandler hinzuzufügen, den man festlegen muss, bevor Fehler ignoriert werden können?


Um ehrlich zu sein, war Gos Idiom, Fehler zusätzlich zu den regulären Rückgabewerten zurückzugeben, eine seiner besseren Innovationen. Aber wie so oft, wenn man Dinge verbessert, legt man oft andere Schwächen offen, und ich werde argumentieren, dass die Fehlerbehandlung von Go nicht innovativ genug war.

Wir Gophers sind darauf versessen, einen Fehler zurückzugeben, anstatt eine Ausnahme auszulösen, daher lautet meine Frage: „Warum sollten wir nicht Fehler von jeder Funktion zurückgeben?“_ Wir tun dies nicht immer, weil das Schreiben von Code ohne Fehlerbehandlung es ist bequemer als damit zu codieren. Also lassen wir die Fehlerbehandlung weg, wenn wir glauben, dass wir davon wegkommen können. Aber häufig raten wir falsch.

Also wirklich, wenn es möglich wäre, den Code elegant und lesbar zu machen, würde ich argumentieren, dass Rückgabewerte und Fehler wirklich getrennt behandelt werden sollten und dass _jede_ Funktion die Fähigkeit haben sollte, Fehler unabhängig von ihren früheren Funktionssignaturen zurückzugeben. Und es wäre ein lohnendes Unterfangen, vorhandenen Code dazu zu bringen, Code, der jetzt Fehler erzeugt, ordnungsgemäß zu handhaben.

Ich habe nichts vorgeschlagen, weil ich mir keine praktikable Syntax vorstellen konnte, aber wenn wir ehrlich zu uns selbst sein wollen, drehte sich nicht alles in diesem Thread und im Zusammenhang mit der Fehlerbehandlung von Go im Allgemeinen um die Tatsache, dass die Fehlerbehandlung und Programmlogik sind seltsame Bettgenossen, also würden Fehler im Idealfall am besten auf irgendeine Weise außerhalb des Bandes behandelt werden?

try als Schlüsselwort hilft sicherlich bei der Lesbarkeit (im Vergleich zu einem Funktionsaufruf) und scheint weniger komplex zu sein. @brynbellomy @crawshaw danke, dass du dir die Zeit genommen hast, die Beispiele aufzuschreiben.

Ich nehme an, mein allgemeiner Gedanke ist, dass try zu viel bewirkt. Es löst: Funktion aufrufen, Variablen zuweisen, Fehler überprüfen und Fehler zurückgeben, falls vorhanden. Ich schlage vor, dass wir stattdessen den Bereich abschneiden und nur für die bedingte Rückgabe auflösen: "Rückgabe, wenn letztes Argument nicht nil".

Dies ist wahrscheinlich keine neue Idee ... Aber nachdem ich die Vorschläge im Fehler-Feedback-Wiki überflogen habe, habe ich sie nicht gefunden (bedeutet nicht, dass sie nicht vorhanden ist).

Mini-Vorschlag zur bedingten Rückgabe

Auszug:

err, thing := newThing(name)
refuse nil, err

Ich habe es auch im Wiki unter "Alternative Ideen" hinzugefügt.

Nichts zu tun scheint auch eine sehr vernünftige Option zu sein.

@alexhornbake , das gibt mir eine etwas andere Idee, die nützlicher wäre

assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)

Auf diese Weise würde es nicht nur für die Fehlerprüfung gelten, sondern für viele Arten von Logikfehlern.

Das Gegebene würde in einen Fehler eingeschlossen und zurückgegeben.

@alexhornbach

Genauso wie try es nicht wirklich versucht, ist refuse nicht wirklich „verweigernd“. Die gemeinsame Absicht hier war, dass wir ein "Schutzrelais" setzen ( relay ist kurz, genau und alliteratives Wort für return ), das "auslöst", wenn einer der verdrahteten Werte eine Bedingung erfüllt (dh ist ein Nicht-Null-Fehler). Es ist eine Art Leistungsschalter und kann meiner Meinung nach einen Mehrwert schaffen, wenn sein Design auf uninteressante Fälle beschränkt wäre, um einfach einige der am niedrigsten hängenden Boilerplates zu reduzieren. Alles, was auch nur annähernd komplex ist, sollte sich auf einfachen Go-Code verlassen.

Ich empfehle Cranshaw auch für seine Arbeit beim Durchsuchen der Standardbibliothek, aber ich bin zu einem ganz anderen Schluss gekommen ... Ich denke, es macht fast alle diese Codeschnipsel schwieriger zu lesen und anfälliger für Missverständnisse.

        req.Header = Header(try(tp.ReadMIMEHeader())

Ich werde sehr oft vermissen, dass dies zu Fehlern führen kann. Ein schnelles Lesen bringt mich zu "ok, setze den Header auf Header von ReadMimeHeader des Dings".

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

Bei diesem hier kreuzen sich meine Augen beim Versuch, diese OpenDB-Zeile zu analysieren. Da ist so viel Dichte ... Das zeigt das Hauptproblem, das alle verschachtelten Funktionsaufrufe haben, nämlich dass Sie von innen nach außen lesen müssen, und Sie müssen es in Ihrem Kopf analysieren, um herauszufinden, wo der innerste Teil ist .

Beachten Sie auch, dass dies von zwei verschiedenen Stellen in derselben Zeile zurückkehren kann ... Sie werden debuggen, und es wird sagen, dass von dieser Zeile ein Fehler zurückgegeben wurde, und das erste, was jeder tun wird, ist, es zu versuchen Finden Sie heraus, warum OpenDB mit diesem seltsamen Fehler fehlschlägt, wenn es tatsächlich OpenConnector ist (oder umgekehrt).

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }   

Dies ist eine Stelle, an der der Code versagen kann, wo dies zuvor unmöglich gewesen wäre. Ohne try kann die Konstruktion von Struct-Literalen nicht fehlschlagen . Meine Augen werden darüber schweifen wie „ok, einen Treiber erstellen Stmt ... weitermachen ...“ und es wird so leicht zu übersehen sein, dass dies tatsächlich dazu führen kann, dass Ihre Funktion fehlerhaft ist. Der einzige Weg, der vorher möglich gewesen wäre, wäre, wenn ctxDriverPrepare in Panik geraten wäre ... und wir alle wissen, dass dies ein Fall ist, der 1.) im Grunde nie passieren sollte und 2.) wenn dies der Fall ist, bedeutet dies, dass etwas drastisch falsch ist.

Wenn Sie ein Schlüsselwort und eine Anweisung ausprobieren, werden viele meiner Probleme damit behoben. Ich weiß, dass das nicht abwärtskompatibel ist, aber ich glaube nicht, dass die Verwendung einer schlechteren Version davon die Lösung für das Problem der Abwärtskompatibilität ist.

@daved Ich bin mir nicht sicher, ob ich folge. Magst du den Namen nicht oder magst du die Idee nicht?

Wie auch immer, ich habe das hier als Alternative gepostet ... Wenn es berechtigtes Interesse gibt, kann ich ein neues Thema zur Diskussion eröffnen, will diesen Thread nicht verschmutzen (vielleicht zu spät?) Daumen hoch / runter für die ursprüngliche Idee geben uns ein Gefühl... Natürlich offen für alternative Namen. Wichtiger Teil ist "bedingte Rückgabe, ohne zu versuchen, die Abtretung zu bearbeiten".

Obwohl mir der Fangvorschlag von @jimmyfrasche gefällt , möchte ich eine Alternative vorschlagen:
go handler fmt.HandleErrorf("copy %s %s", src, dst)
wäre gleichbedeutend mit:
go defer func(){ if(err != nil){ fmt.HandleErrorf(&err,"copy %s %s", src, dst) } }()
wobei err der zuletzt genannte Rückgabewert ist, mit Typ error. Handler können jedoch auch verwendet werden, wenn Rückgabewerte nicht benannt sind. Der allgemeinere Fall wäre auch erlaubt:
go handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
Das Hauptproblem, das ich bei der Verwendung benannter Rückgabewerte habe (was catch nicht löst), ist, dass err überflüssig ist. Wenn ein Aufruf an einen Handler wie fmt.HandleErrorf wird, gibt es kein vernünftiges erstes Argument außer einem Zeiger auf den Fehlerrückgabewert. Warum dem Benutzer die Möglichkeit geben, einen Fehler zu machen?

Im Vergleich zu catch besteht der Hauptunterschied darin, dass handler das Aufrufen vordefinierter Handler etwas einfacher macht, auf Kosten einer ausführlicheren Definition, um sie an Ort und Stelle zu definieren. Ich bin mir nicht sicher, ob dies ideal ist, aber ich denke, es entspricht eher dem ursprünglichen Vorschlag.

@yiyus catch , wie ich es definiert habe, muss err nicht in der Funktion benannt werden, die die catch enthält.

In catch err { ist err der Name des Fehlers innerhalb des Blocks catch . Es ist wie ein Funktionsparametername.

Damit ist etwas wie fmt.HandleErrorf nicht mehr erforderlich, da Sie einfach das normale fmt.Errorf verwenden können:

func f() error {
  catch err {
    return fmt.Errorf("foo: %w", err)
  }
  return errors.New("bar")
}

was einen Fehler zurückgibt, der als foo: bar gedruckt wird.

Ich mag diesen Ansatz nicht, weil:

  • Der Funktionsaufruf try() unterbricht die Codeausführung in der übergeordneten Funktion.
  • es gibt kein Schlüsselwort return , aber der Code gibt tatsächlich zurück.

Es werden viele Möglichkeiten vorgeschlagen, Handler zu machen, aber ich denke, sie verfehlen oft zwei wichtige Anforderungen:

  1. Es muss deutlich anders und besser sein als if x, err := thingie(); err != nil { handle(err) } . Ich denke, Vorschläge in der Art von try x := thingie else err { handle(err) } erfüllen diese Messlatte nicht. Warum sagst du nicht einfach if ?

  2. Es sollte orthogonal zur bestehenden Funktionalität von defer sein. Das heißt, es sollte so unterschiedlich sein, dass klar ist, dass der vorgeschlagene Handhabungsmechanismus eigenständig benötigt wird, ohne seltsame Eckfälle zu erzeugen, wenn Handhabung und Zurückstellung interagieren.

Bitte beachten Sie diese Anforderungen, wenn wir alternative Mechanismen für try /Handle diskutieren.

@carlmjohnson Ich mag die catch -Idee von @jimmyfrasche in Bezug auf Ihren Punkt 2 - es ist nur Syntaxzucker für ein defer , der 2 Zeilen spart und es Ihnen ermöglicht, den Fehlerrückgabewert zu benennen (was in an der Reihe würden Sie auch alle anderen benennen müssen, wenn Sie dies nicht bereits getan haben). Es wirft kein Orthogonalitätsproblem mit defer , weil es defer ist.

Wiederholen, was @ubombi gesagt hat:

Der Aufruf der Funktion try() unterbricht die Codeausführung in der übergeordneten Funktion.; es gibt kein return-Schlüsselwort, aber der Code kehrt tatsächlich zurück.

In Ruby sind Procs und Lambdas ein Beispiel dafür, was try tut ... Ein Proc ist ein Codeblock, dessen return-Anweisung nicht vom Block selbst, sondern vom Aufrufer zurückgegeben wird.

Das ist genau das, was try tut ... es ist nur eine vordefinierte Ruby-Prozedur.

Ich denke, wenn wir diesen Weg gehen würden, könnten wir den Benutzer vielleicht tatsächlich seine eigene try -Funktion definieren lassen, indem wir proc functions einführen

Ich bevorzuge immer noch if err != nil , weil es besser lesbar ist, aber ich denke, try wäre vorteilhafter, wenn der Benutzer seine eigene Prozedur definieren würde:

proc try(err *error, msg string) {
  if *err != nil {
    *err = fmt.Errorf("%v: %w", msg, *err)
    return
  }
}

Und dann kannst du es nennen:

func someFunc() (string, error) {
  err := doSomething()
  try(&err, "someFunc failed")
}

Der Vorteil dabei ist, dass Sie die Fehlerbehandlung nach Ihren eigenen Vorstellungen definieren können. Und Sie können auch ein proc exponiert, privat oder intern machen.

Es ist auch besser als die handle {} -Klausel im ursprünglichen Go2-Vorschlag, da Sie dies nur einmal für die gesamte Codebasis und nicht in jeder Funktion definieren können.

Eine Überlegung für die Lesbarkeit ist, dass ein func() und ein proc() unterschiedlich aufgerufen werden können, wie z. B. func() und proc!() , damit ein Programmierer weiß, dass ein proc-Aufruf tatsächlich aus dem zurückkehren könnte aufrufende Funktion.

@marwan-at-work, sollte try(err, "someFunc failed") try(&err, "someFunc failed") in deinem Beispiel nicht try(&err, "someFunc failed") sein?

@dpinela danke für die Korrektur, Code aktualisiert :)

Die gängige Praxis, die wir hier zu überschreiben versuchen, ist das, was das Standard-Stack-Unwinding in vielen Sprachen in einer Ausnahme vorschlägt (und daher wurde das Wort "versuchen" ausgewählt ...).
Aber wenn wir nur eine Funktion (...try() oder andere) zulassen könnten, die im Trace zwei Ebenen zurückspringen würde, dann

try := handler(err error) {     //which corelates with - try := func(err error) 
   if err !=nil{
       //do what ever you want to do when there's an error... log/print etc
       return2   //2 levels
   }
} 

und dann ein Code wie
f := try(os.Open(Dateiname))
könnte genau das tun, was der Vorschlag empfiehlt, aber da es sich um eine Funktion (oder eigentlich eine "Handler-Funktion") handelt, hat der Entwickler viel mehr Kontrolle darüber, was die Funktion tut, wie sie den Fehler in verschiedenen Fällen formatiert, und verwendet überall einen ähnlichen Handler der Code, um (sagen wir) os.Open zu handhaben, anstatt jedes Mal fmt.Errorf("Fehler beim Öffnen der Datei %s ....") zu schreiben.
Dies würde auch eine Fehlerbehandlung erzwingen, als ob "try" nicht definiert wäre - es ist ein Kompilierzeitfehler.

@guybrand Eine solche zweistufige Rückgabe return2 (oder "nicht lokale Rückgabe", wie das allgemeine Konzept in Smalltalk genannt wird) wäre ein netter Allzweckmechanismus (ebenfalls vorgeschlagen von @mikeschinkel in #32473). . Aber es scheint, dass try in Ihrem Vorschlag noch benötigt wird, daher sehe ich keinen Grund für return2 - try kann nur return tun try schreiben könnte, aber das geht bei beliebigen Signaturen nicht.

@griesemer

_"Also sehe ich keinen Grund für das return2 - das try kann einfach das return tun."_

Ein Grund – wie ich in #32473 _(danke für den Hinweis)_ erwähnt habe – wäre, mehrere Ebenen von break und continue zusätzlich zu return zuzulassen.

Nochmals vielen Dank an alle für all die neuen Kommentare; Es ist eine erhebliche Zeitinvestition, um mit der Diskussion Schritt zu halten und umfangreiches Feedback zu verfassen. Und besser noch, trotz der manchmal leidenschaftlichen Auseinandersetzungen war dies bisher ein eher ziviler Thread. Danke!

Hier ist eine weitere kurze Zusammenfassung, diesmal etwas komprimierter; Entschuldigung an diejenigen, die ich nicht erwähnt, vergessen oder falsch dargestellt habe. An diesem Punkt denke ich, dass sich einige größere Themen abzeichnen:

1) Im Allgemeinen wird die Verwendung eines eingebauten für die try -Funktionalität als schlechte Wahl empfunden: Angesichts der Tatsache, dass es den Kontrollfluss beeinflusst, sollte es _mindestens_ ein Schlüsselwort sein ( @carloslenz "bevorzugt eine Anweisung ohne Klammer"); try als Ausdruck scheint keine gute Idee zu sein, es schadet der Lesbarkeit ( @ChrisHines , @jimmyfrasche), sie sind "Rückgaben ohne return ". @brynbellomy führte eine aktuelle Analyse von try durch, die als Identifikatoren verwendet wurden; es scheint prozentual nur sehr wenige zu geben, so dass es möglich sein könnte, die Keyword-Route zu gehen, ohne zu viel Code zu beeinflussen.

2) @crawshaw brauchte einige Zeit, um ein paar hundert Anwendungsfälle aus der std-Bibliothek zu analysieren, und kam zu dem Schluss, dass try wie vorgeschlagen fast immer die Lesbarkeit verbesserte. @jimmyfrasche kam zum gegenteiligen Ergebnis.

3) Ein weiteres Thema ist, dass die Verwendung defer für die Fehlerdekoration nicht ideal ist. @josharian weist darauf hin, dass die defer immer bei Funktionsrückgabe ausgeführt werden, aber wenn sie zur Fehlerdekoration hier sind, kümmern wir uns nur um ihren Körper, wenn ein Fehler vorliegt, was zu Verwirrung führen könnte.

4) Viele schrieben Vorschläge zur Verbesserung des Vorschlags. @zeebo , @patrick-nyt unterstützen die gofmt Formatierung einfacher if Anweisungen in einer einzigen Zeile (und sind mit dem Status quo zufrieden). @jargv schlug vor, dass try() (ohne Argumente) einen Zeiger auf den aktuell "ausstehenden" Fehler zurückgeben könnte, was die Notwendigkeit beseitigen würde, das Fehlerergebnis zu benennen, nur damit man in einem defer Zugriff darauf hat @masterada schlug vor, stattdessen errorfunc() zu verwenden. @velovix hat die Idee eines 2-Argumentes try wiederbelebt, wobei das 2. Argument ein Fehlerbehandler wäre.

@klaidliadon , @networkimprov sind für spezielle "Zuweisungsoperatoren" wie in f, # := os.Open() statt try . @networkimprov reichte einen umfassenderen Alternativvorschlag ein, der solche Ansätze untersucht (siehe Ausgabe Nr. 32500). @mikeschinkel reichte auch einen alternativen Vorschlag ein, der vorschlug, zwei neue Allzweck-Sprachfunktionen einzuführen, die auch für die Fehlerbehandlung verwendet werden könnten, anstelle eines fehlerspezifischen try (siehe Ausgabe Nr. 32473). @josharian hat eine Möglichkeit wiederbelebt, die wir letztes Jahr auf der GopherCon besprochen haben, wo try bei einem Fehler nicht zurückkehrt, sondern stattdessen (mit einem goto ) zu einem Label namens error springt (alternativ , try könnte den Namen eines Ziellabels annehmen).

5) Zum Thema try als Schlüsselwort sind zwei Gedankengänge aufgetaucht. @brynbellomy schlug eine Version vor, die alternativ einen Handler angeben könnte:

a, b := try f()
a, b := try f() else err { /* handle error */ }

@thepudds geht noch einen Schritt weiter und schlägt try am Anfang der Zeile vor, wodurch try die gleiche Sichtbarkeit erhält wie return :

try a, b := f()

Beides könnte mit defer funktionieren.

@griesemer

Danke für den Verweis auf @mikeschinkel #32473, es hat viel gemeinsam.

bezüglich

Aber es scheint, dass in Ihrem Vorschlag noch ein Versuch erforderlich ist
Obwohl mein Vorschlag mit "jedem" Handler und nicht mit einem reservierten "build in/keyword/expression" implementiert werden kann, halte ich "try()" nicht für eine schlechte Idee (und habe es daher nicht abgelehnt), ich versuche es um es zu "erweitern" - damit es mehr Vorteile zeigt, viele erwarteten "einmal 2.0 wird eingeführt"

Ich denke, das könnte auch die Quelle der "gemischten Schwingungen" sein, die Sie in Ihrer letzten Zusammenfassung gemeldet haben - es ist nicht "try() verbessert die Fehlerbehandlung nicht" - sicher, es tut es, es "wartet darauf, dass Go 3.0 einen anderen schwerwiegenden Fehler behebt Umgang mit Schmerzen", wie oben angegeben, sieht zu lang aus :)

Ich führe eine Umfrage zu „Problemen bei der Fehlerbehandlung“ durch (und stelle fest, dass einige der Probleme lediglich „Ich verwende keine guten Praktiken“ sind, während ich mir bei einigen nicht einmal vorgestellt habe, dass Leute (meistens aus anderen Sprachen) dies tun möchten – von cool zu WTF).

Ich hoffe, ich kann bald einige interessante Ergebnisse teilen.

Zum Schluss -Danke für die tolle Arbeit und Geduld!

Betrachtet man einfach die Länge der derzeit vorgeschlagenen Syntax im Vergleich zu dem, was jetzt verfügbar ist, ist der Fall, in dem der Fehler nur zurückgegeben werden muss, ohne ihn zu behandeln oder zu dekorieren, der Fall, in dem der größte Komfort erzielt wird. Ein Beispiel mit meiner bisherigen Lieblingssyntax:

try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f;  err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f;  err != nil { return err; }

Also, anders als ich vorher dachte, reicht es vielleicht einfach, go fmt zu ändern, zumindest für den dekorierten/behandelten Fehlerfall. Während für das einfache Weitergeben des Fehlerfalls so etwas wie Versuch als syntaktischer Zucker für diesen sehr häufigen Anwendungsfall wünschenswert sein könnte.

In Bezug auf try else denke ich, dass bedingte Fehlerfunktionen wie fmt.HandleErrorf (Bearbeiten: Ich gehe davon aus, dass es nil zurückgibt, wenn die Eingabe nil ist) im ersten Kommentar gut funktionieren, also fügen Sie else hinzu ist unnötig.

a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)

Wie viele andere hier ziehe ich es vor, dass try eine Anweisung und kein Ausdruck ist, vor allem, weil ein Ausdruck, der den Kontrollfluss verändert, Go völlig fremd ist. Da dies kein Ausdruck ist, sollte es am Anfang der Zeile stehen.

Ich stimme auch @daved zu, dass der Name nicht angemessen ist. Schließlich versuchen wir hier, eine geschützte Zuweisung zu erreichen. Warum also nicht wie in Swift guard verwenden und die Klausel else optional machen? Etwas wie

GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [  "else" Identifier Block ] .

wobei Identifier der Name der Fehlervariablen ist, die im folgenden Block gebunden ist. Ohne else -Klausel kehren Sie einfach von der aktuellen Funktion zurück (und verwenden Sie einen Defer-Handler, um bei Bedarf Fehler zu dekorieren).

Anfangs mochte ich eine else Klausel nicht, weil es nur syntaktischer Zucker um die übliche Zuweisung herum ist, gefolgt von if err != nil , aber nachdem ich einige der Beispiele gesehen habe, macht es einfach Sinn: guard zu verwenden

BEARBEITEN: Einige schlugen vor, Dinge wie catch zu verwenden, um irgendwie verschiedene Fehlerhandler anzugeben. Ich finde else semantisch genauso brauchbar und es ist bereits in der Sprache enthalten.

Ich mag zwar die try-else-Anweisung, aber wie wäre es mit dieser Syntax?

a, b, (err) := func() else { return err }

Ausdruck try - else ist ein ternärer Operator.

a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`

Die Anweisung try - else ist eine if -Anweisung.

try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }

Das eingebaute try mit einem optionalen Handler kann entweder mit einer Hilfsfunktion (unten) oder ohne Verwendung try erreicht werden (nicht abgebildet, wir alle wissen, wie das aussieht).

a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
  x, err := f()
  if err != nil {
    try(decorate(err))
  }
  return x, nil
}

Alle drei reduzieren den Textbaustein und helfen, den Umfang der Fehler einzudämmen.

Es gibt die meisten Einsparungen für eingebaute try , aber das hat die Probleme, die im Designdokument erwähnt werden.

Für die Anweisung try - else bietet sie einen Vorteil gegenüber der Verwendung if anstelle von try . Aber der Vorteil ist so marginal, dass ich es schwer finde, ihn zu rechtfertigen, obwohl ich ihn mag.

Alle drei gehen davon aus, dass es üblich ist, für einzelne Fehler eine spezielle Fehlerbehandlung zu benötigen.

Die Behandlung aller Fehler kann in defer gleich behandelt werden. Wenn in jedem else -Block dieselbe Fehlerbehandlung durchgeführt wird, wiederholt sich das etwas:

func printSum(a, b string) error {
  try x := strconv.Atoi(a) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  try y := strconv.Atoi(b) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  fmt.Println("result:", x + y)
  return nil
}

Ich weiß sicherlich, dass es Zeiten gibt, in denen ein bestimmter Fehler eine besondere Behandlung erfordert. Das sind die Fälle, die mir in Erinnerung geblieben sind. Aber wenn das nur passiert, sagen wir, 1 von 100 Mal, wäre es nicht besser, try einfach zu halten und try in solchen Situationen einfach nicht zu verwenden? Wenn es andererseits eher 1 von 10 Mal ist, erscheint das Hinzufügen else /handler sinnvoller.

Es wäre jedoch interessant zu sehen, wie oft try ohne else /Handler vs. try mit else /Handler nützlich wäre das ist nicht einfach Daten zu sammeln.

Ich möchte den letzten Kommentar von @jimmyfrasche erweitern.

Das Ziel dieses Vorschlags ist es, die Boilerplate zu reduzieren

    a, b, err := f()
    if err != nil {
        return nil, err
    }

Dieser Code ist einfach zu lesen. Eine Erweiterung der Sprache lohnt sich nur, wenn wir eine deutliche Reduzierung der Boilerplate erreichen können. Wenn ich sowas sehe

    try a, b := f() else err { return nil, err }

Ich kann mich des Gefühls nicht erwehren, dass wir nicht so viel sparen. Wir sparen drei Zeilen ein, was gut ist, aber nach meiner Zählung reduzieren wir von 56 auf 46 Zeichen. Das ist nicht viel. Vergleichen mit

    a, b := try(f())

wodurch von 56 auf 18 Zeichen gekürzt wird, eine viel bedeutendere Reduzierung. Und während die try -Aussage die potenzielle Änderung des Kontrollflusses deutlicher macht, finde ich die Aussage insgesamt nicht lesbarer. Auf der positiven Seite macht es die try -Anweisung jedoch einfacher, den Fehler zu kommentieren.

Wie auch immer, mein Punkt ist: Wenn wir etwas ändern, sollte es die Boilerplate erheblich reduzieren oder deutlich besser lesbar sein. Letzteres ist ziemlich schwierig, daher muss jede Änderung wirklich an ersterem arbeiten. Wenn wir nur eine geringfügige Reduzierung der Boilerplate erhalten, lohnt es sich meiner Meinung nach nicht.

Wie andere möchte ich mich bei @crawshaw für die Beispiele bedanken.

Wenn Sie diese Beispiele lesen, ermutige ich die Leute, eine Denkweise anzunehmen, in der Sie sich keine Gedanken über den Kontrollfluss aufgrund der try -Funktion machen. Ich glaube, vielleicht zu Unrecht, dass dieser Kontrollfluss für Menschen, die die Sprache beherrschen, schnell zur zweiten Natur werden wird. Im Normalfall glaube ich, dass die Leute einfach aufhören werden, sich Gedanken darüber zu machen, was im Fehlerfall passiert. Versuchen Sie, diese Beispiele zu lesen, während Sie über try glasieren, so wie Sie bereits über if err != nil { return err } glasieren.

Nachdem ich hier alles durchgelesen habe und nach weiterem Nachdenken, bin ich mir nicht sicher, ob ich den Versuch auch nur als eine Aussage sehe, die es wert ist, hinzugefügt zu werden.

  1. Der Grund dafür scheint die Reduzierung des Boilerplate-Codes für die Fehlerbehandlung zu sein. IMHO "entrümpelt" es den Code, aber es beseitigt nicht wirklich die Komplexität; es verdeckt es nur. Dies scheint kein stark genuger Grund zu sein. Das „Geh"Syntax schön eingefangen, wenn ich einen gleichzeitigen Thread starte. Ich bekomme hier nicht dieses "Aha!"-Gefühl. Es fühlt sich nicht richtig an. Das Kosten-Nutzen-Verhältnis ist nicht groß genug.

  2. sein Name spiegelt nicht seine Funktion wider. In seiner einfachsten Form ist das folgende: "Wenn eine Funktion einen Fehler zurückgibt, kehre vom Aufrufer mit einem Fehler zurück", aber das ist zu lang :-) Zumindest wird ein anderer Name benötigt.

  3. Mit der impliziten Rückgabe von try bei Fehlern fühlt es sich an, als würde Go nur widerwillig auf die Ausnahmebehandlung zurückgreifen. Das heißt, wenn A be in einem Try Guard aufruft und B C in einem Try Guard aufruft und C D in einem Try Guard aufruft, wenn D tatsächlich einen Fehler zurückgibt, haben Sie ein nicht lokales goto verursacht. Es fühlt sich zu "magisch" an.

  4. und doch glaube ich, dass ein besserer Weg möglich sein könnte. Wenn Sie Jetzt versuchen auswählen, wird diese Option deaktiviert.

@ianlancetaylor
Wenn ich den Vorschlag "Versuche es sonst" richtig verstehe, scheint es, dass der Block else optional und für die vom Benutzer bereitgestellte Behandlung reserviert ist. In Ihrem Beispiel try a, b := f() else err { return nil, err } ist die Klausel else tatsächlich überflüssig, und der gesamte Ausdruck kann einfach als try a, b := f() geschrieben werden

Ich stimme @ianlancetaylor zu,
Lesbarkeit und Boilerplate sind zwei Hauptanliegen und vielleicht der Antrieb dazu
die Go 2.0-Fehlerbehandlung (obwohl ich einige andere wichtige Bedenken hinzufügen kann)

Auch dass die Strömung

a, b, err := f()
if err != nil {
    return nil, err
}

Ist sehr gut lesbar.
Und da glaube ich

if a, b, err := f(); err != nil {
    return nil, err
}

Ist fast so lesbar, hatte aber seinen Umfang "Probleme", vielleicht a

ifErr a, b, err := f() {
    return nil, err
}

Das wäre nur die ; err != nil part, und würde keinen Geltungsbereich erstellen, oder

ähnlich

versuche a, b, ähm := f() {
gib null zurück, äh
}

Behält die zusätzlichen zwei Zeilen bei, ist aber immer noch lesbar.

Am Di, 11. Juni 2019, 20:19 Uhr Dmitriy Matrenichev, [email protected]
schrieb:

@ianlancetaylor https://github.com/ianlancetaylor
Wenn ich den Vorschlag "Versuche es sonst" richtig verstehe, scheint es, als würde es sonst blockieren
ist optional und für die vom Benutzer bereitgestellte Handhabung reserviert. In deinem Beispiel
try a, b := f() else err { return nil, err } die else-Klausel ist tatsächlich
überflüssig, und der gesamte Ausdruck kann einfach als try a, b := geschrieben werden
F()


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-5009,39404
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.

@ianlancetaylor

Wie auch immer, mein Punkt ist: Wenn wir etwas ändern, sollte es die Boilerplate erheblich reduzieren oder deutlich besser lesbar sein. Letzteres ist ziemlich schwierig, daher muss jede Änderung wirklich an ersterem arbeiten. Wenn wir nur eine geringfügige Reduzierung der Boilerplate erhalten, lohnt es sich meiner Meinung nach nicht.

Einverstanden, und wenn man bedenkt, dass ein else nur syntaktischer Zucker (mit einer seltsamen Syntax!) wäre, der sehr wahrscheinlich nur selten verwendet wird, kümmere ich mich nicht viel darum. Ich würde es trotzdem vorziehen, wenn try eine Aussage wäre.

@ianlancetaylor In Anlehnung an @DmitriyMV wäre der Block else optional. Lassen Sie mich ein Beispiel anfügen, das beides veranschaulicht (und in Bezug auf den relativen Anteil von behandelten vs. nicht behandelten try -Blöcken in echtem Code nicht allzu weit daneben zu liegen scheint):

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    headRef, err := r.Head()
    if err != nil {
        return err
    }

    parentObjOne, err := headRef.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentCommitOne, err := parentObjOne.AsCommit()
    if err != nil {
        return err
    }

    parentCommitTwo, err := parentObjTwo.AsCommit()
    if err != nil {
        return err
    }

    treeOid, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := r.LookupTree(treeOid)
    if err != nil {
        return err
    }

    remoteBranchName, err := remoteBranch.Name()
    if err != nil {
        return err
    }

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    if err != nil {
        return err
    }
    return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()
    try userName, userEmail := r.UserIdentityFromConfig() else err {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return nil
}

Das Muster try / else spart zwar nicht viele Zeichen gegenüber dem zusammengesetzten if , aber es tut es:

  • Vereinheitlichen Sie die Fehlerbehandlungssyntax mit dem unbehandelten try
  • machen auf einen Blick deutlich, dass ein Bedingungsblock eine Fehlerbedingung behandelt
  • Geben Sie uns die Möglichkeit, die Scoping-Verrücktheit zu reduzieren, unter der die Verbindungen if leiden

Nicht gehandhabte try werden jedoch wahrscheinlich am häufigsten vorkommen.

@ianlancetaylor

Versuchen Sie, diese Beispiele zu lesen, während Sie über try glasieren, so wie Sie es bereits glasiert haben, wenn err != nil { return err }.

Ich halte das nicht für möglich/gleichwertig. Fehlen, dass ein Versuch in einer überfüllten Zeile vorhanden ist oder was genau umbrochen wird oder dass es mehrere Instanzen in einer Zeile gibt ... Dies ist nicht dasselbe wie das einfache/schnelle Markieren eines Rückkehrpunkts und sich nicht um die darin enthaltenen Besonderheiten zu kümmern.

@ianlancetaylor

Wenn ich ein Stoppschild sehe, erkenne ich es eher an Form und Farbe als daran, das darauf gedruckte Wort zu lesen und über seine tieferen Implikationen nachzudenken.

Meine Augen mögen über if err != nil { return err } werden, aber gleichzeitig registriert es sich immer noch – klar und sofort.

Was ich an der try -Statement-Variante mag, ist, dass sie die Boilerplate reduziert, aber auf eine Weise, die sowohl leicht zu übersehen als auch schwer zu übersehen ist.

Es kann hier oder da eine zusätzliche Zeile bedeuten, aber das sind immer noch weniger Zeilen als der Status quo.

@brynbellomy

  1. Wie bieten Sie Funktionen an, die mehrere Werte zurückgeben, wie:
    func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) (Hash, Fehler) {
  2. Wie würden Sie die richtige Zeile verfolgen, die den Fehler zurückgegeben hat?
  3. Wenn ich das Bereichsproblem verwerfe (das auf andere Weise gelöst werden kann), bin ich mir nicht sicher
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {

    if headRef, err := r.Head(); err != nil {
        return err
    } else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
        return err
    } else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
        return err
    } ...

ist in Bezug auf die Lesbarkeit nicht so unterschiedlich, aber (oder fmt.Errorf("Fehler beim Abrufen des Kopfes: %s", err.Error() ) ermöglicht es Ihnen, zusätzliche Daten einfach zu ändern und anzugeben.

Was noch nörgelt ist das

  1. nachprüfen müssen ; ähm != null
  2. Zurückgeben des Fehlers wie er ist, wenn wir die zusätzlichen Informationen nicht geben möchten - was in einigen Fällen keine gute Praxis ist, da Sie von der aufgerufenen Funktion abhängig sind, um einen "guten" Fehler widerzuspiegeln, der darauf hinweist, "was schief gelaufen ist ", in Fällen von file.Open , close , Remove , Db-Funktionen usw. können viele der Funktionsaufrufe denselben Fehler zurückgeben (wir können argumentieren, ob das bedeutet, dass der Entwickler, der den Fehler geschrieben hat, gute Arbeit geleistet hat oder nicht ... aber es passiert) - und dann - Sie haben einen Fehler, protokollieren Sie ihn wahrscheinlich von der Funktion, die aufgerufen hat
    "createMergeCommit", kann es aber nicht auf die genaue Zeile zurückverfolgen, in der es aufgetreten ist.

Entschuldigung, wenn jemand so etwas schon gepostet hat (es gibt viele gute Ideen :P ) Wie wäre es mit dieser alternativen Syntax:

fail := func(err error) error {
  log.Print("unexpected error", err)
  return err
}

a, b, err := f1()          // normal
c, d := f2() -> throw      // calls throw(err)
e, f := f3() -> panic      // calls panic(err)
g, h := f4() -> t.Error    // calls t.Error(err)
i, j := f5() -> fail       // calls fail(err)

das heißt, Sie haben ein -> handler rechts von einem Funktionsaufruf, der aufgerufen wird, wenn das zurückgegebene err != nil ist. Der Handler ist eine beliebige Funktion, die einen Fehler als einzelnes Argument akzeptiert und optional einen Fehler zurückgibt (dh func(error) oder func(error) error ). Wenn der Handler einen Null-Fehler zurückgibt, wird die Funktion fortgesetzt, andernfalls wird der Fehler zurückgegeben.

a := b() -> handler entspricht also:

a, err := b()
if err != nil {
  if herr := handler(err); herr != nil {
    return herr
  }
}

Nun, als Abkürzung könnten Sie ein eingebautes try (oder ein Schlüsselwort oder einen ?= -Operator oder was auch immer) unterstützen, das für a := b() -> throw steht, sodass Sie so etwas schreiben könnten:

func() error {
  a, b := try(f1())
  c, d := try(f2())
  e, f := try(f3())
  ...
  return nil
}() -> panic // or throw/fail/whatever

Persönlich finde ich einen ?= -Operator einfacher zu lesen als ein Try-Keyword/Builtin:

func() error {
  a, b ?= f1()
  c, d ?= f2()
  e, f ?= f3()
  ...
  return nil
}() -> panic

Hinweis : Hier verwende ich throw als Platzhalter für eine eingebaute Funktion, die den Fehler an den Aufrufer zurückgibt.

Ich habe die Vorschläge zur Fehlerbehandlung bisher nicht kommentiert, weil ich im Allgemeinen dafür bin und mir gefällt, wie sie in diese Richtung gehen. Sowohl die im Vorschlag definierte try-Funktion als auch die von @thepudds vorgeschlagene try-Anweisung scheinen sinnvolle Ergänzungen der Sprache zu sein. Ich bin zuversichtlich, dass alles, was dem Go-Team einfällt, gut sein wird.

Ich möchte etwas ansprechen, das ich als geringfügiges Problem mit der Art und Weise betrachte, wie try im Vorschlag definiert ist und wie es sich auf zukünftige Erweiterungen auswirken könnte.

Try ist als eine Funktion definiert, die eine variable Anzahl von Argumenten akzeptiert.

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Das Übergeben des Ergebnisses eines Funktionsaufrufs an try wie in try(f()) funktioniert implizit aufgrund der Art und Weise, wie mehrere Rückgabewerte in Go funktionieren.

Nach meiner Lektüre des Vorschlags sind die folgenden Ausschnitte sowohl gültig als auch semantisch äquivalent.

a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)

Der Vorschlag wirft auch die Möglichkeit auf, try mit zusätzlichen Argumenten zu erweitern.

Wenn wir später feststellen, dass es eine gute Idee ist, eine explizit bereitgestellte Fehlerbehandlungsfunktion oder einen anderen zusätzlichen Parameter für diese Angelegenheit zu haben, ist es trivial möglich, dieses zusätzliche Argument an einen try-Aufruf zu übergeben.

Angenommen, wir möchten ein Handler-Argument hinzufügen. Es kann entweder am Anfang oder am Ende der Argumentliste stehen.

var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)

Es an den Anfang zu stellen, funktioniert nicht, weil (angesichts der obigen Semantik) try nicht in der Lage wäre, zwischen einem expliziten Handler-Argument und einer Funktion zu unterscheiden, die einen Handler zurückgibt.

func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())

Es ans Ende zu setzen, würde wahrscheinlich funktionieren, aber dann wäre try einzigartig in der Sprache, da es die einzige Funktion mit einem varargs-Parameter am Anfang der Argumentliste wäre.

Keines dieser Probleme ist ein Showstopper, aber sie lassen try unvereinbar mit dem Rest der Sprache erscheinen, und daher bin ich mir nicht sicher, ob try in Zukunft so einfach zu erweitern wäre Vorschlag Staaten.

@magisch

Einen Handler zu haben ist vielleicht mächtig:
Ich habe Sie bereits h erklärt,

du kannst

var h handler
a, b, h = f()

oder

a, b, h.err = f()

wenn es eine Funktion ist:

h:= handler(err error){
 log(...)
 return ....
} 

Dann gab es einen Vorschlag dazu

a, b, h(err) = f()

Alle können den Handler aufrufen
Und Sie können auch einen Handler "auswählen", der den Fehler zurückgibt oder nur erfasst (Fortfahren/Unterbrechen/Rückkehr), wie einige vorgeschlagen haben.

Und damit ist das Varargs-Problem weg.

Eine Alternative zu else Vorschlag von @brynbellomy:

a, b := try f() else err { /* handle error */ }

könnte sein, eine Dekorationsfunktion unmittelbar nach dem anderen zu unterstützen:

decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

try a, b := f() else decorate
try c, d := g() else decorate

Und vielleicht auch einige Hilfsfunktionen in der Art von:

decorate := fmt.DecorateErrorf("foo failed")

Die Dekorationsfunktion könnte die Signatur func(error) error haben und bei Vorhandensein eines Fehlers von try aufgerufen werden, kurz bevor try von der zugeordneten Funktion, die gerade versucht wird, zurückkehrt.

Das wäre im Geiste einer der früheren „Design-Iterationen“ aus dem Vorschlagsdokument ähnlich:

f := try(os.Open(filename), handler)              // handler will be called in error case

Wenn jemand etwas Komplexeres oder einen Block von Anweisungen haben möchte, könnte er stattdessen if verwenden (so wie er es heute kann).

Allerdings hat die visuelle Ausrichtung von try im Beispiel von @brynbellomy in https://github.com/golang/go/issues/32437#issuecomment -500949780 etwas Schönes.

All dies könnte immer noch mit defer funktionieren, wenn dieser Ansatz für eine einheitliche Fehlerdekoration gewählt wird (oder es könnte sogar theoretisch eine alternative Form der Registrierung einer Dekorationsfunktion geben, aber das ist ein separater Punkt).

Auf jeden Fall bin ich mir nicht sicher, was hier am besten ist, wollte aber eine andere Option explizit machen.

Hier ist das Beispiel von @brynbellomy , das mit der Funktion try neu geschrieben wurde und einen Block var verwendet, um die schöne Ausrichtung beizubehalten, auf die @thepudds in https://github.com/golang/go/issues hingewiesen hat

package main

import (
    "fmt"
    "time"
)

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    var (
        headRef          = try(r.Head())
        parentObjOne     = try(headRef.Peel(git.ObjectCommit))
        parentObjTwo     = try(remoteBranch.Reference.Peel(git.ObjectCommit))
        parentCommitOne  = try(parentObjOne.AsCommit())
        parentCommitTwo  = try(parentObjTwo.AsCommit())
        treeOid          = try(index.WriteTree())
        tree             = try(r.LookupTree(treeOid))
        remoteBranchName = try(remoteBranch.Name())
    )

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return err
}

Es ist so prägnant wie die try -Anweisungsversion, und ich würde argumentieren, dass es genauso lesbar ist. Da try ein Ausdruck ist, könnten einige dieser Zwischenvariablen auf Kosten der Lesbarkeit eliminiert werden, aber das scheint eher eine Frage des Stils als alles andere zu sein.

Es wirft jedoch die Frage auf, wie try in einem var -Block funktioniert. Ich gehe davon aus, dass jede Zeile der var als separate Anweisung zählt und nicht der gesamte Block eine einzelne Anweisung ist, was die Reihenfolge betrifft, was wann zugewiesen wird.

Es wäre gut, wenn der "try"-Vorschlag explizit die Konsequenzen für Tools wie cmd/cover nennen würde, die Testabdeckungsstatistiken mit naiver Anweisungszählung annähern. Ich mache mir Sorgen, dass der unsichtbare Fehlerkontrollfluss zu einer Unterzählung führen könnte.

@thepudds

versuche a, b := f() sonst dekoriere

Vielleicht ist es ein zu tiefes Brennen in meinen Gehirnzellen, aber das trifft mich zu sehr

try a, b := f() ;catch(decorate)

und ein rutschiger Abhang zu a

a, b := f()
catch(decorate)

Ich denke, Sie können sehen, wohin das führt, und für mich vergleichen

    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()

mit

    try ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    )

(oder sogar ein Haken am Ende)
Die zweite ist besser lesbar, betont aber die Tatsache, dass die folgenden Funktionen 2 Variablen zurückgeben, und wir verwerfen auf magische Weise eine und sammeln sie in einem "magisch zurückgegebenen Fehler" .

    try(err) ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    ); err!=nil {
      //handle the err
    }

setzt zumindest die zurückzugebende Variable explizit und lässt mich sie innerhalb der Funktion handhaben, wann immer ich will.

Ich werfe nur einen bestimmten Kommentar ein, da ich niemanden explizit gesehen habe, der ihn ausdrücklich erwähnt hat, insbesondere über die Änderung gofmt , um die folgende einzeilige Formatierung oder eine beliebige Variante zu unterstützen:

if f() { return nil, err }

Bitte nicht. Wenn wir eine einzelne Zeile if wollen, dann machen Sie bitte eine einzelne Zeile if , z.

if f() then return nil, err

Aber bitte, bitte, bitte nehmen Sie keinen Syntaxsalat an, indem Sie die Zeilenumbrüche entfernen, die es einfacher machen, Code zu lesen, der geschweifte Klammern verwendet.

Ich möchte ein paar Dinge hervorheben, die in der Hitze der Diskussion vielleicht vergessen wurden:

1) Der ganze Sinn dieses Vorschlags besteht darin, die allgemeine Fehlerbehandlung in den Hintergrund treten zu lassen - die Fehlerbehandlung sollte den Code nicht dominieren. Sollte aber trotzdem explizit sein. Alle alternativen Vorschläge, die die Fehlerbehandlung noch mehr herausragen lassen, gehen am Ziel vorbei. Wie @ianlancetaylor bereits sagte, wenn diese alternativen Vorschläge die Menge an Textbausteinen nicht wesentlich reduzieren, können wir einfach bei den if -Anweisungen bleiben. (Und die Bitte, Boilerplate zu reduzieren, kommt von Ihnen, der Go-Community.)

2) Eine der Beschwerden über den aktuellen Vorschlag ist die Notwendigkeit, das Fehlerergebnis zu benennen, um darauf zugreifen zu können. Jeder Alternativvorschlag wird das gleiche Problem haben, es sei denn, die Alternative führt eine zusätzliche Syntax ein, dh mehr Textbausteine ​​(wie etwa ... else err { ... } und dergleichen), um diese Variable explizit zu benennen. Aber was interessant ist: Wenn wir uns nicht darum kümmern, einen Fehler zu dekorieren und die Ergebnisparameter nicht zu benennen, aber trotzdem ein explizites return benötigen, weil es eine Art expliziten Handler gibt, diese return -Anweisung müssen alle (normalerweise null) Ergebniswerte aufzählen, da eine nackte Rückgabe in diesem Fall nicht zulässig ist. Besonders wenn eine Funktion viele Fehler zurückgibt, ohne den Fehler zu schmücken, fügen diese expliziten Rückgaben ( return nil, err usw.) der Boilerplate hinzu. Der aktuelle Vorschlag und jede Alternative, die kein explizites return erfordert, beseitigt dies. Andererseits, wenn man den Fehler schmücken will, _erfordert_ der aktuelle Vorschlag, dass man das Fehlerergebnis (und damit alle anderen Ergebnisse) benennt, um Zugriff auf den Fehlerwert zu bekommen. Das hat den netten Nebeneffekt, dass man in einem expliziten Handler eine nackte Rückgabe verwenden kann und nicht alle anderen Ergebniswerte wiederholen muss. (Ich weiß, dass nackte Renditen einige starke Gefühle haben, aber die Realität ist, dass es ein echtes Ärgernis ist, alle anderen (normalerweise null) Ergebniswerte aufzählen zu müssen, wenn wir uns nur um das Fehlerergebnis kümmern - es fügt dem nichts hinzu Verständnis des Codes). Mit anderen Worten, das Fehlerergebnis so benennen zu müssen, dass es dekoriert werden kann, ermöglicht eine weitere Reduzierung der Boilerplate.

@magical Danke für den Hinweis . Das Gleiche ist mir kurz nach dem Posten des Vorschlags aufgefallen (aber nicht zur Sprache gebracht, um keine weitere Verwirrung zu stiften). Sie haben Recht, dass try so wie es ist nicht verlängert werden konnte. Glücklicherweise ist die Lösung einfach genug. (Zufälligerweise hatten unsere früheren internen Vorschläge dieses Problem nicht - es wurde eingeführt, als ich unsere endgültige Version für die Veröffentlichung neu schrieb und versuchte, try zu vereinfachen, um den bestehenden Parameterübergaberegeln besser zu entsprechen. Es schien wie eine nette - aber wie sich herausstellt, fehlerhaft und größtenteils nutzlos - profitieren Sie davon, try(a, b, c, handle) schreiben zu können.)

Eine frühere Version von try definierte es ungefähr wie folgt: try(expr, handler) nimmt einen (oder vielleicht zwei) Ausdruck als Argument, wobei der erste Ausdruck mehrwertig sein kann (kann nur passieren, wenn der Ausdruck ist ein Funktionsaufruf). Der letzte Wert dieses (möglicherweise mehrwertigen) Ausdrucks muss vom Typ error sein, und dieser Wert wird gegen nil getestet. (etc. - den Rest können Sie sich vorstellen).

Wie auch immer, der Punkt ist, dass try syntaktisch nur einen oder vielleicht zwei Ausdrücke akzeptiert. (Aber es ist etwas schwieriger, die Semantik von try zu beschreiben.) Die Konsequenz wäre dieser Code wie:

a, b := try(u, v, err)

wäre nicht mehr erlaubt. Aber es gibt wenig Grund dafür, dass dies überhaupt funktioniert: In den meisten Fällen (es sei denn, a und b sind benannte Ergebnisse) könnte dieser Code - falls er aus irgendeinem Grund wichtig ist - leicht umgeschrieben werden

a, b := u, v  // we don't care if the assignment happens in case of an error
try(err)

(oder verwenden Sie bei Bedarf eine if -Anweisung). Aber auch das scheint unwichtig zu sein.

diese return-Anweisung muss alle (normalerweise null) Ergebniswerte aufzählen, da eine nackte Rückgabe in diesem Fall nicht zulässig ist

Eine nackte Rückkehr ist nicht erlaubt, aber ein Versuch wäre es. Eine Sache, die ich an try (entweder als Funktion oder als Anweisung) mag, ist, dass ich nicht mehr darüber nachdenken muss, wie man fehlerfreie Werte setzt, wenn ein Fehler zurückgegeben wird, ich werde einfach try verwenden.

@griesemer Danke für die Erklärung. Zu dem Schluss bin ich auch gekommen.

Ein kurzer Kommentar zu try als Statement: Wie ich denke, ist im Beispiel in https://github.com/golang/go/issues/32437#issuecomment -501035322 zu sehen, das try begräbt die Lede. Code wird zu einer Reihe von try -Anweisungen, die verschleiern, was der Code tatsächlich tut.

Vorhandener Code kann eine neu deklarierte Fehlervariable nach dem Block if err != nil wiederverwenden. Das Ausblenden der Variablen würde das brechen, und das Hinzufügen einer benannten Rückgabevariablen zur Funktionssignatur wird es nicht immer beheben.

Vielleicht ist es am besten, die Fehlerdeklaration/-zuweisung so zu lassen, wie sie ist, und eine einzeilige Fehlerbehandlungs-stmt zu finden.

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, hname err             // handler invocation without parens
on err, ignore err            // optional ignore handler logs error if defined

if err, return err            // alternatively, use if

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err }
   fmt.Println(clr.name, err)
}

Ein Unterausdruck try könnte panisch werden, was bedeutet, dass niemals ein Fehler erwartet wird. Eine Variante davon könnte jeden Fehler ignorieren.

f(try g()) // panic on error
f(try_ g()) // ignore any error

Der ganze Sinn dieses Vorschlags besteht darin, die allgemeine Fehlerbehandlung in den Hintergrund treten zu lassen – die Fehlerbehandlung sollte den Code nicht dominieren. Sollte aber trotzdem explizit sein.

Ich mag die Idee, dass die Kommentare try als Aussage auflisten. Es ist explizit, immer noch leicht zu beschönigen (da es eine feste Länge hat), aber nicht _so_ leicht zu beschönigen (da es immer an der gleichen Stelle ist), dass sie in einer überfüllten Zeile versteckt werden können. Es kann auch wie zuvor erwähnt mit defer fmt.HandleErrorf(...) kombiniert werden, hat jedoch die Gefahr, benannte Parameter zu missbrauchen, um Fehler zu umschließen (was mir immer noch wie ein cleverer Hack erscheint. Clevere Hacks sind schlecht.)

Einer der Gründe, warum ich try als Ausdruck nicht mochte, ist, dass es entweder zu leicht zu beschönigen ist oder nicht leicht genug, um es zu beschönigen. Nehmen Sie die folgenden zwei Beispiele:

Versuchen Sie es als Ausdruck

// Too hidden, it's in a crowded function with many symbols that complicate the function.

f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)

// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.

fullContents := try(io.CopyN(
    os.Stdout,
    try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
    try(strconv.Atoi(try(buf.ReadString("\n")))),
))

Versuchen Sie es als Aussage

// easy to see while still not being too verbose

try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)

// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.

try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)

try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)

Imbiss

Dieser Code ist definitiv erfunden, das gebe ich zu. Aber worauf ich hinaus will ist, dass try im Allgemeinen als Ausdruck nicht gut funktioniert in:

  1. Die Mitte von überfüllten Ausdrücken, die nicht viel Fehlerprüfung erfordern
  2. Relativ einfache mehrzeilige Anweisungen, die viel Fehlerprüfung erfordern

Ich stimme jedoch @ianlancetaylor zu, dass der Anfang jeder Zeile mit try dem wichtigen Teil jeder Anweisung (der definierten Variablen oder der ausgeführten Funktion) im Wege zu stehen scheint. Ich denke jedoch, weil es sich an derselben Stelle befindet und eine feste Breite hat, ist es viel einfacher, es zu beschönigen, während es immer noch bemerkt wird. Allerdings sind die Augen jedes Menschen anders.

Ich denke auch, dass die Förderung cleverer Einzeiler im Code im Allgemeinen nur eine schlechte Idee ist. Ich bin überrascht, dass ich einen so mächtigen Einzeiler wie in meinem ersten Beispiel erstellen konnte, es ist ein Ausschnitt, der seine eigene vollständige Funktion verdient, weil er so viel leistet – aber er passt in eine Zeile, wenn ich ihn nicht reduziert hätte um der Lesbarkeit willen zu vervielfachen. Alles in einer Zeile:

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Es liest einen Port aus einem *bufio.Reader , startet eine TCP-Verbindung und kopiert eine Anzahl von Bytes, die von demselben *bufio.Reader angegeben werden, nach stdout . Alles mit Fehlerbehandlung. Für eine Sprache mit so strengen Codierungskonventionen denke ich nicht, dass dies überhaupt erlaubt sein sollte. Ich denke, gofmt könnte dabei helfen.

Für eine Sprache mit so strengen Codierungskonventionen denke ich nicht, dass dies überhaupt erlaubt sein sollte.

Es ist möglich, abscheulichen Code in Go zu schreiben. Es ist sogar möglich, es schrecklich zu formatieren; es gibt nur starke Normen und Werkzeuge dagegen. Go hat sogar goto .

Bei Codeüberprüfungen bitte ich die Leute manchmal, komplizierte Ausdrücke in mehrere Anweisungen mit nützlichen Zwischennamen aufzuteilen. Ich würde aus dem gleichen Grund etwas Ähnliches für tief verschachtelte try s machen.

Das ist alles, um zu sagen: Lassen Sie uns nicht zu sehr versuchen, schlechten Code auf Kosten der Sprachverzerrung zu verbieten. Wir haben andere Mechanismen, um Code sauber zu halten, die besser für etwas geeignet sind, das im Wesentlichen menschliches Urteilsvermögen von Fall zu Fall erfordert.

Es ist möglich, abscheulichen Code in Go zu schreiben. Es ist sogar möglich, es schrecklich zu formatieren; es gibt nur starke Normen und Werkzeuge dagegen. Go hat sogar goto.

Bei Codeüberprüfungen bitte ich die Leute manchmal, komplizierte Ausdrücke in mehrere Anweisungen mit nützlichen Zwischennamen aufzuteilen. Ich würde aus dem gleichen Grund etwas Ähnliches für tief verschachtelte Versuche tun.

Das ist alles, um zu sagen: Lassen Sie uns nicht zu sehr versuchen, schlechten Code auf Kosten der Sprachverzerrung zu verbieten. Wir haben andere Mechanismen, um Code sauber zu halten, die besser für etwas geeignet sind, das im Wesentlichen menschliches Urteilsvermögen von Fall zu Fall erfordert.

Das ist ein guter Punkt. Wir sollten eine gute Idee nicht verbieten, nur weil sie zur Erstellung von schlechtem Code verwendet werden kann. Ich denke jedoch, dass es eine gute Idee sein könnte, wenn wir eine Alternative haben, die besseren Code fördert. Ich habe bis zu @ianlancetaylors Kommentar nicht viel _gegen_ die rohe Idee hinter try als Aussage (ohne den ganzen else { ... } -Müll) gesehen, aber ich habe es vielleicht gerade verpasst.

Außerdem hat nicht jeder Code-Reviewer, einige Leute (insbesondere in ferner Zukunft) werden ungeprüften Go-Code pflegen müssen. Go als Sprache stellt normalerweise sehr gut sicher, dass fast der gesamte geschriebene Code gut wartbar ist (zumindest nach einem go fmt ), was nicht zu übersehen ist.

Abgesehen davon stehe ich dieser Idee schrecklich kritisch gegenüber, obwohl sie wirklich nicht schrecklich ist.

Try als Anweisung reduziert die Boilerplate erheblich und mehr als try als Ausdruck, wenn wir zulassen, dass sie wie zuvor vorgeschlagen an einem Block von Ausdrücken arbeitet, auch ohne einen else-Block oder eine Fehlerbehandlungsroutine zuzulassen. Damit wird das Beispiel von deandveloper zu:

try (
    name := FileName()
    file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
    port := r.ReadString("\n")
    lengthStr := r.ReadString("\n")
    length := strconv.Atoi(lengthStr)
    con := net.Dial("tcp", "localhost:"+port)
    io.CopyN(os.Stdout, con, length)
)

Wenn das Ziel darin besteht, die if err!= nil {return err} -Boilerplate zu reduzieren, dann denke ich, dass die Anweisung try, die es ermöglicht, einen Codeblock zu nehmen, das größte Potenzial dafür hat, ohne unklar zu werden.

@beoran Warum sollte man es an diesem Punkt überhaupt versuchen? Lassen Sie einfach eine Zuweisung zu, bei der der letzte Fehlerwert fehlt, und lassen Sie sie sich so verhalten, als wäre es eine try-Anweisung (oder ein Funktionsaufruf). Nicht, dass ich es vorschlagen würde, aber es würde die Boilerplate noch mehr reduzieren.

Ich denke, dass die Boilerplate durch diese Var-Blöcke effizient reduziert werden würde, aber ich befürchte, dass dies dazu führen kann, dass eine große Menge Code um eine zusätzliche Ebene eingerückt wird, was unglücklich wäre.

@deanveloper

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Ich muss zugeben, für mich nicht lesbar, ich würde wahrscheinlich das Gefühl haben, ich muss:

fullContents := try(io.CopyN(os.Stdout, 
                               try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
                                     try(strconv.Atoi(try(r.ReadString("\n"))))))

o.ä., zur besseren Lesbarkeit, und dann sind wir wieder zurück mit einem "try" am Anfang jeder Zeile, mit Einrückung.

Nun, ich denke, wir bräuchten immer noch den Versuch der Abwärtskompatibilität und auch, um explizit über eine Rückkehr zu sprechen, die im Block passieren kann. Aber beachten Sie, dass ich nur der Logik folge, die Kesselplatte zu reduzieren und dann zu sehen, wohin uns das führt. Es gibt immer eine Spannung zwischen der Reduzierung von Boilerplate und Klarheit. Ich denke, das Hauptproblem in dieser Ausgabe ist, dass wir alle uneins zu sein scheinen, wo das Gleichgewicht liegen sollte.

Was die Einzüge betrifft, dafür ist go fmt da, also halte ich das persönlich nicht für ein großes Problem.

Ich möchte mich dem Kampf anschließen, um zwei weitere Möglichkeiten zu erwähnen, von denen jede unabhängig ist, also werde ich sie in separaten Beiträgen behalten.

Ich fand den Vorschlag, dass try() (ohne Argumente) definiert werden könnte, um einen Zeiger auf die Fehlerrückgabevariable zurückzugeben, interessant, aber ich war nicht scharf auf diese Art von Wortspiel – es riecht nach Funktionsüberladung , etwas, das Go vermeidet.

Mir gefiel jedoch die allgemeine Idee einer vordefinierten Kennung, die sich auf den lokalen Fehlerwert bezieht.

Wie wäre es also, wenn Sie den Bezeichner err selbst als Alias ​​für die Fehlerrückgabevariable vordefinieren? Das wäre also gültig:

func foo() error {
    defer handleError(&err, etc)
    try(something())
}

Es wäre funktional identisch mit:

func foo() (err error) {
    defer handleError(&err, etc)
    try(something())
}

Der Bezeichner err würde auf Universumsebene definiert werden, obwohl er als funktionslokaler Alias ​​fungiert, sodass jede Definition auf Paketebene oder funktionslokale Definition von err ihn außer Kraft setzen würde. Das mag gefährlich erscheinen, aber ich habe die 22-Meter-Linien von Go im Go-Korpus gescannt und es ist sehr selten. Es gibt nur 4 unterschiedliche Instanzen err , die als global verwendet werden (alle als Variable, nicht als Typ oder Konstante) - das ist etwas, vet warnen könnte.

Es ist möglich, dass zwei Funktionsfehler-Rückgabevariablen im Geltungsbereich vorhanden sind; In diesem Fall ist es meiner Meinung nach am besten, wenn sich der Compiler über eine Mehrdeutigkeit beschwert und den Benutzer auffordert, die richtige Rückgabevariable explizit zu benennen. Das wäre also ungültig:

func foo() error {
    f := func() error {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

aber du könntest stattdessen auch schreiben:

func foo() error {
    f := func() (err error) {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

Zum Thema try als vordefinierter Bezeichner und nicht als Operator,
Ich habe festgestellt, dass ich zu letzterem tendiere, nachdem ich beim Schreiben wiederholt die Klammern falsch gemacht habe:

try(try(os.Create(filename)).Write(data))

Unter "Warum können wir ? wie Rust nicht verwenden" heißt es in der FAQ:

Bisher haben wir kryptische Abkürzungen oder Symbole in der Sprache vermieden, einschließlich ungewöhnlicher Operatoren wie ?, die mehrdeutige oder nicht offensichtliche Bedeutungen haben.

Ich bin mir nicht ganz sicher, ob das stimmt. Der .() -Operator ist ungewöhnlich, bis Sie Go kennen, ebenso wie die Channel-Operatoren. Wenn wir einen ? -Operator hinzufügen würden, würde er meines Erachtens in Kürze so allgegenwärtig werden, dass er keine nennenswerte Barriere mehr darstellen würde.

Der Rust-Operator ? wird jedoch nach der schließenden Klammer eines Funktionsaufrufs hinzugefügt, und das bedeutet, dass er leicht übersehen werden kann, wenn die Argumentliste lang ist.

Wie wäre es, wenn Sie ?() als Anrufoperator hinzufügen:

Also statt:

x := try(foo(a, b))

du würdest tun:

x := foo?(a, b)

Die Semantik von ?() wäre der vorgeschlagenen eingebauten try sehr ähnlich. Es würde sich wie ein Funktionsaufruf verhalten, außer dass die aufgerufene Funktion oder Methode als letztes Argument einen Fehler zurückgeben muss. Wie bei try gibt die Anweisung ?() ihn zurück, wenn der Fehler nicht null ist.

Es scheint, als ob die Diskussion so konzentriert geworden ist, dass wir uns jetzt um eine Reihe klar definierter und diskutierter Kompromisse drehen. Das ist zumindest für mich ermutigend, da Kompromisse sehr im Sinne dieser Sprache sind.

@ianlancetaylor Ich gebe absolut zu, dass wir am Ende Dutzende von Zeilen mit dem Präfix try haben werden. Ich sehe jedoch nicht, wie das schlimmer ist als Dutzende von Zeilen, denen ein zwei- bis vierzeiliger Bedingungsausdruck nachgestellt wird, der explizit denselben return -Ausdruck angibt. Tatsächlich macht es try (mit else -Klauseln) etwas einfacher zu erkennen, wenn ein Fehlerbehandler etwas Besonderes/Nicht-Standardmäßiges tut. Außerdem denke ich, dass sie in Bezug auf bedingte if -Ausdrücke die Lede mehr begraben als die vorgeschlagene try -as-a-Anweisung: Der Funktionsaufruf befindet sich in derselben Zeile wie die Bedingung , landet die Bedingung selbst ganz am Ende einer bereits überfüllten Zeile, und die Variablenzuweisungen sind auf den Block beschränkt (was eine andere Syntax erfordert, wenn Sie diese Variablen nach dem Block benötigen).

@josharian Ich hatte diesen Gedanken in letzter Zeit ziemlich oft. Go strebt nach Pragmatismus, nicht nach Perfektion, und seine Entwicklung scheint häufig eher datengetrieben als prinzipiengetrieben zu sein. Sie können schreckliches Go schreiben, aber es ist normalerweise schwieriger, als anständiges Go zu schreiben (was für die meisten Leute gut genug ist). Erwähnenswert ist auch, dass wir viele Tools zur Bekämpfung von schlechtem Code haben: nicht nur gofmt und go vet , sondern auch unsere Kollegen und die Kultur, die diese Community (sehr sorgfältig) geschaffen hat, um sich selbst zu leiten. Ich würde es hassen, mich von Verbesserungen fernzuhalten, die dem allgemeinen Fall helfen, nur weil sich jemand irgendwo selbst beschießen könnte.

@beoran Das ist elegant, und wenn Sie darüber nachdenken, unterscheidet es sich tatsächlich semantisch von den try -Blöcken anderer Sprachen, da es nur ein mögliches Ergebnis gibt: die Rückkehr von der Funktion mit einem nicht behandelten Fehler. Allerdings: 1) Dies ist wahrscheinlich verwirrend für neue Go-Programmierer, die mit diesen anderen Sprachen gearbeitet haben (ehrlich gesagt nicht meine größte Sorge; ich vertraue auf die Intelligenz von Programmierern), und 2) dies wird dazu führen, dass riesige Mengen an Code in viele eingerückt werden Codebasen. Was meinen Code betrifft, vermeide ich aus diesem Grund sogar die bestehenden type / const / var Blöcke. Außerdem sind die einzigen Schlüsselwörter, die derzeit solche Blöcke zulassen, Definitionen, keine Steueranweisungen.

@yiyus Ich bin nicht damit einverstanden, das Schlüsselwort zu entfernen, da Explizitheit (meiner Meinung nach) eine der Tugenden von Go ist. Aber ich würde zustimmen, dass es eine schlechte Idee ist, riesige Codemengen einzurücken, um try -Ausdrücke zu nutzen. Also vielleicht gar keine try Blöcke?

@rogpeppe Ich denke, diese Art von subtilem Operator ist nur für Anrufe sinnvoll, die niemals einen Fehler zurückgeben sollten, und geraten in Panik, wenn dies der Fall ist. Oder Anrufe, bei denen man den Fehler immer ignoriert. Aber beides scheint selten zu sein. Wenn Sie offen für einen neuen Operator sind, siehe #32500.

Ich habe vorgeschlagen, dass f(try g()) in https://github.com/golang/go/issues/32437#issuecomment -501074836 in Panik geraten sollte, zusammen mit einem einzeiligen Handhabungs-stmt:
on err, return ...

Ich denke, das optionale else in try ... else { ... } wird den Code zu sehr nach rechts schieben und ihn möglicherweise verdecken. Ich gehe davon aus, dass der Fehlerblock die meiste Zeit mindestens 25 Zeichen umfassen sollte. Außerdem werden bis jetzt Blöcke nicht von go fmt auf derselben Zeile gehalten und ich gehe davon aus, dass dieses Verhalten für try else beibehalten wird. Wir sollten also Beispiele diskutieren und vergleichen, bei denen sich der else -Block in einer separaten Zeile befindet. Aber selbst dann bin ich mir über die Lesbarkeit von else { am Ende der Zeile nicht sicher.

@yiyus https://github.com/golang/go/issues/32437#issuecomment -501139662

@beoran Warum sollte man es an diesem Punkt überhaupt versuchen? Lassen Sie einfach eine Zuweisung zu, bei der der letzte Fehlerwert fehlt, und lassen Sie sie sich so verhalten, als wäre es eine try-Anweisung (oder ein Funktionsaufruf). Nicht, dass ich es vorschlagen würde, aber es würde die Boilerplate noch mehr reduzieren.

Das ist nicht möglich, da Go1 bereits das Aufrufen von func foo() error als nur foo() erlaubt. Das Hinzufügen , error zu den Rückgabewerten des Aufrufers würde das Verhalten des vorhandenen Codes innerhalb dieser Funktion ändern. Siehe https://github.com/golang/go/issues/32437#issuecomment -500289410

@rogpeppe In Ihrem Kommentar zum richtigen Klammern bei verschachtelten try 's: Haben Sie irgendwelche Meinungen zum Vorrang von try ? Siehe auch die ausführliche Designdokumentation zu diesem Thema .

@griesemer Ich bin aus den dort genannten Gründen in der Tat nicht so scharf auf try als unären Präfixoperator. Mir ist aufgefallen, dass ein alternativer Ansatz darin bestehen würde, try als Pseudomethode für ein Funktionsrückgabetupel zuzulassen:

 f := os.Open(path).try()

Das löst das Vorrangproblem, denke ich, aber es ist nicht wirklich sehr Go-artig.

@rogpeppe

Sehr interessant! . Sie können hier wirklich etwas unternehmen.

Und wie wäre es, wenn wir diese Idee so erweitern?

for _,fp := range filepaths {
    f := os.Open(path).try(func(err error)bool{
        fmt.Printf( "Cannot open file %s\n", fp );
        continue;
    });
}

Übrigens bevorzuge ich vielleicht einen anderen Namen als try() wie vielleicht guard() , aber ich sollte den Namen nicht bikeshed, bevor die Architektur von anderen diskutiert wird.

gegen:

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

Ich mag try a,b := foo() anstelle von if err!=nil {return err} , weil es eine Textvorschrift für wirklich einfache Fälle ersetzt. Aber für alles andere, was Kontext hinzufügt, brauchen wir wirklich etwas anderes als if err!=nil {...} (es wird sehr schwierig sein, etwas Besseres zu finden)?

Wenn normalerweise eine zusätzliche Linie für die Dekoration / Verpackung erforderlich ist, lassen Sie uns einfach eine Linie dafür "zuordnen".

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

@networkimprov Ich glaube, das könnte mir auch gefallen. Ich drücke einen alliterativeren und beschreibenderen Begriff aus, den ich bereits angesprochen habe ...

f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }

// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args

oder

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy

@daved Freut mich, dass es dir gefällt! on err, ... würde jeden Single-stmt-Handler zulassen:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop
on err, hname err             // named handler invocation without parens
on err, ignore err            // logs error if handle ignore() defined

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

BEARBEITEN: on leiht sich von Javascript. Ich wollte if nicht überladen.
Ein Komma ist nicht unbedingt erforderlich, aber ich mag dort kein Semikolon. Vielleicht Doppelpunkt?

Ich kann relay nicht ganz folgen; es bedeutet Return-on-Error?

Ein Schutzrelais wird ausgelöst, wenn eine Bedingung erfüllt ist. Wenn in diesem Fall ein Fehlerwert nicht Null ist, ändert das Relais den Steuerfluss, um unter Verwendung der nachfolgenden Werte zurückzukehren.

*Ich möchte , für diesen Fall nicht überladen und bin kein Fan des Begriffs on , aber ich mag die Prämisse und das allgemeine Aussehen der Codestruktur.

Zu @josharians Punkt vorhin habe ich das Gefühl, dass ein großer Teil der Diskussion über übereinstimmende Klammern hauptsächlich hypothetisch ist und erfundene Beispiele verwendet. Ich weiß nicht, wie es Ihnen geht, aber es fällt mir nicht schwer, Funktionsaufrufe in meiner täglichen Programmierung zu schreiben. Wenn ich an einen Punkt komme, an dem ein Ausdruck schwer zu lesen oder zu verstehen ist, teile ich ihn mithilfe von Zwischenvariablen in mehrere Ausdrücke auf. Ich sehe nicht ein, warum try() mit Funktionsaufrufsyntax in dieser Hinsicht in der Praxis anders wäre.

@eandre Normalerweise haben Funktionen keine solche dynamische Definition. Viele Formen dieses Vorschlags verringern die Sicherheit rund um die Kommunikation des Steuerflusses, und das ist problematisch.

@networkimprov @daved Ich mag diese beiden Ideen nicht, aber sie scheinen mir keine Verbesserung genug zu sein, um einfach einzeilige if err != nil { ... } -Anweisungen zuzulassen, um eine Sprachänderung zu rechtfertigen. Trägt es auch dazu bei, sich wiederholende Textbausteine ​​zu reduzieren, wenn Sie einfach den Fehler zurückgeben? Oder ist die Idee, dass man die return immer ausschreiben muss?

@brynbellomy In meinem Beispiel gibt es kein return . relay ist ein Schutzrelais, das wie folgt definiert ist: "Wenn dieser Fehler nicht null ist, wird Folgendes zurückgegeben".

Mit meinem zweiten Beispiel von früher:

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

Könnte auch so etwas sein:

f, err := os.Open(path)
relay(err)

Der Fehler, der das Relais auslöst, wird zusammen mit Nullwerten für andere Rückgabewerte zurückgegeben (oder welche Werte auch immer für benannte Rückgabewerte festgelegt sind). Eine andere Form, die nützlich sein könnte:

wrap := func(err error, msg string) error {
    if err != nil {
        fmt.Errorf("%s: %s", msg, err)
    }
    return nil
}

// ...

f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))

Wobei das zweite Relais arg nicht aufgerufen wird, es sei denn, das Relais wird durch das erste Relais arg ausgelöst. Der optionale zweite Relaisfehler arg wäre der zurückgegebene Wert.

Sollte _go fmt_ einzeilige if zulassen, aber nicht case, for, else, var () ? Ich hätte gerne alle, bitte ;-)

Das Go-Team hat viele Anfragen nach einzeiligen Fehlerprüfungen abgelehnt.

on err, return err Aussagen könnten sich wiederholen, aber sie sind explizit, prägnant und klar.

@magical Ihr Feedback wurde in der aktualisierten Version des detaillierten Vorschlags berücksichtigt.

Eine Kleinigkeit, aber wenn try ein Schlüsselwort ist, könnte es als abschließende Anweisung erkannt werden, also statt

func f() error {
  try(g())
  return nil
}

du kannst einfach machen

func f() error {
  try g()
}

( try -Anweisung erhält das kostenlos, try -Operator würde eine besondere Behandlung benötigen, ich weiß, dass das obige kein großartiges Beispiel ist: aber es ist minimal)

@jimmyfrasche try könnte als abschließende Anweisung erkannt werden, auch wenn es sich nicht um ein Schlüsselwort handelt - wir tun dies bereits mit panic , es ist keine zusätzliche spezielle Behandlung erforderlich, abgesehen von dem, was wir bereits tun. Aber abgesehen davon ist try keine abschließende Aussage, und der Versuch, es künstlich zu einer zu machen, erscheint seltsam.

Alle gültigen Punkte. Ich denke, es könnte nur dann zuverlässig als abschließende Anweisung angesehen werden, wenn es die allerletzte Zeile einer Funktion ist, die nur einen Fehler zurückgibt, wie CopyFile im detaillierten Vorschlag, oder wenn es als try(err) verwendet wird in einem if wo bekannt ist, dass err != nil . Scheint sich nicht zu lohnen.

Da dieser Thread lang und schwer zu verfolgen ist (und sich bis zu einem gewissen Grad wiederholt), denke ich, dass wir uns alle einig sind, dass wir bei "einigen der Vorteile, die jeder Vorschlag bietet, einen Kompromiss eingehen müssten.

Da wir die oben vorgeschlagenen Code-Permutationen weiterhin mögen oder nicht mögen, helfen wir uns nicht dabei, ein echtes Gefühl dafür zu bekommen, "ist dies ein vernünftigerer Kompromiss als ein anderer / was wurde bereits angeboten"?

Ich denke, wir brauchen einige objektive Kriterien, um unsere "try"-Variationen und alternativen Vorschläge zu bewerten.

  • Verringert es die Boilerplate?
  • Lesbarkeit
  • Komplexität zur Sprache hinzugefügt
  • Fehlerstandardisierung
  • Go-isch
    ...
    ...
  • Implementierungsaufwand und Risiken
    ...

Wir können natürlich auch einige Grundregeln für No-Go’s aufstellen (keine Abwärtskompatibilität wäre eine) , und eine Grauzone für „sieht es ansprechend aus/Bauchgefühl etc. .).

Wenn wir einen Vorschlag anhand dieser Liste testen und jeden Punkt bewerten (Boilerplate 5 Punkte, Lesbarkeit 4 Punkte usw.), dann denke ich, dass wir uns stattdessen an Folgendem orientieren können:
Unsere Optionen sind wahrscheinlich A, B und C, außerdem könnte jemand, der einen neuen Vorschlag hinzufügen möchte, (bis zu einem gewissen Grad) testen, ob sein Vorschlag die Kriterien erfüllt.

Wenn dies sinnvoll ist, Daumen hoch , wir können versuchen, den ursprünglichen Vorschlag zu überarbeiten
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Und vielleicht würden einige der anderen Vorschläge in die Kommentare eingefügt oder verlinkt, vielleicht würden wir etwas lernen oder sogar eine Mischung finden, die höher bewertet würde.

Kriterien += Wiederverwendung von Fehlerbehandlungscode, paketübergreifend und innerhalb der Funktion

Vielen Dank an alle für das kontinuierliche Feedback zu diesem Vorschlag.

Die Diskussion ist ein wenig vom Kernthema abgekommen. Es wird auch von etwa einem Dutzend Mitwirkenden (Sie wissen, wer Sie sind) dominiert, die herausarbeiten, was auf alternative Vorschläge hinausläuft.

Lassen Sie mich freundlich daran erinnern, dass es in dieser Ausgabe um einen _spezifischen_ Vorschlag geht. Dies ist _keine_ Aufforderung zu neuartigen syntaktischen Ideen zur Fehlerbehandlung (was eine feine Sache ist, aber es ist nicht _dieses_ Problem).

Lassen Sie uns die Diskussion wieder stärker fokussieren und wieder auf Kurs bringen.

Feedback ist am produktivsten, wenn es hilft, technische _Fakten_ zu identifizieren, die wir übersehen haben, wie z. B. "dieser Vorschlag funktioniert in diesem Fall nicht richtig" oder "es wird diese Implikation haben, die wir nicht erkannt haben".

Zum Beispiel wies @magical darauf hin, dass der Vorschlag wie geschrieben nicht so erweiterbar war wie behauptet (der ursprüngliche Text hätte es unmöglich gemacht, ein zukünftiges zweites Argument hinzuzufügen). Glücklicherweise war dies ein kleines Problem, das mit einer kleinen Anpassung des Vorschlags leicht behoben werden konnte. Sein Beitrag hat direkt dazu beigetragen, den Vorschlag zu verbessern.

@crawshaw nahm sich die Zeit, ein paar hundert Anwendungsfälle aus der std-Bibliothek zu analysieren und zeigte, dass try selten in einem anderen Ausdruck landet, und widerlegte damit direkt die Befürchtung, dass try vergraben und unsichtbar werden könnte. Das ist ein sehr nützliches faktenbasiertes Feedback, das in diesem Fall das Design validiert.

Im Gegensatz dazu sind persönliche _ästhetische_ Urteile nicht sehr hilfreich. Wir können dieses Feedback registrieren, aber wir können nicht darauf reagieren (abgesehen davon, einen anderen Vorschlag zu machen).

Zur Erstellung von Alternativvorschlägen: Der aktuelle Vorschlag ist das Ergebnis einer Menge Arbeit, angefangen mit dem letztjährigen Entwurf . Wir haben dieses Design mehrmals wiederholt und Feedback von vielen Leuten eingeholt, bevor wir uns sicher genug fühlten, es zu veröffentlichen und zu empfehlen, es in die eigentliche Experimentphase zu bringen, aber wir haben das Experiment noch nicht durchgeführt. Es macht durchaus Sinn, zurück ans Reißbrett zu gehen, wenn das Experiment scheitert, oder uns das Feedback schon im Voraus sagt, dass es eindeutig scheitern wird. Wenn wir auf der Grundlage des ersten Eindrucks spontan umgestalten, verschwenden wir nur die Zeit aller, und schlimmer noch, wir lernen nichts aus dem Prozess.

Alles in allem ist die größte Sorge, die von vielen bei diesem Vorschlag geäußert wird, dass er nicht explizit Fehlerdekoration fördert, abgesehen von dem, was wir bereits in der Sprache tun können. Vielen Dank, wir haben dieses Feedback registriert. Wir haben intern genau das gleiche Feedback erhalten, bevor wir diesen Vorschlag veröffentlicht haben. Aber keine der Alternativen, die wir in Betracht gezogen haben, ist besser als die, die wir jetzt haben (und wir haben uns viele eingehend angesehen). Stattdessen haben wir uns entschieden, eine minimale Idee vorzuschlagen, die einen Teil der Fehlerbehandlung gut anspricht und die bei Bedarf erweitert werden kann, um genau dieses Problem anzugehen (der Vorschlag spricht ausführlich darüber).

Danke.

(Ich stelle fest, dass ein paar Leute, die sich für alternative Vorschläge einsetzen, ihre eigenen separaten Ausgaben gestartet haben. Das ist eine gute Sache und hilft, die jeweiligen Themen im Fokus zu behalten. Danke.)

@griesemer
Ich stimme vollkommen zu, dass wir uns konzentrieren sollten, und genau das hat mich dazu gebracht, zu schreiben:

Wenn dies sinnvoll ist, Daumen hoch , wir können versuchen, den ursprünglichen Vorschlag zu überarbeiten
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Zwei Fragen:

  1. Stimmen Sie zu, wenn wir die Vorteile (Reduzierung von Textbausteinen, Lesbarkeit usw.) gegenüber den Nachteilen (keine explizite Fehlerdekoration/geringere Rückverfolgbarkeit der Fehlerzeilenquelle) markieren, können wir tatsächlich sagen: Dieser Vorschlag zielt stark darauf ab, a,b etwas zu lösen help c, zielt nicht darauf ab, d, e zu lösen
    Und dadurch verlieren Sie alle Unordnung von "aber es tut nicht", "wie kann es e" und bringen es mehr in Richtung technischer Probleme, wie @magical darauf hingewiesen hat
    Und entmutigen Sie auch Kommentare wie „Aber Lösung XXX löst d,e besser.
  2. Viele Inline-Beiträge sind "Vorschläge für geringfügige Änderungen am Vorschlag" - ich weiß, es ist ein schmaler Grat, aber ich denke, es ist sinnvoll, diese beizubehalten.

LMKWYT.

Steht die Verwendung try() mit Null-Argumenten (oder einem anderen Built-in) noch zur Überlegung an oder wurde dies ausgeschlossen.

Nach den Änderungen am Vorschlag bin ich immer noch besorgt, wie es die Verwendung von benannten Rückgabewerten "üblicher" macht. Ich habe jedoch keine Daten, um das zu belegen :upside_down_ face:.
Wenn try() mit Null-Argumenten (oder eine andere integrierte Funktion) zum Vorschlag hinzugefügt wird, könnten die Beispiele im Vorschlag aktualisiert werden, um try() (oder eine andere integrierte Funktion) zu verwenden, um benannte Rückgaben zu vermeiden?

@guybrand Upvoting und Downvoting ist eine gute Sache, um _Sentiment_ auszudrücken - aber das war es auch schon. Da sind keine weiteren Informationen drin. Wir werden keine Entscheidung aufgrund der Stimmenzahl treffen, dh allein aufgrund der Stimmung. Natürlich, wenn jeder – sagen wir über 90 % – einen Vorschlag hasst, ist das wahrscheinlich ein schlechtes Zeichen und wir sollten es uns zweimal überlegen, bevor wir weitermachen. Aber das scheint hier nicht der Fall zu sein. Viele Leute scheinen damit zufrieden zu sein, Dinge auszuprobieren, und sind zu anderen Dingen übergegangen (und machen sich nicht die Mühe, diesen Thread zu kommentieren).

Wie ich oben versucht habe auszudrücken, basiert die Stimmung in diesem Stadium des Vorschlags nicht auf tatsächlichen Erfahrungen mit dem Feature; es ist ein Gefühl. Gefühle neigen dazu, sich mit der Zeit zu ändern, besonders wenn man die Gelegenheit hatte, das Thema, um das es bei den Gefühlen geht, tatsächlich zu erfahren... :-)

@Goodwine Niemand hat try() ausgeschlossen, um zum Fehlerwert zu gelangen; obwohl _wenn_ so etwas benötigt wird, ist es möglicherweise besser, eine vordeklarierte err -Variable zu haben, wie @rogpeppe vorgeschlagen hat (glaube ich).

Auch dieser Vorschlag schließt nichts davon aus. Lass uns dorthin gehen, wenn wir herausfinden, dass es notwendig ist.

@griesemer
Ich glaube du hast mich total missverstanden.
Ich beschäftige mich nicht damit, diesen oder irgendeinen Vorschlag hoch- oder runterzustimmen, ich habe nur nach einer Möglichkeit gesucht, ein gutes Gefühl dafür zu bekommen: „Halten wir es für sinnvoll, eine Entscheidung auf der Grundlage harter Kriterien zu treffen, anstatt ‚Ich mag x ' oder 'y sieht nicht gut aus' "

Von dem, was Sie geschrieben haben - das ist GENAU das, was Sie denken ... also stimmen Sie bitte meinem Kommentar zu, indem Sie sagen:

„Ich denke, wir sollten eine Liste erstellen, was dieser Vorschlag verbessern soll, und auf dieser Grundlage können wir das tun
A. entscheiden, ob das aussagekräftig genug ist
B. entscheiden, ob es so aussieht, als ob der Vorschlag wirklich löst, was er lösen soll
C. (wie Sie hinzugefügt haben) machen Sie sich die zusätzliche Mühe, um zu sehen, ob es machbar ist ...

@guybrand sie sind offensichtlich davon überzeugt, dass es sich lohnt, in der Vorabversion 1.14 (?) Prototypen zu erstellen und Feedback von praktischen Benutzern zu sammeln. IOW ist eine Entscheidung gefallen.

Außerdem wurde #32611 zur Diskussion über on err, <statement> eingereicht

@guybrand Entschuldigung. Ja, ich stimme zu, dass wir uns die verschiedenen Eigenschaften eines Vorschlags ansehen müssen, wie z. B. die Reduzierung von Standardvorgaben, löst er das vorliegende Problem usw. Aber ein Vorschlag ist mehr als die Summe seiner Teile – am Ende des Tages wir muss das Gesamtbild betrachten. Das ist Engineering, und Engineering ist chaotisch: Es gibt viele Faktoren, die in ein Design einfließen, und selbst wenn ein Teil eines Designs objektiv (auf der Grundlage harter Kriterien) nicht zufriedenstellend ist, kann es immer noch das „richtige“ Design insgesamt sein. Daher zögere ich wenig, eine Entscheidung zu unterstützen, die auf einer Art _unabhängiger_ Bewertung der einzelnen Aspekte eines Vorschlags basiert.

(Hoffentlich spricht das besser an, was Sie meinten.)

Aber in Bezug auf die relevanten Kriterien macht dieser Vorschlag meines Erachtens deutlich, worauf er abzielt. Das heißt, die Liste, auf die Sie sich beziehen, existiert bereits:

..., unser Ziel ist es, die Fehlerbehandlung leichter zu machen, indem wir die Menge an Quellcode reduzieren, die ausschließlich der Fehlerprüfung gewidmet ist. Wir möchten auch das Schreiben von Fehlerbehandlungscode bequemer machen, um die Wahrscheinlichkeit zu erhöhen, dass sich Programmierer die Zeit dafür nehmen. Gleichzeitig möchten wir Fehlerbehandlungscode explizit im Programmtext sichtbar halten.

Es passiert einfach so, dass wir für die Fehlerdekoration vorschlagen, einen defer und benannte Ergebnisparameter (oder die alte if -Anweisung) zu verwenden, weil das keine Sprachänderung erfordert - was eine fantastische Sache ist denn Sprachänderungen haben enorme versteckte Kosten. Wir bekommen, dass viele Kommentatoren das Gefühl haben, dass dieser Teil des Designs "total beschissen" ist. Dennoch denken wir an diesem Punkt, im Gesamtbild, mit allem, was wir wissen, dass es gut genug sein könnte. Andererseits brauchen wir eine Sprachänderung - eher Sprachunterstützung - um die Boilerplate loszuwerden, und try ist ungefähr die minimale Änderung, die wir uns einfallen lassen könnten. Und klar, alles ist noch explizit im Code.

Ich würde sagen, dass der Grund für so viele Reaktionen und so viele Mini-Vorschläge darin besteht, dass dies ein Thema ist, bei dem fast alle zustimmen, dass die Go-Sprache etwas tun muss, um die Fehlerbehandlung zu verringern, aber das tun wir nicht wirklich vereinbaren, wie es geht.

Dieser Vorschlag läuft im Wesentlichen auf ein eingebautes "Makro" für einen sehr häufigen, aber spezifischen Fall von Boilerplate hinaus, ähnlich wie die eingebaute Funktion append() . Während es also für den speziellen id err!=nil { return err } Anwendungsfall nützlich ist, ist das auch alles, was es tut. Da es in anderen Fällen nicht sehr hilfreich ist und auch nicht wirklich allgemein anwendbar ist, würde ich sagen, dass es nicht berauschend ist. Ich habe das Gefühl, dass die meisten Go-Programmierer etwas mehr erwartet haben, und so geht die Diskussion in diesem Thread weiter.

Es ist als Funktion kontraintuitiv. Weil es in Go nicht möglich ist, mit dieser Reihenfolge der Argumente func(... interface{}, error) zu funktionieren.
Zuerst eingegeben, dann variable Anzahl von irgendetwas Muster ist überall in Go-Modulen.

Je mehr ich denke, ich mag den aktuellen Vorschlag, so wie er ist.

Wenn wir eine Fehlerbehandlung benötigen, haben wir immer die if-Anweisung.

Hallo allerseits. Vielen Dank für die ruhige, respektvolle und konstruktive Diskussion bisher. Ich verbrachte einige Zeit damit, Notizen zu machen, und war schließlich so frustriert, dass ich ein Programm erstellte, das mir dabei half, eine andere Sicht auf diesen Kommentar-Thread zu bewahren, der navigierbarer und vollständiger sein sollte als das, was GitHub zeigt. (Es lädt auch schneller!) Siehe https://swtch.com/try.html. Ich werde es auf dem Laufenden halten, aber in Chargen, nicht Minute für Minute. (Dies ist eine Diskussion, die sorgfältiges Nachdenken erfordert und durch "Internetzeit" nicht unterstützt wird.)

Ich habe einige Gedanken hinzuzufügen, aber das muss wahrscheinlich bis Montag warten. Danke noch einmal.

@mishak87 Wir sprechen dies im detaillierten Vorschlag an. Beachten Sie, dass wir andere integrierte Funktionen ( try , make , unsafe.Offsetof usw.) haben, die „unregelmäßig“ sind – dafür sind integrierte Funktionen da.

@rsc , super nützlich! Wenn Sie es immer noch überarbeiten, verlinken Sie vielleicht die #id Issue Refs? Und die serifenlose Schriftart?

Dies wurde wahrscheinlich schon einmal behandelt, also entschuldige ich mich dafür, dass ich noch mehr Lärm hinzugefügt habe, aber ich wollte nur einen Punkt über try builtin im Vergleich zur try ... else-Idee machen.

Ich denke, die eingebaute Funktion auszuprobieren kann während der Entwicklung etwas frustrierend sein. Gelegentlich möchten wir möglicherweise Debug-Symbole hinzufügen oder mehr fehlerspezifischen Kontext hinzufügen, bevor wir zurückkehren. Man müsste eine Zeile umschreiben wie

user := try(getUser(userID))

zu

user, err := getUser(userID)
if err != nil {  
    // inspect error here
    return err
}

Das Hinzufügen einer defer-Anweisung kann hilfreich sein, aber es ist immer noch nicht die beste Erfahrung, wenn eine Funktion mehrere Fehler auslöst, da sie bei jedem try()-Aufruf ausgelöst werden würde.

Das Umschreiben mehrerer verschachtelter try()-Aufrufe in derselben Funktion wäre noch ärgerlicher.

Andererseits fügt man Kontext oder Prüfcode hinzu

user := try getUser(userID)

wäre so einfach wie das Hinzufügen einer catch-Anweisung am Ende, gefolgt vom Code

user := try getUser(userID) catch {
   // inspect error here
}

Das Entfernen oder vorübergehende Deaktivieren eines Handlers wäre so einfach wie das Unterbrechen der Zeile vor catch und das Auskommentieren.

Das Umschalten zwischen try() und if err != nil fühlt sich meiner Meinung nach viel nerviger an.

Dies gilt auch für das Hinzufügen oder Entfernen von Fehlerkontexten. Man kann try func() schreiben, während man etwas sehr schnell prototypisiert, und dann bei Bedarf Kontext zu bestimmten Fehlern hinzufügen, wenn das Programm reift, im Gegensatz zu try() als eingebautem Programm, bei dem man das neu schreiben müsste Zeilen, um während des Debuggens Kontext hinzuzufügen oder zusätzlichen Inspektionscode hinzuzufügen.

Ich bin mir sicher, dass try() nützlich wäre, aber wenn ich mir vorstelle, es in meiner täglichen Arbeit zu verwenden, kann ich nicht anders, als mir vorzustellen, wie viel hilfreicher und weniger nervig try ... catch wäre, wenn ich ' d müssen für einige Fehler zusätzlichen Code hinzufügen/entfernen.


Außerdem denke ich, dass das Hinzufügen try() und das anschließende Empfehlen, if err != nil zu verwenden, um Kontext hinzuzufügen, sehr ähnlich ist wie make() vs. new() vs. := gegen var . Diese Funktionen sind in verschiedenen Szenarien nützlich, aber wäre es nicht schön, wenn wir weniger Möglichkeiten oder sogar eine einzige Möglichkeit zum Initialisieren von Variablen hätten? Natürlich zwingt niemand jemanden, try zu verwenden, und die Leute können weiterhin if err != nil verwenden, aber ich glaube, dass dies die Fehlerbehandlung in Go aufteilen wird, genau wie die verschiedenen Möglichkeiten, neue Variablen zuzuweisen. Ich denke, dass jede Methode, die der Sprache hinzugefügt wird, auch eine Möglichkeit bieten sollte, Fehlerhandler einfach hinzuzufügen / zu entfernen, anstatt die Leute zu zwingen, ganze Zeilen neu zu schreiben, um Handler hinzuzufügen / zu entfernen. Das fühlt sich für mich nicht nach einem guten Ergebnis an.

Nochmals Entschuldigung für den Lärm, aber ich wollte darauf hinweisen, falls jemand einen separaten detaillierten Vorschlag für die try ... else -Idee schreiben wollte.

//cc @brynbellomy

Danke, @owais , dass du das noch einmal angesprochen hast – es ist ein fairer Punkt (und das Debugging-Problem wurde tatsächlich schon einmal erwähnt). try lässt die Tür offen für Erweiterungen, wie z. B. ein zweites Argument, das eine Handler-Funktion sein könnte. Aber es ist wahr, dass eine try -Funktion das Debuggen nicht einfacher macht - man muss den Code möglicherweise etwas mehr umschreiben als eine try - catch oder try - else .

@owais

Das Hinzufügen einer defer-Anweisung kann hilfreich sein, aber es ist immer noch nicht die beste Erfahrung, wenn eine Funktion mehrere Fehler auslöst, da sie bei jedem try()-Aufruf ausgelöst werden würde.

Sie könnten immer einen Typschalter in die verzögerte Funktion einfügen, der verschiedene Arten von Fehlern vor der Rückgabe auf geeignete Weise behandelt (oder nicht).

Angesichts der bisherigen Diskussion – insbesondere der Antworten des Go-Teams – habe ich den starken Eindruck, dass das Team plant, mit dem Vorschlag, der auf dem Tisch liegt, voranzukommen. Wenn ja, dann ein Kommentar und eine Bitte:

  1. Der Ist-Vorschlag IMO wird zu einer nicht unerheblichen Verringerung der Codequalität in den öffentlich verfügbaren Repos führen. Meine Erwartung ist, dass viele Entwickler den Weg des geringsten Widerstands gehen, Techniken zur Ausnahmebehandlung effektiv einsetzen und sich dafür entscheiden, try() zu verwenden, anstatt Fehler an dem Punkt zu behandeln, an dem sie auftreten. Aber angesichts der vorherrschenden Stimmung in diesem Thread ist mir klar, dass jede Hervorhebung jetzt nur einen verlorenen Kampf führen würde, also registriere ich nur meinen Einwand für die Nachwelt.

  2. Unter der Annahme, dass das Team mit dem Vorschlag wie derzeit geschrieben vorankommt, können Sie bitte einen Compiler-Schalter hinzufügen, der try() für diejenigen deaktiviert , die keinen Code wollen, der Fehler auf diese Weise ignoriert, und um Programmierer, die sie einstellen, nicht zuzulassen davon ab, es zu benutzen? _(über CI natürlich.)_ Vielen Dank im Voraus für diese Überlegung.

können Sie bitte einen Compiler-Schalter hinzufügen, der try() deaktiviert

Dies müsste auf einem Linting-Tool liegen, nicht auf dem Compiler IMO, aber ich stimme zu

Dies müsste auf einem Linting-Tool liegen, nicht auf dem Compiler IMO, aber ich stimme zu

Ich fordere ausdrücklich eine Compiler-Option und kein Linting-Tool an, weil ich das Kompilieren einer solchen Option nicht zulassen möchte. Andernfalls wird es zu einfach sein, während der lokalen Entwicklung das Fusseln zu _"vergessen"_.

@mikeschinkel Wäre es in dieser Situation nicht genauso einfach zu vergessen, die Compiler-Option einzuschalten?

Compiler-Flags sollten die Spezifikation der Sprache nicht ändern. Dies ist viel besser geeignet für Tierarzt / Fussel

Wäre es in dieser Situation nicht genauso einfach zu vergessen, die Compiler-Option einzuschalten?

Nicht bei der Verwendung von Tools wie GoLand, bei denen es keine Möglichkeit gibt, die Ausführung eines Lints vor einer Kompilierung zu erzwingen.

Compiler-Flags sollten die Spezifikation der Sprache nicht ändern.

-nolocalimports ändert die Spezifikation und -s warnt.

Compiler-Flags sollten die Spezifikation der Sprache nicht ändern.

-nolocalimports ändert die Spezifikation und -s warnt.

Nein, es ändert nichts an der Spezifikation. Nicht nur die Grammatik der Sprache bleibt gleich, sondern die Spezifikation besagt ausdrücklich:

Die Interpretation des ImportPath ist von der Implementierung abhängig, aber es ist normalerweise eine Teilzeichenfolge des vollständigen Dateinamens des kompilierten Pakets und kann relativ zu einem Repository installierter Pakete sein.

Nicht bei der Verwendung von Tools wie GoLand, bei denen es keine Möglichkeit gibt, die Ausführung eines Lints vor einer Kompilierung zu erzwingen.

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

@deanveloper

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

Das gibt es sicherlich, aber Sie vergleichen Apfel mit Organen. Was Sie zeigen, ist ein Dateibeobachter, der bei sich ändernden Dateien ausgeführt wird, und da GoLand Dateien automatisch speichert, bedeutet dies, dass er ständig ausgeführt wird, was weitaus mehr Rauschen als Signal erzeugt.

Der Lint ist immer nicht und kann (AFAIK) nicht als Vorbedingung für die Ausführung des Compilers konfiguriert werden:

image

Nein, es ändert nichts an der Spezifikation. Nicht nur die Grammatik der Sprache bleibt gleich, sondern die Spezifikation besagt ausdrücklich:

Sie spielen hier mit der Semantik, anstatt sich auf das Ergebnis zu konzentrieren. Also werde ich das gleiche tun.

Ich fordere, dass eine Compiler-Option hinzugefügt wird, die das Kompilieren von Code mit try() verbietet. Das ist keine Aufforderung, die Sprachspezifikation zu ändern, sondern nur eine Aufforderung an den Compiler, in diesem speziellen Fall anzuhalten.

Und wenn es hilft, kann die Sprachspezifikation aktualisiert werden, um so etwas zu sagen:

Die Interpretation von try() ist implementierungsabhängig, aber es ist normalerweise eine, die eine Rückgabe auslöst, wenn der letzte Parameter ein Fehler ist, aber es kann so implementiert werden, dass es nicht erlaubt ist.

Der Zeitpunkt, um nach einem Compiler-Wechsel oder einer Vet-Prüfung zu fragen, ist, nachdem der try() -Prototyp im 1.14(?)-Tipp gelandet ist. An diesem Punkt würden Sie ein neues Problem dafür einreichen (und ja, ich denke, es ist eine gute Idee). Wir wurden gebeten, Kommentare hier auf sachliche Beiträge zum aktuellen Designdokument zu beschränken.

Hallo, um das ganze Problem mit dem Hinzufügen von Debug-Anweisungen und dergleichen während der Entwicklung zu ergänzen.
Ich denke, dass die Idee mit dem zweiten Parameter für die try() -Funktion in Ordnung ist, aber eine andere Idee, sie einfach rauszuschmeißen, besteht darin, eine emit -Klausel als zweiten Teil für try() hinzuzufügen.

Zum Beispiel glaube ich, dass es beim Entwickeln und dergleichen einen Fall geben könnte, in dem ich fmt für diesen Moment anrufen möchte, um den Fehler auszugeben. Also ich könnte davon ausgehen:

func writeStuff(filename string) (io.ReadCloser, error) {
    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Kann für Debug-Anweisungen oder allgemeine Behandlung oder den Fehler vor der Rückkehr in etwas wie dieses umgeschrieben werden.

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        fmt.Printf("something happened [%v]\n", err.Error())
        return nil, err
    }

    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Hier habe ich also einen Vorschlag für ein neues Schlüsselwort emit gemacht, das eine Aussage oder ein Einzeiler zur sofortigen Rückgabe sein könnte, wie die anfängliche try() -Funktionalität:

emit return nil, err

Was die Ausgabe wäre, ist im Wesentlichen nur eine Klausel, in die Sie jede gewünschte Logik einfügen können, wenn try() durch einen Fehler ungleich Null ausgelöst wird. Eine weitere Möglichkeit mit dem Schlüsselwort emit besteht darin, dass Sie direkt auf den Fehler zugreifen können, wenn Sie direkt nach dem Schlüsselwort einen Variablennamen hinzufügen, wie ich es im ersten Beispiel getan habe.

Dieser Vorschlag macht die try() -Funktion etwas ausführlicher, aber ich denke, es ist zumindest etwas klarer, was mit dem Fehler passiert. Auf diese Weise können Sie auch die Fehler dekorieren, ohne dass sie alle in einer Zeile eingeklemmt werden, und Sie können sofort sehen, wie die Fehler behandelt werden, wenn Sie die Funktion lesen.

Dies ist eine Antwort an @mikeschinkel , ich setze meine Antwort in einen Detailblock, damit ich die Diskussion nicht zu sehr durcheinander bringe. Wie auch immer, @networkimprov hat Recht, dass diese Diskussion eingereicht werden sollte, bis dieser Vorschlag umgesetzt ist (falls dies der Fall ist).

Details zu einem Flag zum Deaktivieren von try
@mikeschinkel

Der Lint ist immer nicht und kann (AFAIK) nicht als Vorbedingung für die Ausführung des Compilers konfiguriert werden:

GoLand neu installiert, nur um dies zu testen. Dies scheint gut zu funktionieren, der einzige Unterschied besteht darin, dass die Kompilierung nicht fehlschlägt, wenn der Lint etwas findet, das ihm nicht gefällt. Das könnte jedoch leicht mit einem benutzerdefinierten Skript behoben werden, das golint ausführt und mit einem Exit-Code ungleich Null fehlschlägt, wenn es eine Ausgabe gibt.
image

(Bearbeiten: Ich habe den Fehler behoben, den es mir unten mitteilen wollte. Es lief gut, auch wenn der Fehler vorhanden war, aber das Ändern von "Run Kind" in das Verzeichnis entfernte den Fehler und es funktionierte gut.)

Auch ein weiterer Grund, warum es KEIN Compiler-Flag sein sollte - der gesamte Go-Code wird aus der Quelle kompiliert. Dazu gehören Bibliotheken. Das bedeutet, dass Sie, wenn Sie try über den Compiler ausschalten möchten, auch try für jede einzelne der von Ihnen verwendeten Bibliotheken ausschalten würden. Es ist nur eine schlechte Idee, es als Compiler-Flag zu haben.

Sie spielen hier mit der Semantik, anstatt sich auf das Ergebnis zu konzentrieren.

Nein, bin ich nicht. Compiler-Flags sollten die Spezifikation der Sprache nicht ändern. Die Spezifikation ist sehr übersichtlich und damit etwas "Go" ist, muss es der Spezifikation folgen. Die von Ihnen erwähnten Compiler-Flags ändern das Verhalten der Sprache, aber egal was passiert, sie stellen sicher, dass die Sprache immer noch der Spezifikation entspricht. Dies ist ein wichtiger Aspekt von Go. Solange Sie der Go-Spezifikation folgen, sollte Ihr Code auf jedem Go-Compiler kompiliert werden.

Ich fordere, dass eine Compiler-Option hinzugefügt wird, die das Kompilieren von Code mit try() verbietet. Das ist keine Aufforderung, die Sprachspezifikation zu ändern, sondern nur eine Aufforderung an den Compiler, in diesem speziellen Fall anzuhalten.

Es ist eine Aufforderung, die Spezifikation zu ändern. Dieser Vorschlag an sich ist eine Aufforderung, die Spezifikation zu ändern. Eingebaute Funktionen sind sehr speziell in der Spezifikation enthalten. . Die Bitte um ein Compiler-Flag, das das eingebaute try entfernt, wäre daher ein Compiler-Flag, das die Spezifikation der zu kompilierenden Sprache ändern würde.

Davon abgesehen denke ich, dass ImportPath in der Spezifikation standardisiert werden sollte. Dazu kann ich einen Vorschlag machen.

Und wenn es hilft, kann die Sprachspezifikation aktualisiert werden, um so etwas wie [...]

Obwohl dies zutrifft, möchten Sie nicht, dass die Implementierung von try von der Implementierung abhängig ist. Es soll ein wichtiger Teil der Fehlerbehandlung der Sprache sein, was bei jedem Go-Compiler gleich sein müsste.

@deanveloper

_"In jedem Fall hat @networkimprov Recht , dass diese Diskussion eingereicht werden sollte, bis dieser Vorschlag umgesetzt ist (falls dies der Fall ist)."_

Warum haben Sie sich dann entschieden, diesen Vorschlag zu ignorieren und trotzdem in diesem Thread zu posten, anstatt auf später zu warten? Sie haben Ihre Punkte hier argumentiert und gleichzeitig behauptet, dass ich Ihre Punkte nicht in Frage stellen sollte. Üben, was Sie predigen...

Wenn Sie die Wahl haben, antworte ich auch, ebenfalls in einem Detailblock

Hier:

_"Das könnte jedoch leicht mit einem benutzerdefinierten Skript behoben werden, das golint ausführt und mit einem Exit-Code ungleich Null fehlschlägt, wenn es eine Ausgabe gibt."_

Ja, mit genügend Codierung kann _jedes_ Problem behoben werden. Aber wir wissen beide aus Erfahrung, dass je komplexer eine Lösung ist, desto weniger Leute, die sie verwenden wollen, werden sie am Ende tatsächlich verwenden.

Ich habe hier also ausdrücklich nach einer einfachen Lösung gefragt, nicht nach einer Roll-your-own-Lösung.

_"Sie würden try auch für jede einzelne der von Ihnen verwendeten Bibliotheken deaktivieren."_

Und das ist _ausdrücklich_ der Grund, warum ich darum gebeten habe. Weil ich sicherstellen möchte, dass der gesamte Code, der dieses lästige _"Feature"_ verwendet, nicht in die von uns vertriebenen ausführbaren Dateien gelangt.

_"Es ist eine Anfrage, die Spezifikation zu ändern. Dieser Vorschlag an sich ist eine Anfrage, die Spezifikation zu ändern._"

Es ist ABSOLUT keine Änderung der Spezifikation. Es ist eine Anforderung für einen Schalter, um das _Verhalten_ des build -Befehls zu ändern, keine Änderung der Sprachspezifikation.

Wenn jemand nach dem Befehl go fragt, um einen Schalter zu haben, um seine Terminalausgabe in Mandarin anzuzeigen, ist das keine Änderung der Sprachspezifikation.

Wenn go build diesen Schalter sehen würde, würde es in ähnlicher Weise einfach eine Fehlermeldung ausgeben und anhalten, wenn es auf ein try() stößt. Keine Sprachspezifikationsänderungen erforderlich.

_"Es wurde entwickelt, um ein wichtiger Teil der Fehlerbehandlung der Sprache zu sein, was bei jedem Go-Compiler gleich sein müsste."_

Es wird ein problematischer Teil der Fehlerbehandlung der Sprache sein, und es optional zu machen, wird es denen ermöglichen, die seine Probleme vermeiden wollen, dies tun zu können.

Ohne den Switch werden die meisten Leute wahrscheinlich nur eine neue Funktion sehen und sie annehmen und sich nie fragen, ob sie tatsächlich verwendet werden sollte.

_Mit dem Switch_ – und Artikeln, die das neue Feature erklären, in denen der Switch erwähnt wird – werden viele Leute verstehen, dass es ein problematisches Potenzial hat, und so wird es dem Go-Team ermöglichen, zu untersuchen, ob es eine gute Aufnahme war oder nicht, indem es sieht, wie viel öffentlicher Code seine Verwendung vermeidet vs. wie öffentlicher Code ihn verwendet. Das könnte das Design von Go 3 beeinflussen.

_"Nein, bin ich nicht. Compiler-Flags sollten die Spezifikation der Sprache nicht ändern."_

Zu sagen, dass Sie keine Semantik spielen, bedeutet nicht, dass Sie keine Semantik spielen.

Bußgeld. Dann fordere ich stattdessen einen neuen Top-Level-Befehl namens _(so etwas wie)_ build-guard an, der verwendet wird, um problematische Funktionen während der Kompilierung zu verbieten, beginnend mit dem Verbieten try() .

Das beste Ergebnis ist natürlich, wenn das try() -Feature mit einem Plan vorgelegt wird, das Problem in Zukunft anders zu lösen, ein Weg, dem die große Mehrheit zustimmt. Aber ich fürchte, das Schiff ist bereits auf try() gesegelt, also hoffe ich, seine Nachteile zu minimieren.


Also, wenn Sie wirklich mit @networkimprov einverstanden sind, dann verschieben Sie Ihre Antwort auf später, wie sie vorgeschlagen haben.

Entschuldigung für die Unterbrechung, aber ich habe Fakten zu berichten :-)

Ich bin mir sicher, dass das Go-Team das Defer bereits Benchmarking durchgeführt hat, aber ich habe keine Zahlen gesehen ...

$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2      20000000                72.3 ns/op
BenchmarkAlways4-2      20000000                68.1 ns/op
BenchmarkAlways6-2      20000000                68.0 ns/op

BenchmarkNever2-2       100000000               16.5 ns/op
BenchmarkNever4-2       100000000               13.1 ns/op
BenchmarkNever6-2       100000000               13.5 ns/op

Quelle

package deferbench

import (
   "fmt"
   "errors"
   "testing"
)

func Always(iM, iN int) (err error) {
   defer func() {
      if err != nil {
         err = fmt.Errorf("d: %v", err)
      }
   }()
   if iN % iM == 0 {
      return errors.New("e")
   }
   return nil
}

func Never(iM, iN int) (err error) {
   if iN % iM == 0 {
      return fmt.Errorf("r: %v", errors.New("e"))
   }
   return nil
}

func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}

func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}

@networkimprov

Von https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency -of-defer (meine Hervorhebung in Fettdruck)

Unabhängig davon hat das Runtime- und Compiler-Team von Go alternative Implementierungsoptionen diskutiert, und wir glauben, dass wir typische Verzögerungsanwendungen für die Fehlerbehandlung etwa so effizient wie bestehenden „manuellen“ Code machen können. Wir hoffen, diese schnellere Defer-Implementierung in Go 1.14 verfügbar zu machen (siehe auch * CL 171758 * , was ein erster Schritt in diese Richtung ist).

dh defer ist jetzt eine 30%ige Leistungsverbesserung für go1.13 für den allgemeinen Gebrauch und sollte schneller und genauso effizient sein wie der Non-defer-Modus in go 1.14

Vielleicht kann jemand Zahlen für den 1.13 und den 1.14 CL posten?

Optimierungen überleben nicht immer den Kontakt mit dem Feind ... äh, dem Ökosystem.

1.13 Verzögerungen werden etwa 30% schneller sein:

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

Folgendes erhalte ich bei den obigen Tests von @networkimprov (1.12.5 bis Tipp):

name       old time/op  new time/op  delta
Always2-4  59.8ns ± 1%  47.5ns ± 1%  -20.57%  (p=0.008 n=5+5)
Always4-4  57.9ns ± 2%  43.5ns ± 1%  -24.96%  (p=0.008 n=5+5)
Always6-4  57.6ns ± 2%  44.1ns ± 1%  -23.43%  (p=0.008 n=5+5)
Never2-4   13.7ns ± 8%   3.8ns ± 4%  -72.27%  (p=0.008 n=5+5)
Never4-4   10.5ns ± 6%   1.3ns ± 2%  -87.76%  (p=0.008 n=5+5)
Never6-4   10.8ns ± 6%   1.2ns ± 1%  -88.46%  (p=0.008 n=5+5)

(Ich bin mir nicht sicher, warum Never Ones so viel schneller sind. Vielleicht Inlining-Änderungen?)

Die Optimierungen für Verzögerungen für 1.14 sind noch nicht implementiert, daher wissen wir nicht, wie die Leistung sein wird. Aber wir denken, wir sollten uns der Leistung eines regulären Funktionsaufrufs annähern.

Warum haben Sie sich dann entschieden, diesen Vorschlag zu ignorieren und trotzdem in diesem Thread zu posten, anstatt auf später zu warten?

Der Detailblock wurde später bearbeitet, nachdem ich den Kommentar von @networkimprov gelesen hatte. Es tut mir leid, dass ich es so aussehen lasse, als hätte ich verstanden, was er gesagt hat, und es ignoriert. Ich beende die Diskussion nach dieser Aussage, ich wollte mich erklären, da Sie mich gefragt hatten, warum ich den Kommentar gepostet habe.


Was die Optimierungen zum Aufschieben betrifft, bin ich gespannt darauf. Sie unterstützen diesen Vorschlag ein wenig und machen defer HandleErrorf(...) etwas weniger schwer. Ich mag aber immer noch nicht die Idee, benannte Parameter zu missbrauchen, damit dieser Trick funktioniert. Um wie viel wird es voraussichtlich für 1.14 beschleunigt? Sollten sie mit ähnlichen Geschwindigkeiten laufen?

@griesemer Ein Bereich, der es wert sein könnte, etwas mehr erweitert zu werden, ist die Funktionsweise von Übergängen in einer Welt mit try , vielleicht einschließlich:

  • Die Kosten für den Übergang zwischen fehlerhaften Dekorationsstilen.
  • Die Klassen möglicher Fehler, die beim Übergang zwischen Stilen auftreten können.
  • Welche Klassen von Fehlern würden (a) sofort von einem Compiler-Fehler abgefangen werden, vs. (b) von vet oder staticcheck oder ähnlichem abgefangen, vs. (c) könnten zu einem Fehler führen, der möglicherweise nicht bemerkt oder müssten durch Tests abgefangen werden.
  • Das Ausmaß, in dem Werkzeuge die Kosten und die Fehlerwahrscheinlichkeit beim Übergang zwischen Stilen mindern könnten, und insbesondere, ob gopls (oder ein anderes Dienstprogramm) eine Rolle bei der Automatisierung gängiger Dekorationsstilübergänge spielen könnte oder sollte.

Stufen der Fehlerdekoration

Dies ist nicht erschöpfend, aber eine repräsentative Reihe von Phasen könnte etwa so aussehen:

0. Keine Fehlerdekoration (z. B. Verwendung try ohne Dekoration).
1. Einheitliche Fehlerdekoration (z. B. Verwendung try + defer für einheitliche Dekoration).
2. N-1 Austrittspunkte haben eine einheitliche Fehlerdekoration , aber 1 Austrittspunkt hat eine unterschiedliche Dekoration (z. B. vielleicht eine permanente detaillierte Fehlerdekoration an nur einer Stelle oder vielleicht ein temporäres Debug-Protokoll usw.).
3. Alle Austrittspunkte haben jeweils eine einzigartige Fehlerdekoration oder etwas, das sich einzigartig annähert.

Jede gegebene Funktion wird keinen strengen Verlauf durch diese Phasen haben, also ist "Stufen" vielleicht das falsche Wort, aber einige Funktionen wechseln von einem Dekorationsstil zu einem anderen, und es könnte nützlich sein, genauer zu sagen, was diese Übergänge sind sind wie wenn oder ob sie passieren.

Stufe 0 und Stufe 1 scheinen für den aktuellen Vorschlag ideal zu sein und sind zufällig auch ziemlich häufige Anwendungsfälle. Ein Übergang von Stufe 0 nach 1 scheint unkompliziert. Wenn Sie in Stufe 0 try ohne Dekoration verwendet haben, können Sie so etwas wie defer fmt.HandleErrorf(&err, "foo failed with %s", arg1) hinzufügen. Möglicherweise müssen Sie in diesem Moment auch benannte Rückgabeparameter unter dem ursprünglich geschriebenen Vorschlag einführen. Wenn der Vorschlag jedoch einen der Vorschläge in Anlehnung an eine vordefinierte integrierte Variable übernimmt, die ein Alias ​​für den endgültigen Fehlerergebnisparameter ist, könnten die Kosten und das Fehlerrisiko hier gering sein?

Andererseits erscheint ein Übergang von Stufe 1 nach 2 unangenehm (oder "nervig", wie einige andere gesagt haben), wenn Stufe 1 eine einheitliche Fehlerdekoration mit einem defer war. Um ein bestimmtes Stück Dekoration an einem Ausgangspunkt hinzuzufügen, müssten Sie zuerst das defer entfernen (um doppelte Dekoration zu vermeiden), dann müsste man anscheinend alle Rückkehrpunkte besuchen, um die try zu entzuckern if -Anweisungen verwendet, wobei N-1 der Fehler auf die gleiche Weise dekoriert werden und 1 anders dekoriert wird.

Ein Übergang von Stufe 1 zu 3 erscheint auch umständlich, wenn er manuell durchgeführt wird.

Fehler beim Übergang zwischen Dekorationsstilen

Einige Fehler, die im Rahmen eines manuellen Entzuckerungsprozesses passieren können, umfassen das versehentliche Beschatten einer Variablen oder das Ändern der Auswirkung eines benannten Rückgabeparameters usw. Wenn Sie sich beispielsweise das erste und größte Beispiel im Abschnitt „Beispiele“ der Versuchsvorschlag, die CopyFile -Funktion hat 4 try Verwendungen, einschließlich in diesem Abschnitt:

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

Wenn jemand eine "offensichtliche" manuelle Entzuckerung von w := try(os.Create(dst)) durchgeführt hat, könnte diese eine Zeile erweitert werden zu:

        w, err := os.Create(dst)
        if err != nil {
            // do something here
            return err
        }

Das sieht auf den ersten Blick gut aus, aber je nachdem, in welchem ​​Block sich diese Änderung befindet, könnte das auch versehentlich den benannten Rückgabeparameter err und die Fehlerbehandlung im nachfolgenden defer .

Automatisieren des Übergangs zwischen Dekorationsstilen

Um den Zeitaufwand und das Risiko von Fehlern zu verringern, könnte gopls (oder ein anderes Dienstprogramm) vielleicht eine Art Befehl haben, um ein bestimmtes try zu entzuckern, oder einen Befehl, der alle Verwendungen von try entzuckert gopls Befehle nur auf das Entfernen und Ersetzen try konzentrieren, aber vielleicht könnte ein anderer Befehl alle Verwendungen von try entzuckern und gleichzeitig zumindest häufige Fälle von Dingen transformieren wie defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) oben in der Funktion in den entsprechenden Code an jeder der früheren try -Positionen (was beim Übergang von Stufe 1->2 oder Stufe 1->3 hilfreich wäre). Das ist keine ausgereifte Idee, aber vielleicht lohnt es sich, darüber nachzudenken, was möglich oder wünschenswert ist, oder den Vorschlag mit aktuellen Überlegungen zu aktualisieren.

Idiomatische Ergebnisse?

Ein verwandter Kommentar ist, dass es nicht sofort offensichtlich ist, wie häufig eine programmatische fehlerfreie Transformation eines try am Ende wie normaler idiomatischer Go-Code aussehen würde. Passen Sie eines der Beispiele aus dem Vorschlag an, wenn Sie beispielsweise entzuckern möchten:

x1, x2, x3 = try(f())

In manchen Fällen könnte eine programmatische Transformation, die das Verhalten beibehält, so enden:

t1, t2, t3, te := f()  // visible temporaries
if te != nil {
        return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3

Diese genaue Form mag selten sein, und es scheint, dass die Ergebnisse eines Editors oder einer IDE, die programmatisches Entzuckern durchführen, oft idiomatischer aussehen könnten, aber es wäre interessant zu hören, wie wahr das ist, auch angesichts der möglicherweise werdenden benannten Rückgabeparameter häufiger und unter Berücksichtigung von Schatten, := vs. = , andere Verwendungen von err in derselben Funktion usw.

Der Vorschlag spricht über mögliche Verhaltensunterschiede zwischen if und try aufgrund von benannten Ergebnisparametern, aber in diesem speziellen Abschnitt scheint es hauptsächlich um den Übergang von if zu try zu gehen. Abschnitt , der _"Obwohl dies ein subtiler Unterschied ist, glauben wir, dass solche Fälle selten sind. Wenn aktuelles Verhalten erwartet wird, behalten Sie die if-Anweisung bei."_). Im Gegensatz dazu kann es beim Übergang von try zurück zu if verschiedene mögliche Fehler geben, die näher erläutert werden sollten, während das identische Verhalten beibehalten wird.


Entschuldigen Sie auf jeden Fall den langen Kommentar, aber es scheint, dass die Angst vor hohen Übergangskosten zwischen den Stilen einigen der Bedenken zugrunde liegt, die in einigen der anderen hier geposteten Kommentare geäußert werden, und daher der Vorschlag, diese Übergangskosten und mögliche Abmilderungen.

@thepudds Ich liebe dich hebt die Kosten und potenziellen Fehler hervor, die damit verbunden sind, wie Sprachfunktionen das Refactoring entweder positiv oder negativ beeinflussen können. Es ist kein Thema, das ich oft diskutiert sehe, aber eines, das einen großen nachgelagerten Effekt haben kann.

Ein Übergang von Stufe 1 nach 2 erscheint unangenehm, wenn Stufe 1 eine einheitliche Fehlerdekoration mit einer Verzögerung war. Um ein bestimmtes Stück Dekoration an einem Austrittspunkt hinzuzufügen, müssten Sie zuerst die Verzögerung entfernen (um eine doppelte Dekoration zu vermeiden), dann müsste man anscheinend alle Rückgabepunkte besuchen, um die try-Verwendungen in if-Anweisungen mit N zu entzuckern -1 der Fehler wird auf die gleiche Weise dekoriert und 1 wird anders dekoriert.

Hier glänzt die Verwendung break anstelle von return mit 1.12. Verwenden Sie es in einem for range once { ... } -Block, in dem once = "1" die Codesequenz abgrenzt, die Sie möglicherweise verlassen möchten, und wenn Sie dann nur einen Fehler dekorieren müssen, tun Sie dies an der Stelle von break . Und wenn Sie alle Fehler dekorieren müssen, tun Sie dies direkt vor dem einzigen return am Ende der Methode.

Der Grund, warum es ein so gutes Muster ist, ist, dass es sich ändernden Anforderungen widersteht; Sie müssen selten funktionierenden Code brechen, um neue Anforderungen zu implementieren. Und es ist meiner Meinung nach ein saubererer und offensichtlicherer Ansatz, als zum Anfang der Methode zurückzuspringen, bevor man sie wieder verlässt.

fwiw

Die Ergebnisse von @ randall77 für meinen Benchmark zeigen einen Overhead von 40+ns pro Anruf für 1.12 und Tipp. Das bedeutet, dass Verzögerungen Optimierungen verhindern können, wodurch Verbesserungen in einigen Fällen hinfällig werden.

@networkimprov Defer verhindert derzeit Optimierungen, und das ist ein Teil dessen, was wir gerne beheben würden. Zum Beispiel wäre es schön, den Körper der zurückgestellten Funktion einzubetten, genau wie wir normale Aufrufe einbetten.

Ich verstehe nicht, wie alle Verbesserungen, die wir vornehmen, strittig wären. Woher kommt diese Behauptung?

Woher kommt diese Behauptung?

Der Mehraufwand von 40 ns pro Aufruf für eine Funktion mit einer Verzögerung zum Umschließen des Fehlers hat sich nicht geändert.

Die Änderungen in 1.13 sind ein Teil der Optimierung der Verzögerung. Es sind weitere Verbesserungen geplant. Dies wird im Entwurfsdokument und in dem Teil des Entwurfsdokuments behandelt, der an irgendeiner Stelle oben zitiert wird.

Re swtch.com/try.html und https://github.com/golang/go/issues/32437#issuecomment -502192315:

@rsc , super nützlich! Wenn Sie es immer noch überarbeiten, verlinken Sie vielleicht die #id Issue Refs? Und die serifenlose Schriftart?

Auf dieser Seite geht es um Inhalte. Konzentrieren Sie sich nicht auf die Rendering-Details. Ich verwende die Ausgabe von blackfriday auf dem Eingabe-Markdown unverändert (also keine GitHub-spezifischen #id-Links) und bin mit der Serifenschrift zufrieden.

Erneutes Deaktivieren/Überprüfen versuchen :

Es tut mir leid, aber es wird keine Compiler-Optionen geben, um bestimmte Go-Funktionen zu deaktivieren, noch wird es tierärztliche Kontrollen geben, die besagen, diese Funktionen nicht zu verwenden. Wenn die Funktion schlecht genug ist, um sie zu deaktivieren oder zu überprüfen, werden wir sie nicht einbauen. Umgekehrt, wenn die Funktion vorhanden ist, kann sie verwendet werden. Es gibt eine Go-Sprache, nicht eine andere Sprache für jeden Entwickler, basierend auf der Wahl der Compiler-Flags.

@mikeschinkel , jetzt hast du zweimal zu diesem Thema die Verwendung von try als _ignoring_ error beschrieben.
Am 7. Juni schrieben Sie unter der Überschrift „Erleichtert Entwicklern das Ignorieren von Fehlern“:

Dies ist eine totale Wiederholung dessen, was andere kommentiert haben, aber was im Grunde try() liefert, ist in vielerlei Hinsicht analog dazu, das Folgende einfach als idomatischen Code zu umarmen, und dies ist Code, der niemals seinen Weg in irgendeinen Code finden wird - Respektieren von Entwicklerschiffen:

f, _ := os.Open(filename)

Ich weiß, dass ich in meinem eigenen Code besser sein kann, aber ich weiß auch, dass viele von uns auf die Größe anderer Go-Entwickler angewiesen sind, die einige enorm nützliche Pakete veröffentlichen, aber nach dem, was ich in _"Other People's Code(tm)"_ gesehen habe Best Practices bei der Fehlerbehandlung werden oft ignoriert.

Also im Ernst, wollen wir es Entwicklern wirklich erleichtern, Fehler zu ignorieren und ihnen erlauben, GitHub mit nicht robusten Paketen zu verschmutzen?

Und dann, am 14. Juni , bezeichneten Sie die Verwendung von try erneut als "Code, der Fehler auf diese Weise ignoriert".

Ohne das Code-Snippet f, _ := os.Open(filename) würde ich denken, dass Sie einfach übertreiben, indem Sie "auf einen Fehler prüfen und ihn zurückgeben" als "Ignorieren" eines Fehlers charakterisieren. Aber das Code-Snippet zusammen mit den vielen Fragen, die bereits im Vorschlagsdokument oder in der Sprachspezifikation beantwortet wurden, lassen mich fragen, ob wir nicht doch über die gleiche Semantik sprechen. Also nur um das klarzustellen und deine Fragen zu beantworten:

Beim Studium des Codes des Vorschlags finde ich, dass das Verhalten nicht offensichtlich und etwas schwer zu begründen ist.

Wenn ich sehe, dass try() einen Ausdruck umschließt, was passiert, wenn ein Fehler zurückgegeben wird?

Wenn Sie try(f()) sehen und f() einen Fehler zurückgibt, stoppt try die Ausführung des Codes und gibt diesen Fehler von der Funktion zurück, in deren Körper try erscheint.

Wird der Fehler einfach ignoriert?

Nein. Der Fehler wird nie ignoriert. Es wird zurückgegeben, genau wie bei der Verwendung einer return-Anweisung. Wie:

{ err := f(); if err != nil { return err } }

Oder springt es zum ersten oder letzten defer ,

Die Semantik ist dieselbe wie bei der Verwendung einer return-Anweisung.

Zurückgestellte Funktionen laufen „ in umgekehrter Reihenfolge ab, in der sie zurückgestellt wurden “.

und wenn ja, wird es automatisch eine Variable namens err innerhalb des Abschlusses setzen, oder wird es als Parameter übergeben _(Ich sehe keinen Parameter?)_.

Die Semantik ist dieselbe wie bei der Verwendung einer return-Anweisung.

Wenn Sie auf einen Ergebnisparameter in einem zurückgestellten Funktionsrumpf verweisen müssen, können Sie ihm einen Namen geben. Siehe das result Beispiel in https://golang.org/ref/spec#Defer_statements.

Und wenn kein automatischer Fehlername, wie benenne ich ihn? Und bedeutet das, dass ich meine eigene Variable err in meiner Funktion nicht deklarieren kann, um Konflikte zu vermeiden?

Die Semantik ist dieselbe wie bei der Verwendung einer return-Anweisung.

Eine return-Anweisung weist immer den tatsächlichen Funktionsergebnissen zu, selbst wenn das Ergebnis unbenannt ist, und selbst wenn das Ergebnis benannt, aber schattiert ist.

Und wird es alle defer s anrufen? In umgekehrter Reihenfolge oder in normaler Reihenfolge?

Die Semantik ist dieselbe wie bei der Verwendung einer return-Anweisung.

Zurückgestellte Funktionen laufen „ in umgekehrter Reihenfolge ab, in der sie zurückgestellt wurden “. (Umgekehrte Reihenfolge ist reguläre Reihenfolge.)

Oder wird es sowohl von der Schließung als auch von func zurückkehren, wo der Fehler zurückgegeben wurde? _(Etwas, an das ich nie gedacht hätte, wenn ich hier nicht Worte gelesen hätte, die das implizieren.)_

Ich weiß nicht, was das bedeutet, aber wahrscheinlich ist die Antwort nein. Ich würde dazu ermutigen, sich auf den Vorschlagstext und die Spezifikation zu konzentrieren und nicht auf andere Kommentare hier darüber, was dieser Text bedeuten könnte oder nicht.

Nachdem ich den Vorschlag und alle bisherigen Kommentare gelesen habe, kenne ich die Antworten auf die obigen Fragen ehrlich gesagt immer noch nicht. Ist das die Art von Feature, die wir einer Sprache hinzufügen möchten, deren Befürworter sich dafür einsetzen, _"Captain Obvious"_ zu sein?

Generell streben wir eine einfache, leicht verständliche Sprache an. Es tut mir leid, dass Sie so viele Fragen hatten. Aber dieser Vorschlag verwendet wirklich so viel wie möglich von der bestehenden Sprache (insbesondere defers), sodass es nur sehr wenige zusätzliche Details zu lernen gibt. Sobald Sie das wissen

x, y := try(f())

bedeutet

tmp1, tmp2, tmpE := f()
if tmpE != nil {
   return ..., tmpE
}
x, y := tmp1, tmp2

fast alles andere sollte sich aus den Implikationen dieser Definition ergeben.

Dies ist kein "Ignorieren" von Fehlern. Ignorieren eines Fehlers ist, wenn Sie schreiben:

c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)

und der Code gerät in Panik, weil net.Dial fehlgeschlagen ist und der Fehler ignoriert wurde, c gleich nil ist und der Aufruf von io.Copy an c.Read fehlschlägt. Im Gegensatz dazu prüft dieser Code den Fehler und gibt ihn zurück:

 c := try(net.Dial("tcp", "127.0.0.1:1234"))
 io.Copy(os.Stdout, c)

Um Ihre Frage zu beantworten, ob wir letzteres gegenüber ersterem fördern wollen: ja.

@damienfamed75 Ihre vorgeschlagene emit -Anweisung sieht im Wesentlichen genauso aus wie die handle -Anweisung des Designentwurfs . Der Hauptgrund für den Verzicht auf die Deklaration handle war ihre Überschneidung mit defer . Mir ist nicht klar, warum man nicht einfach defer verwenden könnte, um den gleichen Effekt zu erzielen, den emit erzielt.

@dominikh fragte :

Wird acme anfangen, den Versuch hervorzuheben?

So viel über den Versuchsvorschlag ist unentschieden, in der Luft, unbekannt.

Aber diese Frage kann ich definitiv beantworten: nein.

@rsc

Danke für Ihre Antwort.

_"Zweimal haben Sie bei diesem Problem die Verwendung von try als Ignorieren von Fehlern beschrieben."_

Ja, ich habe aus meiner Perspektive kommentiert und war technisch nicht korrekt.

Was ich meinte, war _„Erlauben, dass Fehler weitergegeben werden, ohne dekoriert zu werden.“_ Für mich ist das _„Ignorieren“_ – ähnlich wie Menschen, die Ausnahmebehandlung verwenden, Fehler _„ignorieren“_ –, aber ich kann mir durchaus vorstellen, wie andere es tun würden halte meine Formulierung für fachlich nicht korrekt.

_"Wenn Sie try(f()) sehen und f() einen Fehler zurückgibt, stoppt der Versuch die Ausführung des Codes und gibt diesen Fehler von der Funktion zurück, in deren Hauptteil der Versuch erscheint."_

Das war eine Antwort auf eine Frage aus meinem Kommentar vor einiger Zeit, aber inzwischen habe ich das herausgefunden.

Und am Ende macht es zwei Dinge, die mich traurig machen. Gründe dafür:

  1. Es wird den Weg des geringsten Widerstands bereiten, um Dekorationsfehler zu vermeiden – viele Entwickler werden dazu ermutigt, genau das zu tun – und viele werden diesen Code für andere zur Verwendung veröffentlichen, was zu öffentlich zugänglichem Code von geringerer Qualität mit weniger robuster Fehlerbehandlung/Fehlerberichterstattung führt .

  2. Für diejenigen wie mich, die break und continue für die Fehlerbehandlung anstelle von return verwenden – ein Muster, das widerstandsfähiger gegenüber sich ändernden Anforderungen ist – werden wir nicht einmal in der Lage sein, es zu verwenden try() , auch wenn es wirklich keinen Grund gibt, den Fehler zu kommentieren.

_"Oder wird es sowohl von der Schließung als auch von der Funktion zurückkehren, wo der Fehler zurückgegeben wurde? (Etwas, an das ich nie gedacht hätte, wenn ich hier nicht Wörter gelesen hätte, die dies implizieren.)"_

_"Ich weiß nicht, was das bedeutet, aber wahrscheinlich lautet die Antwort nein. Ich würde dazu ermutigen, sich auf den Vorschlagstext und die Spezifikation zu konzentrieren und nicht auf andere Kommentare hier darüber, was dieser Text bedeuten könnte oder nicht."_

Auch diese Frage war vor über einer Woche, also verstehe ich sie jetzt besser.

Um es für die Nachwelt klarzustellen, das defer hat einen Verschluss, richtig? Wenn Sie von dieser Schließung zurückkehren, wird es – sofern ich es nicht falsch verstehe – nicht nur von der Schließung zurückkehren, sondern auch von den func , wo der Fehler aufgetreten ist, richtig? _(Keine Notwendigkeit zu antworten, wenn ja.)_

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

Übrigens, meines Wissens ist der Grund für try() , weil Entwickler sich über Boilerplate beschwert haben. Ich finde das auch traurig, weil ich denke, dass die Anforderung, zurückgegebene Fehler zu akzeptieren, die zu diesem Boilerplate führt, dazu beiträgt, Go-Apps robuster zu machen als in vielen anderen Sprachen.

Ich persönlich würde es vorziehen zu sehen, dass Sie es schwieriger machen, Fehler nicht zu schmücken, als es einfacher zu machen, sie zu ignorieren. Aber ich gebe zu, dass ich in dieser Hinsicht in der Minderheit zu sein scheine.


Übrigens haben einige Leute eine Syntax wie eine der folgenden vorgeschlagen _(ich habe ein hypothetisches .Extend() hinzugefügt, um meine Beispiele kurz zu halten):_

f := try os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err   
}

Oder

try f := os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Und dann behaupten andere, dass es darüber nicht wirklich Zeichen spart:

f,err := os.Open(filename)
if err != nil {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Aber eine Sache, die Kritik vermisst, ist die Verschiebung von 5 Zeilen auf 4 Zeilen, eine Reduzierung des vertikalen Raums , und das scheint erheblich zu sein, insbesondere wenn Sie viele solcher Konstrukte in einem func benötigen.

Noch besser wäre so etwas, das 40 % des vertikalen Platzes eliminieren würde _(obwohl ich angesichts der Kommentare zu Schlüsselwörtern bezweifle, dass dies in Betracht gezogen würde):_

try f := os.Open(filename) 
    else err().Extend("Cannot open file %s",filename)
    end //break, continue or return err    

#fwiw


JEDENfalls , wie ich bereits sagte, schätze ich, dass das Schiff gesegelt ist, also werde ich einfach lernen, es zu akzeptieren.

Ziele

Einige Kommentare hier haben in Frage gestellt, was wir mit dem Vorschlag bezwecken. Zur Erinnerung: In der Fehlerbehandlungs-Problembeschreibung , die wir letzten August veröffentlicht haben, heißt es im Abschnitt „Ziele“ :

„Für Go 2 möchten wir die Fehlerprüfung leichter machen und die Menge an Go-Programmtext für die Fehlerprüfung reduzieren. Wir möchten auch das Schreiben von Fehlerbehandlungen bequemer machen und die Wahrscheinlichkeit erhöhen, dass Programmierer sich die Zeit dafür nehmen.

Sowohl die Fehlerprüfung als auch die Fehlerbehandlung müssen explizit, dh im Programmtext sichtbar bleiben. Wir wollen die Fallstricke der Ausnahmebehandlung nicht wiederholen.

Vorhandener Code muss weiter funktionieren und so gültig bleiben, wie er heute ist. Alle Änderungen müssen mit dem bestehenden Code zusammenarbeiten.“

Weitere Informationen zu „den Fallstricken der Ausnahmebehandlung“ finden Sie in der Diskussion im längeren Abschnitt „Problem“ . Insbesondere müssen die Fehlerprüfungen eindeutig dem Geprüften zugeordnet werden.

@mikeschinkel ,

Um es für die Nachwelt klarzustellen, das defer hat einen Verschluss, richtig? Wenn Sie von dieser Schließung zurückkehren, wird es – sofern ich das nicht falsch verstehe – nicht nur von der Schließung zurückkehren, sondern auch von dem func , wo der Fehler aufgetreten ist, richtig? _(Keine Notwendigkeit zu antworten, wenn ja.)_

Nein. Hier geht es nicht um Fehlerbehandlung, sondern um zurückgestellte Funktionen. Sie sind nicht immer Schließungen. Ein gängiges Muster ist zum Beispiel:

func (d *Data) Op() int {
    d.mu.Lock()
    defer d.mu.Unlock()

     ... code to implement Op ...
}

Jede Rückkehr von d.Op führt den verzögerten Unlock-Aufruf nach der return-Anweisung aus, aber bevor der Code an den Aufrufer von d.Op übertragen wird. Nichts, was innerhalb von d.mu.Unlock getan wird, wirkt sich auf den Rückgabewert von d.Op aus. Eine return-Anweisung in d.mu.Unlock gibt von der Entsperrung zurück. Es kehrt nicht von selbst aus d.Op zurück. Sobald d.mu.Unlock zurückkehrt, kehrt natürlich auch d.Op zurück, aber nicht direkt wegen d.mu.Unlock. Es ist ein subtiler Punkt, aber ein wichtiger.

Um zu deinem Beispiel zu kommen:

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

Zumindest wie geschrieben, ist dies ein ungültiges Programm. Ich versuche hier nicht pedantisch zu sein – die Details sind wichtig. Hier ist ein gültiges Programm:

func example() (err error) {
    defer func() {
        if err != nil {
            println("FAILED:", err.Error())
        }
    }()

    try(funcReturningError())
    return nil
}

Jedes Ergebnis eines verzögerten Funktionsaufrufs wird verworfen, wenn der Aufruf ausgeführt wird. Wenn es sich bei dem verzögerten Aufruf also um einen Abschluss handelt, macht es überhaupt keinen Sinn, den Abschluss so zu schreiben, dass er einen Wert zurückgibt. Wenn Sie also return err in den Closure-Body schreiben, sagt Ihnen der Compiler "too many arguments to return" .

Also, nein, das Schreiben return err kehrt weder von der zurückgestellten Funktion noch von der äußeren Funktion im eigentlichen Sinne zurück, und im herkömmlichen Sprachgebrauch ist es nicht einmal möglich, Code zu schreiben, der dies zu tun scheint.

Viele der zu dieser Ausgabe geposteten Gegenvorschläge, die andere, leistungsfähigere Fehlerbehandlungskonstrukte vorschlagen, duplizieren vorhandene Sprachkonstrukte, wie die if-Anweisung. (Oder sie widersprechen dem Ziel , „Fehlerüberprüfungen leichter zu machen und die Menge an Go-Programmtext für die Fehlerüberprüfung zu reduzieren.“ Oder beides.)

Im Allgemeinen hat Go bereits ein perfekt fähiges Konstrukt zur Fehlerbehandlung: die gesamte Sprache, insbesondere if-Anweisungen. @DavexPro verwies zu Recht auf den Go-Blogeintrag Errors are values ​​. Wir müssen keine ganze separate Untersprache entwerfen, die sich mit Fehlern befasst, und das sollten wir auch nicht. Ich denke, die wichtigste Erkenntnis des letzten halben Jahres war, „handle“ aus dem „check/handle“-Vorschlag zu entfernen, um die Sprache wiederzuverwenden, die wir bereits haben, einschließlich des Rückgriffs auf if-Anweisungen, wo dies angemessen ist. Diese Beobachtung, so wenig wie möglich zu tun, schließt die meisten Ideen zur weiteren Parametrisierung eines neuen Konstrukts aus.

Mit Dank an @brynbellomy für seine vielen guten Kommentare werde ich sein try-else als anschauliches Beispiel verwenden. Ja, wir könnten schreiben:

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

aber alles in allem ist dies wahrscheinlich keine signifikante Verbesserung gegenüber der Verwendung bestehender Sprachkonstrukte:

func doSomething() (int, error) {
    a, b, err := SomeFunc()
    if err != nil {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    logAndContinue := func(err error) {
        log.Errorf("non-critical error: %v", err)
    }
    annotate:= func(err error) (int, error) {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d, err := SomeFunc()
    if err != nil {
        logAndContinue(err)
    }
    e, f, err := SomeFunc()
    if err != nil {
        return annotate(err)
    }

    // ...

    return 123, nil
}

Das heißt, es scheint vorzuziehen, sich weiterhin auf die vorhandene Sprache zu verlassen, um Fehlerbehandlungslogik zu schreiben, anstatt eine neue Anweisung zu erstellen, sei es try-else, try-goto, try-arrow oder irgendetwas anderes.

Aus diesem Grund ist try auf die einfache Semantik if err != nil { return ..., err } beschränkt und nicht mehr: Verkürzen Sie das eine gemeinsame Muster, aber versuchen Sie nicht, alle möglichen Kontrollflüsse neu zu erfinden. Wenn eine if-Anweisung oder eine Hilfsfunktion angemessen ist, erwarten wir voll und ganz, dass die Leute sie weiterhin verwenden.

@rsc Danke für die Klarstellung.

Richtig, ich habe die Details nicht richtig verstanden. Ich schätze, ich verwende defer nicht oft genug, um mich an seine Syntax zu erinnern.

_(FWIW Ich finde die Verwendung defer für etwas Komplexeres als das Schließen eines Dateihandles weniger offensichtlich, da in func vor der Rückkehr rückwärts gesprungen wird. Fügen Sie diesen Code also immer einfach am Ende von ein func nach for range once{...} ist mein Fehlerbehandlungscode break aus.)_

Der Vorschlag, jeden try-Aufruf in mehrere Zeilen zu gofmt, widerspricht direkt dem Ziel , „Fehlerprüfungen leichter zu machen und die Menge an Go-Programmtext für die Fehlerprüfung zu reduzieren“.

Auch der Vorschlag, eine if-Anweisung zur Fehlerprüfung in einer einzigen Zeile zu verwenden, steht diesem Ziel direkt entgegen. Die Fehlerprüfungen werden durch das Entfernen der inneren Zeilenumbruchzeichen weder wesentlich leichter noch in der Menge reduziert. Wenn überhaupt, werden sie schwieriger zu überfliegen.

Der Hauptvorteil von try besteht darin, eine eindeutige Abkürzung für den einen häufigsten Fall zu haben, wodurch die ungewöhnlichen Fälle besser hervorstechen und es wert sind, sorgfältig gelesen zu werden.

Von gofmt zu allgemeinen Tools zurückkehrend, ist der Vorschlag, sich auf Tools zum Schreiben von Fehlerprüfungen zu konzentrieren, anstatt auf einen Sprachwechsel, ebenso problematisch. Wie Abelson und Sussman es ausdrückten: „Programme müssen geschrieben werden, damit Menschen sie lesen können, und nur nebenbei, damit Maschinen sie ausführen können.“ Wenn Werkzeugmaschinen _erforderlich_ sind, um mit der Sprache fertig zu werden, dann erfüllt die Sprache ihre Aufgabe nicht. Die Lesbarkeit darf nicht auf Personen beschränkt sein, die bestimmte Tools verwenden.

Einige Leute führten die Logik in die entgegengesetzte Richtung: Leute können komplexe Ausdrücke schreiben, also werden sie es unweigerlich tun, also bräuchten Sie IDE oder andere Tool-Unterstützung, um die try-Ausdrücke zu finden, also ist try eine schlechte Idee. Es gibt hier jedoch ein paar nicht unterstützte Sprünge. Die wichtigste ist die Behauptung, dass, weil es _möglich_ ist, komplexen, unlesbaren Code zu schreiben, solcher Code allgegenwärtig werden wird. Wie @josharian feststellte, ist es bereits „ möglich, abscheulichen Code in Go zu schreiben “. Das ist nicht alltäglich, weil Entwickler Normen haben, wie sie versuchen, den am besten lesbaren Weg zu finden, um ein bestimmtes Stück Code zu schreiben. Es ist also gewiss _nicht_ der Fall, dass IDE-Unterstützung benötigt wird, um Programme zu lesen, die try beinhalten. Und in den wenigen Fällen, in denen Leute wirklich schrecklichen Code schreiben und versuchen, es zu missbrauchen, wird die IDE-Unterstützung wahrscheinlich nicht viel nützen. Dieser Einwand – Leute können mit dem neuen Feature sehr schlechten Code schreiben – wird in so ziemlich jeder Diskussion über jedes neue Sprachfeature in jeder Sprache vorgebracht. Es ist nicht sehr hilfreich. Ein hilfreicherer Einwand wäre von der Form „Leute werden Code schreiben, der zunächst gut erscheint, sich aber aus diesem unerwarteten Grund als weniger gut herausstellt“, wie in der Diskussion über das Debuggen von Drucken .

Nochmals: Die Lesbarkeit darf nicht auf Personen beschränkt sein, die bestimmte Tools verwenden.
(Ich drucke und lese Programme immer noch auf Papier, obwohl mich die Leute dafür oft komisch angucken.)

Vielen Dank an @rsc für Ihre Gedanken darüber, ob if -Anweisungen als einzelne Zeile gofmt werden können.

Auch der Vorschlag, eine if-Anweisung zur Fehlerprüfung in einer einzigen Zeile zu verwenden, steht diesem Ziel direkt entgegen. Die Fehlerprüfungen werden durch das Entfernen der inneren Zeilenumbruchzeichen weder wesentlich leichter noch in der Menge reduziert. Wenn überhaupt, werden sie schwieriger zu überfliegen.

Ich schätze diese Behauptungen anders ein.

Ich finde, die Anzahl der Linien von 3 auf 1 zu reduzieren, ist wesentlich leichter. Wäre es nicht wesentlich schwerer, eine if-Anweisung zu verlangen, die beispielsweise 9 (oder sogar 5) Zeilenumbrüche anstelle von 3 enthält? Es ist derselbe Faktor (Menge) der Reduzierung/Erweiterung. Ich würde argumentieren, dass Struct-Literale genau diesen Kompromiss haben und mit der Hinzufügung von try genauso viel Kontrollfluss ermöglichen wie eine if -Anweisung.

Zweitens finde ich das Argument, dass sie schwieriger zu überfliegen sind, genauso gut auf try anzuwenden, wenn nicht sogar noch mehr. Mindestens eine if -Anweisung müsste in einer eigenen Zeile stehen. Aber vielleicht verstehe ich falsch, was in diesem Zusammenhang mit "überfliegen" gemeint ist. Ich benutze es, um zu bedeuten "meistens überspringen, aber sich dessen bewusst sein".

Alles in allem basierte der Vorschlag von gofmt auf einem noch konservativeren Schritt als try und hat keine Auswirkungen auf try , es sei denn, es wäre ausreichend. Es hört sich so an, als wäre es das nicht, und wenn ich es weiter diskutieren möchte, werde ich ein neues Problem / einen neuen Vorschlag eröffnen. :+1:

Ich finde, die Anzahl der Linien von 3 auf 1 zu reduzieren, ist wesentlich leichter.

Ich denke, jeder ist sich einig, dass Code zu dicht sein kann. Wenn Ihr gesamtes Paket beispielsweise aus einer Zeile besteht, sind wir uns alle einig, dass dies ein Problem darstellt. Wir sind uns wahrscheinlich alle nicht einig über die genaue Linie. Für mich haben wir uns etabliert

n, err := src.Read(buf)
if err == io.EOF {
    return nil
} else if err != nil {
    return err
}

als Weg, diesen Code zu formatieren, und ich denke, es wäre ziemlich verwirrend, zu versuchen, zu Ihrem Beispiel zu wechseln

n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

stattdessen. Wenn wir so angefangen hätten, wäre es sicher in Ordnung gewesen. Aber wir haben es nicht getan, und es ist nicht dort, wo wir jetzt sind.

Ich persönlich finde das ehemalige leichtere Gewicht auf der Seite in dem Sinne, dass es einfacher zu überfliegen ist. Sie können das if-else auf einen Blick sehen, ohne irgendwelche tatsächlichen Buchstaben zu lesen. Im Gegensatz dazu ist die dichtere Version aus einer Folge von drei Aussagen auf den ersten Blick schwer zu erkennen, sodass man genauer hinschauen muss, bevor die Bedeutung klar wird.

Am Ende ist es in Ordnung, wenn wir die Linie zwischen Dichte und Lesbarkeit an verschiedenen Stellen ziehen, was die Anzahl der Zeilenumbrüche betrifft. Der Versuchsvorschlag konzentriert sich darauf, nicht nur Zeilenumbrüche zu entfernen, sondern die Konstrukte vollständig zu entfernen, und das erzeugt eine leichtere Seitenpräsenz, die von der gofmt-Frage getrennt ist.

Einige Leute führten die Logik in die entgegengesetzte Richtung: Leute können komplexe Ausdrücke schreiben, also werden sie es unweigerlich tun, also bräuchten Sie IDE oder andere Tool-Unterstützung, um die try-Ausdrücke zu finden, also ist try eine schlechte Idee. Es gibt hier jedoch ein paar nicht unterstützte Sprünge. Die wichtigste ist die Behauptung, dass, weil es _möglich_ ist, komplexen, unlesbaren Code zu schreiben, solcher Code allgegenwärtig werden wird. Wie @josharian feststellte, ist es bereits „ möglich, abscheulichen Code in Go zu schreiben “. Das ist nicht alltäglich, weil Entwickler Normen haben, wie sie versuchen, den am besten lesbaren Weg zu finden, um ein bestimmtes Stück Code zu schreiben. Es ist also gewiss _nicht_ der Fall, dass IDE-Unterstützung benötigt wird, um Programme zu lesen, die try beinhalten. Und in den wenigen Fällen, in denen Leute wirklich schrecklichen Code schreiben und versuchen, es zu missbrauchen, wird die IDE-Unterstützung wahrscheinlich nicht viel nützen. Dieser Einwand – Leute können mit dem neuen Feature sehr schlechten Code schreiben – wird in so ziemlich jeder Diskussion über jedes neue Sprachfeature in jeder Sprache vorgebracht. Es ist nicht sehr hilfreich.

Ist das nicht der ganze Grund, warum Go keinen ternären Operator hat ?

Ist das nicht der ganze Grund, warum Go keinen ternären Operator hat?

Nein. Wir können und sollten unterscheiden zwischen „diese Funktion kann zum Schreiben von sehr gut lesbarem Code verwendet werden, kann aber auch missbraucht werden, um unlesbaren Code zu schreiben“ und „diese Funktion wird überwiegend zum Schreiben von nicht lesbarem Code verwendet“.

Erfahrung mit C legt nahe, dass ? : fällt direkt in die zweite Kategorie. (Mit der möglichen Ausnahme von min und max bin ich mir nicht sicher, ob ich jemals Code gesehen habe, der ? verwendet: das wurde nicht verbessert, indem es umgeschrieben wurde, um stattdessen eine if-Anweisung zu verwenden. Aber dieser Absatz geht vom Thema ab.)

Syntax

Diese Diskussion hat sechs verschiedene Syntaxen identifiziert, um dieselbe Semantik aus dem Vorschlag zu schreiben:

(Entschuldigung, wenn ich die Ursprungsgeschichten falsch verstanden habe!)

All dies hat Vor- und Nachteile, und das Schöne ist, dass es nicht so wichtig ist, zwischen den verschiedenen Syntaxen zu wählen, um weiter zu experimentieren, da sie alle dieselbe Semantik haben.

Ich fand dieses Beispiel von @brynbellomy zum Nachdenken anregend:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Es gibt natürlich keinen großen Unterschied zwischen diesen spezifischen Beispielen. Und wenn der Versuch in allen Linien vorhanden ist, warum nicht sie aneinanderreihen oder ausklammern? Ist das nicht sauberer? Darüber habe ich mich auch gewundert.

Aber wie @ianlancetaylor bemerkte : „Der Versuch begräbt die Lede. Code wird zu einer Reihe von try-Anweisungen, die verschleiern, was der Code tatsächlich tut.“

Ich denke, das ist ein kritischer Punkt: Den Versuch so aneinander zu reihen oder wie im Block auszuklammern, impliziert eine falsche Parallelität. Es impliziert, dass es bei diesen Aussagen wichtig ist, dass sie es alle versuchen. Das ist normalerweise nicht das Wichtigste am Code und nicht das, worauf wir uns beim Lesen konzentrieren sollten.

Nehmen Sie der Argumentation halber an, dass AsCommit niemals fehlschlägt und folglich keinen Fehler zurückgibt. Jetzt haben wir:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Was Sie auf den ersten Blick sehen, ist, dass sich die mittleren beiden Linien deutlich von den anderen unterscheiden. Warum? Es stellt sich heraus, wegen der Fehlerbehandlung. Ist das das wichtigste Detail dieses Codes, das Ihnen auf den ersten Blick auffallen sollte? Meine Antwort ist nein. Ich denke, Sie sollten zuerst die Kernlogik dessen beachten, was das Programm tut, und die Fehlerbehandlung später. In diesem Beispiel verhindern die try-Anweisung und der try-Block diese Ansicht der Kernlogik. Für mich deutet dies darauf hin, dass sie nicht die richtige Syntax für diese Semantik sind.

Damit bleiben die ersten vier Syntaxen übrig, die einander noch ähnlicher sind:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)

// vs

headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?

// vs

headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)

Es ist schwer, sich zu sehr darüber aufzuregen, eine der anderen vorzuziehen. Sie alle haben ihre guten und schlechten Seiten. Die wichtigsten Vorteile des eingebauten Formulars sind:

(1) Der genaue Operand ist sehr klar, insbesondere im Vergleich zum Präfix-Operator try x.y().z() .
(2) Tools, die try nicht kennen müssen, können es als einfachen Funktionsaufruf behandeln, sodass goimports beispielsweise ohne Anpassungen gut funktioniert, und
(3) es gibt Raum für zukünftige Erweiterungen und Anpassungen, falls erforderlich.

Es ist durchaus möglich, dass wir, nachdem wir echten Code gesehen haben, der diese Konstrukte verwendet, ein besseres Gefühl dafür entwickeln, ob die Vorteile einer der anderen drei Syntaxen diese Vorteile der Funktionsaufrufsyntax überwiegen. Das können uns nur Experimente und Erfahrungen sagen.

Danke für die ganze Aufklärung. Je mehr ich nachdenke, desto mehr gefällt mir der Vorschlag und ich sehe, wie er zu den Zielen passt.

Warum nicht eine Funktion wie recover() anstelle von err verwenden, von der wir nicht wissen, woher sie kommt? Es wäre konsistenter und vielleicht einfacher zu implementieren.

func f() error {
 defer func() {
   if err:=error();err!=nil {
     ...
   }
 }()
}

Bearbeiten: Ich verwende nie benannte Rückgabe, dann wird es seltsam für mich sein, benannte Rückgabe nur dafür hinzuzufügen

@flibustenet , siehe auch https://swtch.com/try.html#named für einige ähnliche Vorschläge.
(Um alle zu beantworten: Wir könnten das tun, aber es ist angesichts benannter Ergebnisse nicht unbedingt erforderlich, also könnten wir genauso gut versuchen, das vorhandene Konzept zu verwenden, bevor wir entscheiden, dass wir einen zweiten Weg anbieten müssen.)

Eine unbeabsichtigte Folge von try() kann sein, dass Projekte _go fmt_ aufgeben, um einzeilige Fehlerprüfungen zu erhalten. Das sind fast alle Vorteile von try() ohne die Kosten. Ich mache das seit ein paar Jahren; Es funktioniert gut.

Aber ich würde lieber in der Lage sein , einen Fehlerbehandler als letzten Ausweg für das Paket zu definieren und alle Fehlerprüfungen zu eliminieren, die ihn benötigen. Was ich definieren würde, ist nicht try() .

@networkimprov , Sie scheinen aus einer anderen Position zu kommen als die Go-Benutzer, auf die wir abzielen, und Ihre Nachricht würde mehr zur Konversation beitragen, wenn sie zusätzliche Details oder Links enthalten würde, damit wir Ihren Standpunkt besser verstehen können.

Es ist unklar, welche "Kosten" Ihrer Meinung nach der Versuch hat. Und während Sie sagen, dass das Aufgeben von gofmt "keine der Kosten" des Versuchs (was auch immer das sein mag) verursacht, scheinen Sie zu ignorieren, dass die Formatierung von gofmt diejenige ist, die von allen Programmen verwendet wird, die helfen, den Go-Quellcode neu zu schreiben, wie goimports, z. B. gorename , und so weiter. Sie verzichten auf go fmt auf Kosten dieser Helfer oder nehmen zumindest beträchtliche zufällige Änderungen an Ihrem Code in Kauf, wenn Sie sie aufrufen. Trotzdem, wenn es für Sie gut funktioniert, ist das großartig: Machen Sie auf jeden Fall weiter so.

Es ist auch unklar, was "Definieren eines Fehlerbehandlers als letztes Mittel für das Paket" bedeutet oder warum es angemessen wäre, eine Fehlerbehandlungsrichtlinie auf ein gesamtes Paket anstatt auf eine einzelne Funktion gleichzeitig anzuwenden. Wenn Sie in einer Fehlerbehandlungsroutine hauptsächlich Kontext hinzufügen möchten, wäre derselbe Kontext nicht für das gesamte Paket geeignet.

@rsc , wie Sie vielleicht gesehen haben, kehrte ich, während ich die Try-Block-Syntax vorschlug, später zur "Nein"-Seite für diese Funktion zurück - teilweise, weil ich mich unwohl fühle, eine oder mehrere bedingte Fehlerrückgaben in einer Anweisung oder Funktionsanwendung zu verstecken. Aber lassen Sie mich einen Punkt klarstellen. Im Try-Block-Vorschlag habe ich ausdrücklich Anweisungen zugelassen, die try nicht benötigen . Ihr letztes Try-Block-Beispiel wäre also:

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
        parentCommitOne := parentObjOne.AsCommit()
        parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Dies besagt einfach, dass alle innerhalb des try-Blocks zurückgegebenen Fehler an den Aufrufer zurückgegeben werden. Wenn die Steuerung den Try-Block passiert, gab es keine Fehler in dem Block.

Du sagtest

Ich denke, Sie sollten zuerst die Kernlogik dessen beachten, was das Programm tut, und die Fehlerbehandlung später.

Genau aus diesem Grund dachte ich an einen Try-Block! Herausgerechnet wird nicht nur das Schlüsselwort, sondern auch die Fehlerbehandlung. Ich möchte nicht über N verschiedene Stellen nachdenken müssen, die Fehler generieren können (außer wenn ich ausdrücklich versuche, bestimmte Fehler zu behandeln).

Einige weitere Punkte, die erwähnenswert sein könnten:

  1. Der Anrufer weiß nicht, wo genau der Fehler beim Angerufenen herkommt. Dies gilt auch für den einfachen Vorschlag, den Sie im Allgemeinen in Betracht ziehen. Ich habe spekuliert, dass der Compiler dazu gebracht werden kann, seine eigene Anmerkung zum Fehlerrückgabepunkt hinzuzufügen. Aber ich habe nicht viel darüber nachgedacht.
  2. Mir ist nicht klar, ob Ausdrücke wie try(try(foo(try(bar)).fum()) erlaubt sind. Eine solche Verwendung mag verpönt sein, aber ihre Semantik muss spezifiziert werden. Im try-Block-Fall muss der Compiler härter arbeiten, um solche Verwendungen zu erkennen und die gesamte Fehlerbehandlung auf die Ebene des try-Blocks zu pressen.
  3. Ich neige eher dazu, return-on-error statt try zu mögen. Dies ist auf Blockebene einfacher zu schlucken!
  4. Auf der anderen Seite machen lange Schlüsselwörter die Dinge weniger lesbar.

FWIW, ich glaube immer noch nicht, dass sich das lohnt.

@rsc

[...]
Die wichtigste ist die Behauptung, dass, weil es möglich ist, komplexen, unlesbaren Code zu schreiben, solcher Code allgegenwärtig werden wird. Wie @josharian feststellte, ist es bereits „möglich, abscheulichen Code in Go zu schreiben“.
[...]

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())

Ich verstehe Ihre Position zu "schlechtem Code", dass wir heute schrecklichen Code wie den folgenden Block schreiben können.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Was halten Sie davon, verschachtelte try -Aufrufe zu verbieten, damit wir nicht versehentlich schlechten Code schreiben können?

Wenn Sie verschachtelte try in der ersten Version nicht zulassen, können Sie diese Einschränkung später bei Bedarf entfernen, andersherum wäre es nicht möglich.

Ich habe diesen Punkt bereits besprochen, aber er scheint relevant zu sein - die Codekomplexität sollte vertikal skaliert werden, nicht horizontal.

try als Ausdruck fördert die horizontale Skalierung der Codekomplexität, indem verschachtelte Aufrufe gefördert werden. try als Anweisung fördert die vertikale Skalierung der Codekomplexität.

@rsc , zu deinen Fragen,

Mein Handler auf Paketebene als letzter Ausweg - wenn kein Fehler zu erwarten ist:

func quit(err error) {
   fmt.Fprintf(os.Stderr, "quit after %s\n", err)
   debug.PrintStack()      // because panic(err) produces a pile of noise
   os.Exit(3)
}

Kontext: Ich mache intensiven Gebrauch von os.File (wo ich zwei Fehler gefunden habe: #26650 & #32088)

Ein Decorator auf Paketebene, der grundlegenden Kontext hinzufügt, würde ein caller -Argument benötigen – eine generierte Struktur, die die Ergebnisse von runtime.Caller() bereitstellt.

Ich wünschte, der _go fmt_-Rewriter würde vorhandene Formatierungen verwenden oder Sie die Formatierung pro Transformation angeben lassen. Ich begnüge mich mit anderen Tools.

Die Kosten (dh Nachteile) von try() sind oben gut dokumentiert.

Ich bin ehrlich gesagt platt, dass das Go-Team uns zuerst check/handle (wohltätig, eine neuartige Idee) und dann die ternaryesken try() angeboten hat. Ich verstehe nicht, warum Sie keine RFP zur Fehlerbehandlung herausgegeben und dann Community-Kommentare zu einigen der resultierenden Vorschläge gesammelt haben (siehe #29860). Es gibt eine Menge Weisheit hier draußen, die Sie nutzen könnten!

@rsc

Syntax

Diese Diskussion hat sechs verschiedene Syntaxen identifiziert, um dieselbe Semantik aus dem Vorschlag zu schreiben:

try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

... und meiner Meinung nach die Lesbarkeit (durch Alliteration) sowie die semantische Genauigkeit verbessern:

f, err := os.Open(file)
relay err

oder

f, err := os.Open(file)
relay err wrap

oder

f, err := os.Open(file)
relay err wrap { a, b }

oder

f, err := os.Open(file)
relay err { a, b }

Ich weiß, dass das Befürworten von Relay versus Try leicht als Off-Topic abgetan werden kann, aber ich kann mir gut vorstellen, zu erklären, dass Try nichts versucht und nichts wirft. Es ist nicht klar UND hat Gepäck. relay wäre ein neuer Begriff, der eine klare Erklärung erlauben würde, und die Beschreibung hat eine Grundlage in der Schaltung (worum es sowieso geht).

Edit zur Verdeutlichung:
Versuchen kann bedeuten - 1. etwas zu erleben und dann subjektiv zu beurteilen 2. etwas objektiv zu verifizieren 3. etwas zu versuchen 4. mehrere Kontrollflüsse abzufeuern, die unterbrochen werden können, und gegebenenfalls eine abfangbare Benachrichtigung zu starten

In diesem Vorschlag tut try nichts davon. Wir führen tatsächlich eine Funktion aus. Es leitet dann den Steuerfluss basierend auf einem Fehlerwert neu. Dies ist buchstäblich die Definition eines Schutzrelais. Wir schalten Schaltungen direkt um (dh schließen den aktuellen Funktionsumfang kurz) entsprechend dem Wert eines getesteten Fehlers.

Im try-Block-Vorschlag habe ich ausdrücklich Anweisungen zugelassen, die try nicht benötigen

Der Hauptvorteil der Fehlerbehandlung von Go, den ich gegenüber dem Try-Catch-System von Sprachen wie Java und Python sehe, ist, dass immer klar ist, welche Funktionsaufrufe zu einem Fehler führen können und welche nicht. Das Schöne an try , wie es im ursprünglichen Vorschlag dokumentiert ist, ist, dass es einfache Fehlerbehandlungsbausteine ​​einsparen kann, während dieses wichtige Merkmal weiterhin beibehalten wird.

Um von den Beispielen von @Goodwine zu leihen, trotz seiner Hässlichkeit, aus Sicht der Fehlerbehandlung sogar dies:

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

... ist besser als das, was Sie oft in Try-Catch-Sprachen sehen

parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()

... weil Sie immer noch erkennen können, welche Teile des Codes den Kontrollfluss aufgrund eines Fehlers umleiten können und welche nicht.

Ich weiß, dass @bakul diesen Blocksyntax-Vorschlag sowieso nicht befürwortet, aber ich denke, er bringt einen interessanten Punkt zur Fehlerbehandlung von Go im Vergleich zu anderen hervor. Ich denke, es ist wichtig, dass jeder Vorschlag zur Fehlerbehandlung, den Go annimmt, nicht verschleiern sollte, welche Teile des Codes fehlerhaft sein können und welche nicht.

Ich habe ein kleines Tool geschrieben: tryhard (das sich im Moment nicht sehr bemüht) arbeitet auf einer Datei-für-Datei-Basis und verwendet einen einfachen AST-Musterabgleich, um potenzielle Kandidaten für try zu erkennen Dokumentation für Details.

Die Anwendung auf $GOROOT/src at tip meldet > 5000 (!) Gelegenheiten für try . Es mag viele Fehlalarme geben, aber die Überprüfung einer anständigen Probe von Hand deutet darauf hin, dass die meisten Möglichkeiten real sind.

Die Verwendung der Rewrite-Funktion zeigt, wie der Code mit try aussehen wird. Auch hier zeigt ein flüchtiger Blick auf die Ausgabe meiner Meinung nach eine deutliche Verbesserung.

( Achtung: Die Rewrite-Funktion zerstört Dateien! Nutzung auf eigene Gefahr. )

Hoffentlich gibt dies einen konkreten Einblick, wie Code mit try aussehen könnte, und lässt uns über müßige und unproduktive Spekulationen hinausgehen.

Danke & viel Spaß.

Ich verstehe Ihre Position zu "schlechtem Code", dass wir heute schrecklichen Code wie den folgenden Block schreiben können.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Meine Position ist, dass Go-Entwickler gute Arbeit leisten, wenn sie klaren Code schreiben, und dass der Compiler mit ziemlicher Sicherheit nicht das Einzige ist, was Sie oder Ihre Kollegen daran hindert, Code zu schreiben, der so aussieht.

Was halten Sie davon, verschachtelte try -Aufrufe zu verbieten, damit wir nicht versehentlich schlechten Code schreiben können?

Ein großer Teil der Einfachheit von Go stammt von der Auswahl orthogonaler Merkmale, die sich unabhängig voneinander zusammensetzen. Das Hinzufügen von Einschränkungen bricht Orthogonalität, Zusammensetzbarkeit, Unabhängigkeit und damit die Einfachheit.

Heute ist es eine Regel, dass, wenn Sie:

x := expression
y := f(x)

ohne andere Verwendung von x irgendwo, dann ist es eine gültige Programmtransformation, um dies zu vereinfachen

y := f(expression)

Wenn wir eine Einschränkung für try-Ausdrücke übernehmen würden, würde dies jedes Tool beschädigen, das davon ausgeht, dass dies immer eine gültige Transformation ist. Oder wenn Sie einen Codegenerator hätten, der mit Ausdrücken arbeitet und try-Ausdrücke verarbeiten könnte, müsste er sich alle Mühe geben, Temporäre einzuführen, um die Einschränkungen zu erfüllen. Und so weiter und so weiter.

Kurz gesagt, Einschränkungen erhöhen die Komplexität erheblich. Sie brauchen eine deutliche Rechtfertigung, nicht "mal sehen, ob jemand gegen diese Mauer stößt und uns bittet, sie abzubauen".

Eine längere Erklärung habe ich vor zwei Jahren unter https://github.com/golang/go/issues/18130#issuecomment -264195616 (im Zusammenhang mit Type Aliases) geschrieben, die hier genauso gut zutrifft.

@Bakul ,

Aber lassen Sie mich einen Punkt klarstellen. Im Try-Block-Vorschlag habe ich ausdrücklich Anweisungen zugelassen, die try _nicht benötigen_.

Damit würde das zweite Ziel verfehlt : "Sowohl Fehlerprüfungen als auch Fehlerbehandlung müssen explizit, also im Programmtext sichtbar bleiben. Wir wollen die Fallstricke der Ausnahmebehandlung nicht wiederholen."

Die Hauptfalle der traditionellen Ausnahmebehandlung besteht darin, nicht zu wissen, wo sich die Prüfungen befinden. Erwägen:

try {
    s = canThrowErrors()
    t = cannotThrowErrors()
    u = canThrowErrors() // a second call
} catch {
    // how many ways can you get here?
}

Wenn die Funktionen nicht so hilfreich benannt wurden, kann es sehr schwierig sein, zu sagen, welche Funktionen fehlschlagen und welche garantiert erfolgreich sein werden, was bedeutet, dass Sie nicht leicht darüber nachdenken können, welche Codefragmente durch eine Ausnahme unterbrochen werden können und welche nicht.

Vergleichen Sie dies mit dem Ansatz von Swift , wo sie einen Teil der traditionellen Ausnahmebehandlungssyntax übernehmen, aber tatsächlich eine Fehlerbehandlung durchführen, mit einer expliziten Markierung für jede überprüfte Funktion und ohne Möglichkeit, sich über den aktuellen Stapelrahmen hinaus zu entspannen:

do {
    let s = try canThrowErrors()
    let t = cannotThrowErrors()
    let u = try canThrowErrors() // a second call
} catch {
    handle error from try above
}

Ob Rust oder Swift oder dieser Vorschlag, die entscheidende Verbesserung gegenüber der Ausnahmebehandlung besteht darin, im Text – selbst mit einem sehr leichten Marker – jede Stelle, an der sich ein Häkchen befindet, explizit zu markieren.

Weitere Informationen zum Problem der impliziten Prüfungen finden Sie im Abschnitt Problem der Problemübersicht vom letzten August, insbesondere in den Links zu den beiden Artikeln von Raymond Chen.

Bearbeiten: Siehe auch den dritten Kommentar von @velovix , der hereinkam, während ich an diesem arbeitete.

@daved , ich bin froh, dass die Analogie "Schutzrelais" für Sie funktioniert. Es funktioniert nicht für mich. Programme sind keine Schaltungen.

Jedes Wort kann missverstanden werden:
"break" unterbricht Ihr Programm nicht.
"Continue" setzt die Ausführung nicht wie gewohnt bei der nächsten Anweisung fort.
"goto" ... gut, goto ist eigentlich unmöglich zu missverstehen. :-)

https://www.google.com/search?q=define+try sagt „versuchen oder bemühen sich, etwas zu tun“ und „vorbehaltlich einer Prüfung“. Beides gilt für "f := try(os.Open(file))". Es versucht, os.Open auszuführen (oder es testet das Fehlerergebnis), und wenn der Versuch (oder das Fehlerergebnis) fehlschlägt, kehrt es von der Funktion zurück.

Wir haben letzten August Schecks verwendet. Das war auch ein gutes Wort. Wir sind trotz des historischen Ballasts von C++/Java/Python zu try gewechselt, weil die aktuelle Bedeutung von try in diesem Vorschlag mit der Bedeutung in Swifts try (ohne das umgebende do-catch) und in Rusts ursprünglichem try übereinstimmt! . Es wird nicht schlimm sein, wenn wir später entscheiden, dass Scheck doch das richtige Wort ist, aber im Moment sollten wir uns auf andere Dinge als den Namen konzentrieren.

Hier ist ein interessantes tryhard falsch negatives Ergebnis von github.com/josharian/pct . Ich erwähne es hier, weil:

  • es zeigt, wie schwierig die automatische try -Erkennung ist
  • es zeigt, dass die visuellen Kosten von if err != nil sich darauf auswirken, wie Leute (zumindest ich) ihren Code strukturieren, und dass try dabei helfen können

Vor:

var err error
switch {
case *flagCumulative:
    _, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
    _, err = fmt.Fprintln(w, line.s)
default:
    _, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
    return err
}

Nach (manuelles Umschreiben):

switch {
case *flagCumulative:
    try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
    try(fmt.Fprintln(w, line.s))
default:
    try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}

Änderung https://golang.org/cl/182717 erwähnt dieses Problem: src: apply tryhard -r $GOROOT/src

Eine visuelle Vorstellung von try in der std-Bibliothek finden Sie unter CL 182717 .

Danke, @josharian , dafür . Ja, es kann sogar für ein gutes Tool unmöglich sein, alle möglichen Verwendungskandidaten für try zu erkennen. Aber glücklicherweise ist das nicht das primäre Ziel (dieses Vorschlags). Ein Werkzeug zu haben ist nützlich, aber ich sehe den Hauptvorteil von try in Code, der noch nicht geschrieben ist (weil davon viel mehr als Code vorhanden sein wird, den wir bereits haben).

"break" unterbricht Ihr Programm nicht.
"Continue" setzt die Ausführung nicht wie gewohnt bei der nächsten Anweisung fort.
"goto" ... gut, goto ist eigentlich unmöglich zu missverstehen. :-)

break unterbricht die Schleife. continue setzt die Schleife fort und goto geht zum angegebenen Ziel. Letztendlich verstehe ich Sie, aber denken Sie bitte darüber nach, was passiert, wenn eine Funktion meistens abgeschlossen wird und einen Fehler zurückgibt, aber kein Rollback durchführt. Es war kein Versuch. Ich denke, check ist in dieser Hinsicht weit überlegen (den Fortschritt von "durch "Untersuchung" zu "stoppen" ist sicherlich angemessen).

Relevanter bin ich neugierig auf die Form von try/check, die ich im Gegensatz zu den anderen Syntaxen angeboten habe.
try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

Die Standardbibliothek ist letztendlich nicht repräsentativ für "echten" Go-Code, da sie nicht viel Zeit damit verbringt, andere Pakete zu koordinieren oder zu verbinden. Wir haben in der Vergangenheit festgestellt, dass dies der Grund dafür ist, dass die Kanalnutzung in der Standardbibliothek im Vergleich zu Paketen weiter oben in der Abhängigkeits-Nahrungskette so gering ist. Ich vermute, dass die Fehlerbehandlung und -weitergabe in dieser Hinsicht ähnlich wie bei den Kanälen ist: Sie werden mehr finden, je höher Sie gehen.

Aus diesem Grund wäre es für jemanden interessant, einige größere Anwendungscodebasen zu testen und zu sehen, welche lustigen Dinge in diesem Zusammenhang entdeckt werden können. (Die Standardbibliothek ist auch interessant, aber eher ein Mikrokosmos als eine genaue Stichprobe der Welt.)

Ich bin neugierig auf die Form von try/check, die ich im Gegensatz zu den anderen Syntaxen angeboten habe.

Ich denke , dass diese Form am Ende bestehende Kontrollstrukturen neu erschafft .

@networkimprov , re https://github.com/golang/go/issues/32437#issuecomment -502879351

Ich bin ehrlich gesagt platt, dass das Go-Team uns zuerst Check/Handle (wohltätig, eine neuartige Idee) und dann den ternaryesken try() angeboten hat. Ich verstehe nicht, warum Sie keine Ausschreibung zur Fehlerbehandlung herausgegeben und dann Community-Kommentare zu einigen der resultierenden Vorschläge gesammelt haben (siehe #29860). Es gibt eine Menge Weisheit hier draußen, die Sie nutzen könnten!

Wie wir in #29860 besprochen haben, sehe ich ehrlich gesagt keinen großen Unterschied zwischen dem, was Sie vorschlagen, dass wir in Bezug auf das Einholen von Community-Feedback hätten tun sollen, und dem, was wir tatsächlich getan haben. Auf der Entwurfsseite heißt es ausdrücklich, sie seien „Ausgangspunkte für Diskussionen mit dem Ziel, Entwürfe zu produzieren, die gut genug sind, um in tatsächliche Vorschläge umgewandelt zu werden“. Und die Leute haben viele Dinge geschrieben, von kurzen Rückmeldungen bis hin zu vollständigen alternativen Vorschlägen. Und das meiste davon war hilfreich und ich schätze Ihre Hilfe besonders beim Organisieren und Zusammenfassen. Sie scheinen darauf fixiert zu sein, ihm einen anderen Namen zu geben oder zusätzliche Bürokratieschichten einzuführen, für die wir, wie wir zu diesem Thema diskutiert haben, keinen wirklichen Bedarf sehen.

Aber bitte behaupte nicht, dass wir irgendwie nicht um Rat aus der Community gebeten oder ihn ignoriert hätten. Das stimmt einfach nicht.

Ich kann auch nicht sehen, wie Try in irgendeiner Weise "ternaryesque" ist, was auch immer das bedeuten würde.

Einverstanden, ich denke, das war mein Ziel; Komplexere Mechanismen halte ich nicht für sinnvoll. Wenn ich an Ihrer Stelle wäre, würde ich höchstens ein bisschen syntaktischen Zucker anbieten, um die meisten Beschwerden zum Schweigen zu bringen, und nicht mehr.

@rsc , Entschuldigung für das Abschweifen vom Thema!
Ich habe Handler auf Paketebene in https://github.com/golang/go/issues/32437#issuecomment -502840914 ausgelöst
und auf Ihre Bitte um Klarstellung unter https://github.com/golang/go/issues/32437#issuecomment -502879351 geantwortet

Ich sehe Handler auf Paketebene als ein Feature, hinter dem sich praktisch jeder verstecken könnte.

Bitte verwenden Sie die syntax try {} catch{}, bauen Sie keine Räder mehr

Bitte verwenden Sie die syntax try {} catch{}, bauen Sie keine Räder mehr

Ich denke, es ist angemessen, bessere Räder zu bauen, wenn die Räder, die andere Leute benutzen, wie Quadrate geformt sind

@jimwei

Die auf Ausnahmen basierende Fehlerbehandlung ist möglicherweise ein bereits vorhandenes Rad, hat aber auch einige bekannte Probleme. Die Problemstellung im ursprünglichen Entwurfsentwurf leistet einen hervorragenden Beitrag, um diese Probleme zu skizzieren.

Um meinen eigenen weniger gut durchdachten Kommentar hinzuzufügen, finde ich es interessant, dass viele sehr erfolgreiche neuere Sprachen (nämlich Swift, Rust und Go) keine Ausnahmen eingeführt haben. Das sagt mir, dass die breitere Software-Community nach den vielen Jahren, in denen wir mit ihnen arbeiten mussten, Ausnahmen überdenkt.

Als Antwort auf https://github.com/golang/go/issues/32437#issuecomment -502837008 ( @rscs Kommentar zu try als Aussage)

Sie sprechen einen guten Punkt an. Es tut mir leid, dass ich diesen Kommentar irgendwie verpasst hatte, bevor ich diesen gemacht habe: https://github.com/golang/go/issues/32437#issuecomment -502871889

Ihre Beispiele mit try als Ausdruck sehen viel besser aus als die mit try als Anweisung. Die Tatsache, dass die Aussage mit try beginnt, macht sie tatsächlich viel schwerer zu lesen. Ich mache mir jedoch immer noch Sorgen, dass die Leute Aufrufe von Nest-try versuchen werden, um schlechten Code zu machen, da try als Ausdruck dieses Verhalten in meinen Augen wirklich _ermutigt_.

Ich glaube, ich würde diesen Vorschlag etwas mehr schätzen, wenn golint verschachtelte try -Aufrufe verbieten würde. Ich denke, dass das Verbot aller try -Aufrufe innerhalb anderer Ausdrücke ein bisschen zu streng ist, try als Ausdruck zu haben, hat seine Vorzüge.

In Anlehnung an Ihr Beispiel sieht selbst das Verschachteln von 2 try-Aufrufen ziemlich abscheulich aus, und ich kann sehen, wie Go-Programmierer dies tun, insbesondere wenn sie ohne Code-Reviewer arbeiten.

parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))

Das ursprüngliche Beispiel sah eigentlich ganz nett aus, aber dieses hier zeigt, dass das Verschachteln der try-Ausdrücke (sogar nur 2-tief) die Lesbarkeit des Codes wirklich drastisch beeinträchtigt. Das Verweigern verschachtelter try -Aufrufe würde auch beim Problem der „Debugging“ helfen, da es viel einfacher ist, ein try in ein if zu erweitern, wenn es sich außerhalb eines Ausdrucks befindet.

Nochmals, ich möchte fast sagen, dass ein try innerhalb eines Unterausdrucks durch golint gekennzeichnet werden sollte, aber ich denke, das könnte ein wenig zu streng sein. Es würde auch Code wie diesen kennzeichnen, was in meinen Augen in Ordnung ist:

x := 5 + try(strconv.Atoi(input))

Auf diese Weise erhalten wir beide Vorteile von try als Ausdruck, aber wir fördern nicht, der horizontalen Achse zu viel Komplexität hinzuzufügen.

Vielleicht wäre eine andere Lösung, dass golint nur maximal 1 try pro Anweisung zulassen sollte, aber es ist spät, ich werde müde und ich muss vernünftiger darüber nachdenken. Wie auch immer, ich stand diesem Vorschlag an einigen Stellen ziemlich negativ gegenüber, aber ich denke, ich kann ihn tatsächlich wirklich mögen, solange es einige golint -Standards gibt, die damit zusammenhängen.

@rsc

Wir können und sollten unterscheiden zwischen _„diese Funktion kann zum Schreiben von sehr gut lesbarem Code verwendet werden, kann aber auch missbraucht werden, um unlesbaren Code zu schreiben“_ und „die vorherrschende Verwendung dieser Funktion wird das Schreiben von unlesbarem Code sein“.
Erfahrung mit C legt nahe, dass ? : fällt direkt in die zweite Kategorie. (Mit der möglichen Ausnahme von min und max,

Was mir zuerst an try() – vs. try als Aussage – auffiel, war, wie ähnlich es in der Nestbarkeit dem ternären Operator war und doch wie gegensätzlich die Argumente für try() und gegen ternäre waren waren _(paraphrasiert):_

  • ternary: _"Wenn wir es zulassen, werden die Leute es verschachteln und das Ergebnis wird eine Menge schlechter Code sein"_ und dabei ignorieren, dass manche Leute damit besseren Code schreiben, vs.
  • try(): _"Sie können es verschachteln, aber wir bezweifeln, dass viele es tun werden, weil die meisten Leute guten Code schreiben wollen"_,

Bei allem Respekt, dieser rationale Unterschied zwischen den beiden fühlt sich so subjektiv an, dass ich um eine Selbstbeobachtung bitten und zumindest überlegen würde, ob Sie einen Unterschied für ein Feature, das Sie bevorzugen, im Vergleich zu einem Feature, das Sie nicht mögen, rationalisieren könnten. #Please_dont_shoot_the_messenger

_"Ich bin mir nicht sicher, ob ich jemals Code gesehen habe, der ? verwendet: Das wurde nicht verbessert, indem es umgeschrieben wurde, um stattdessen eine if-Anweisung zu verwenden. Aber dieser Absatz schweift vom Thema ab.)"_

In anderen Sprachen verbessere ich häufig Anweisungen, indem ich sie von einem if in einen ternären Operator umschreibe, z. B. von Code, den ich heute in PHP geschrieben habe:

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Vergleichen mit:

if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
    return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else { 
    return null;
}

Soweit es mich betrifft, ist ersteres gegenüber letzterem viel besser.

fwiw

Ich denke, die Kritik an diesem Vorschlag ist größtenteils auf die hohen Erwartungen zurückzuführen, die durch den vorherigen Vorschlag geweckt wurden, der viel umfassender gewesen wäre. Ich denke jedoch, dass solch hohe Erwartungen aus Gründen der Konstanz gerechtfertigt waren. Ich denke, was viele Leute gerne gesehen hätten, ist ein einziges, umfassendes Konstrukt zur Fehlerbehandlung, das in allen Anwendungsfällen nützlich ist.

Vergleichen Sie diese Funktion zum Beispiel mit der eingebauten Funktion append() . Append wurde erstellt, weil das Anhängen an Slice ein sehr häufiger Anwendungsfall war, und obwohl es möglich war, es manuell zu tun, war es auch leicht, es falsch zu machen. Jetzt erlaubt append() nicht nur ein, sondern viele Elemente oder sogar einen ganzen Slice anzuhängen, und es erlaubt sogar, einen String an einen []Byte-Slice anzuhängen. Es ist leistungsfähig genug, um alle Anwendungsfälle des Anhängens an ein Slice abzudecken. Und daher fügt niemand mehr Slices manuell an.

try() ist jedoch anders. Es ist nicht leistungsfähig genug, um es in allen Fällen der Fehlerbehandlung verwenden zu können. Und ich denke, das ist der schwerwiegendste Fehler dieses Vorschlags. Die eingebaute Funktion try() ist nur wirklich nützlich, in dem Sinne, dass sie Boilerplate reduziert, in den einfachsten Fällen, nämlich nur das Weitergeben eines Fehlers an den Aufrufer und mit einer defer-Anweisung, wenn alle Fehler der Funktion müssen auf die gleiche Weise gehandhabt werden.

Für eine komplexere Fehlerbehandlung müssen wir immer noch if err != nil {} verwenden. Dies führt dann zu zwei unterschiedlichen Stilen für die Fehlerbehandlung, wo es vorher nur einen gab. Wenn dieser Vorschlag alles ist, was uns bei der Fehlerbehandlung in Go hilft, dann denke ich, dass es besser wäre, nichts zu tun und die Fehlerbehandlung mit if so zu behandeln, wie wir es immer getan haben, denn zumindest ist dies konsistent und hatte den Vorteil "es gibt nur einen Weg, es zu tun".

@rsc , Entschuldigung für das Abschweifen vom Thema!
Ich habe Handler auf Paketebene in #32437 (Kommentar) ausgelöst.
und antwortete auf Ihre Bitte um Klarstellung in #32437 (Kommentar)

Ich sehe Handler auf Paketebene als ein Feature, hinter dem sich praktisch jeder verstecken könnte.

Ich sehe nicht, was das Konzept eines Pakets mit einer spezifischen Fehlerbehandlung verbindet. Es ist schwer vorstellbar, dass das Konzept eines Handlers auf Paketebene beispielsweise für net/http nützlich ist. In ähnlicher Weise kann ich mir, obwohl ich im Allgemeinen kleinere Pakete als net/http schreibe, keinen einzigen Anwendungsfall vorstellen, bei dem ich ein Konstrukt auf Paketebene für die Fehlerbehandlung bevorzugt hätte. Im Allgemeinen habe ich festgestellt, dass die Annahme, dass jeder seine Erfahrungen, Anwendungsfälle und Meinungen teilt, gefährlich ist :)

@beoran Ich glaube, dass dieser Vorschlag weitere Verbesserungen ermöglicht. Wie ein Decorator am letzten Argument von try(..., func(err) error) oder ein tryf(..., "context of my error: %w") ?

@flibustenet Während solche späteren Erweiterungen möglich sein könnten, scheint der Vorschlag, wie er jetzt ist, von solchen Erweiterungen abzuraten, vor allem, weil das Hinzufügen eines Fehlerhandlers mit defer überflüssig wäre.

Ich denke, das schwierige Problem besteht darin, eine umfassende Fehlerbehandlung zu erreichen, ohne die Funktionalität von defe zu duplizieren. Vielleicht könnte die defer-Anweisung selbst irgendwie verbessert werden, um eine einfachere Fehlerbehandlung in komplexeren Fällen zu ermöglichen ... Aber das ist ein anderes Thema.

https://github.com/golang/go/issues/32437#issuecomment -502975437

Dies führt dann zu zwei unterschiedlichen Stilen für die Fehlerbehandlung, wo es vorher nur einen gab. Wenn dieser Vorschlag alles ist, was uns bei der Fehlerbehandlung in Go hilft, dann denke ich, dass es besser wäre, nichts zu tun und die Fehlerbehandlung mit if so zu behandeln, wie wir es immer getan haben, denn zumindest ist dies konsistent und hatte den Vorteil "es gibt nur einen Weg, es zu tun".

@beoran Einverstanden. Aus diesem Grund habe ich vorgeschlagen, die überwiegende Mehrheit der Fehlerfälle unter dem Schlüsselwort try ( try und try / else ) zu vereinheitlichen. Obwohl die Syntax try / else uns gegenüber dem bestehenden if err != nil -Stil keine signifikante Reduzierung der Codelänge bringt, gibt sie uns Konsistenz mit dem try (kein else ) Fall. Diese beiden Fälle (try and try-else) decken wahrscheinlich die überwiegende Mehrheit der Fehlerbehandlungsfälle ab. Ich habe das der eingebauten No-Else-Version von try gegenübergestellt, die nur in Fällen gilt, in denen der Programmierer nichts tut, um den Fehler zu behandeln, außer zurückzugeben (was, wie andere in diesem Thread erwähnt haben, ist nicht unbedingt etwas, das wir überhaupt erst fördern wollen).

Konsistenz ist wichtig für die Lesbarkeit.

append ist die definitive Art, einem Slice Elemente hinzuzufügen. make ist der definitive Weg, um einen neuen Kanal, eine neue Karte oder ein neues Slice zu erstellen (mit Ausnahme von Literalen, von denen ich nicht begeistert bin). Aber try() (als eingebaute Funktion und ohne else ) würde über Codebasen verteilt werden, je nachdem, wie der Programmierer mit einem bestimmten Fehler umgehen muss, auf eine Weise, die wahrscheinlich etwas chaotisch und verwirrend ist der Leser. Es scheint nicht im Sinne der anderen Builtins zu sein (nämlich einen Fall zu handhaben, der entweder ziemlich schwierig oder gar nicht anders zu machen ist). Wenn dies die erfolgreiche Version von try ist, werden mich Konsistenz und Lesbarkeit dazu zwingen, sie nicht zu verwenden, genauso wie ich versuche, Map/Slice-Literale zu vermeiden (und new wie die Pest zu vermeiden).

Wenn es darum geht, den Umgang mit Fehlern zu ändern, scheint es ratsam, zu versuchen, den Ansatz für so viele Fälle wie möglich zu vereinheitlichen, anstatt etwas hinzuzufügen, das bestenfalls „nimm es oder lass es“ ist. Ich befürchte, letzteres wird tatsächlich Rauschen hinzufügen, anstatt es zu reduzieren.

@deanveloper schrieb:

Ich denke, ich würde diesen Vorschlag ein bisschen mehr schätzen, wenn Golint verschachtelte Try-Aufrufe verbieten würde.

Ich stimme zu, dass tief verschachtelte try schwer zu lesen sein könnten. Aber das gilt auch für Standardfunktionsaufrufe, nicht nur für die eingebaute Funktion try . Daher sehe ich nicht ein, warum golint dies verbieten sollte.

@brynbellomy schrieb:

Auch wenn uns die try/else-Syntax gegenüber dem bestehenden if err != nil-Stil keine signifikante Verringerung der Codelänge bringt, gibt sie uns Konsistenz mit dem try (no else)-Fall.

Das einzigartige Ziel der eingebauten try -Funktion ist es, Boilerplate zu reduzieren, daher ist es schwer einzusehen, warum wir die von Ihnen vorgeschlagene try/else-Syntax übernehmen sollten, wenn Sie anerkennen, dass sie uns "keine signifikante Reduzierung bringt in Codelänge".

Sie erwähnen auch, dass die von Ihnen vorgeschlagene Syntax den try-Fall mit dem try/else-Fall konsistent macht. Aber es schafft auch eine inkonsistente Art der Verzweigung, wenn wir bereits if/else haben. Bei einem bestimmten Anwendungsfall gewinnen Sie ein wenig an Konsistenz, verlieren aber beim Rest viel Inkonsistenz.

Ich habe das Bedürfnis, meine Meinungen für das auszudrücken, was sie wert sind. Obwohl nicht alles akademischer und technischer Natur ist, denke ich, dass es gesagt werden muss.

Ich glaube, diese Änderung ist einer dieser Fälle, in denen Engineering um des Engineering willen durchgeführt wird und "Fortschritt" zur Rechtfertigung verwendet wird. Die Fehlerbehandlung in Go ist nicht kaputt und dieser Vorschlag verstößt gegen einen Großteil der Designphilosophie, die ich an Go liebe.

Machen Sie die Dinge einfach zu verstehen, nicht einfach zu tun
Dieser Vorschlag wählt die Optimierung für Faulheit statt Korrektheit. Der Fokus liegt auf einer einfacheren Fehlerbehandlung und im Gegenzug geht viel Lesbarkeit verloren. Die gelegentlich mühsame Art der Fehlerbehandlung ist aufgrund der Lesbarkeits- und Debuggbarkeitsgewinne akzeptabel.

Vermeiden Sie es, Rückgabeargumente zu benennen
Es gibt einige Grenzfälle mit defer -Anweisungen, bei denen die Benennung des Rückgabearguments gültig ist. Außerhalb dieser sollte es vermieden werden. Dieser Vorschlag fördert die Verwendung von Namensrückgabeargumenten. Dies trägt nicht dazu bei, den Go-Code besser lesbar zu machen.

Kapselung soll eine neue Semantik schaffen, bei der man absolut präzise ist
Diese neue Syntax enthält keine Genauigkeit. Das Ausblenden der Fehlervariablen und der Rückgabe trägt nicht zum besseren Verständnis bei. Tatsächlich fühlt sich die Syntax von allem, was wir heute in Go tun, sehr fremd an. Wenn jemand eine ähnliche Funktion schreiben würde, würde die Community meiner Meinung nach zustimmen, dass die Abstraktion die Kosten verbirgt und die Einfachheit, die sie zu bieten versucht, nicht wert ist.

Wem versuchen wir zu helfen?
Ich bin besorgt, dass diese Änderung eingeführt wird, um Unternehmensentwickler von ihren aktuellen Sprachen weg und zu Go zu locken. Das Implementieren von Sprachänderungen, nur um die Anzahl zu erhöhen, schafft einen schlechten Präzedenzfall. Ich denke, es ist fair, diese Frage zu stellen und eine Antwort auf das Geschäftsproblem zu erhalten, das gelöst werden soll, und auf den erwarteten Gewinn, der erzielt werden soll?

Das habe ich jetzt schon mehrfach gesehen. Es scheint ziemlich klar zu sein, dass dieser Vorschlag angesichts all der jüngsten Aktivitäten des Sprachteams im Grunde genommen in Stein gemeißelt ist. Es wird mehr die Umsetzung verteidigt als eine tatsächliche Debatte über die Umsetzung selbst. All das begann vor 13 Tagen. Wir werden sehen, welche Auswirkungen diese Änderung auf die Sprache, die Community und die Zukunft von Go hat.

Die Fehlerbehandlung in Go ist nicht kaputt und dieser Vorschlag verstößt gegen einen Großteil der Designphilosophie, die ich an Go liebe.

Bill drückt meine Gedanken perfekt aus.

Ich kann nicht verhindern, dass try eingeführt wird, aber wenn es so ist, werde ich es nicht selbst verwenden; Ich werde es nicht lehren und ich werde es nicht in PRs akzeptieren, die ich überprüfe. Es wird einfach zur Liste der anderen „Dinge in Go, die ich nie verwende“ hinzugefügt (mehr dazu in Mat Ryers amüsantem Vortrag auf YouTube).

@ardan-bkennedy, danke für deine Kommentare.

Sie haben nach dem "zu lösenden Geschäftsproblem" gefragt. Ich glaube nicht, dass wir auf die Probleme eines bestimmten Unternehmens abzielen, außer vielleicht auf „Programmieren“. Aber allgemeiner formulierten wir das Problem, das wir zu lösen versuchten, letzten August in der Gophercon-Entwurfsdiskussionsauftaktveranstaltung (siehe Problemübersicht , insbesondere den Abschnitt „Ziele“). Die Tatsache, dass dieses Gespräch seit letztem August andauert, widerspricht auch rundheraus Ihrer Behauptung, dass „das alles vor 13 Tagen angefangen hat“.

Sie sind nicht die einzige Person, die angedeutet hat, dass dies kein Problem oder kein Problem ist, das es wert ist, gelöst zu werden. Weitere derartige Kommentare finden Sie unter https://swtch.com/try.html#nonissue . Wir haben diese notiert und möchten sicherstellen, dass wir ein tatsächliches Problem lösen. Ein Teil des Weges, um dies herauszufinden, besteht darin, den Vorschlag auf echten Codebasen zu bewerten. Tools wie Robert's tryhard helfen uns dabei. Ich habe vorhin darum gebeten, dass die Leute uns wissen lassen, was sie in ihren eigenen Codebasen finden. Diese Informationen sind von entscheidender Bedeutung, um zu beurteilen, ob sich die Änderung lohnt oder nicht. Sie haben eine Vermutung und ich habe eine andere, und das ist in Ordnung. Die Antwort ist, diese Vermutungen durch Daten zu ersetzen.

Wir werden alles Notwendige tun, um sicherzustellen, dass wir ein tatsächliches Problem lösen.

Auch hier sind experimentelle Daten der Weg nach vorne, nicht Bauchreaktionen. Leider erfordert das Sammeln von Daten mehr Aufwand. An dieser Stelle möchte ich Menschen, die helfen wollen, ermutigen, hinauszugehen und Daten zu sammeln.

@ardan-bkennedy, Entschuldigung für das zweite Follow-up, aber in Bezug auf:

Ich bin besorgt, dass diese Änderung eingeführt wird, um Unternehmensentwickler von ihren aktuellen Sprachen weg und zu Go zu locken. Das Implementieren von Sprachänderungen, nur um die Anzahl zu erhöhen, schafft einen schlechten Präzedenzfall.

Es gibt zwei ernsthafte Probleme mit dieser Linie, an denen ich nicht vorbeigehen kann.

Erstens weise ich die implizite Behauptung zurück, dass es Klassen von Entwicklern – in diesem Fall „Unternehmensentwickler“ – gibt, die es irgendwie nicht wert sind, Go zu verwenden oder ihre Probleme zu berücksichtigen. Im speziellen Fall von „Unternehmen“ sehen wir viele Beispiele von kleinen und großen Unternehmen, die Go sehr effektiv einsetzen.

Zweitens haben wir – Robert, Rob, Ken, Ian und ich – seit Beginn des Go-Projekts Sprachänderungen und -funktionen basierend auf unserer kollektiven Erfahrung beim Aufbau vieler Systeme evaluiert. Wir fragen: "Würde das in den Programmen, die wir schreiben, gut funktionieren?" Das war ein erfolgreiches Rezept mit breiter Anwendbarkeit und ist dasjenige, das wir weiterhin verwenden werden, wieder ergänzt durch die Daten, um die ich in den vorherigen Kommentaren und Erfahrungsberichten im Allgemeinen gebeten habe. Wir würden keine Sprachänderung vorschlagen oder unterstützen, die wir nicht in unseren eigenen Programmen verwenden können oder die unserer Meinung nach nicht gut zu Go passt. Und wir würden sicherlich keine schlechte Änderung vorschlagen oder unterstützen, nur um mehr Go-Programmierer zu haben. Wir verwenden Go schließlich auch.

@rsc
Es wird keinen Mangel an Orten geben, an denen dieser Komfort platziert werden kann. Welche Metrik wird gesucht, die darüber hinaus die Substanz des Mechanismus beweist? Gibt es eine Liste klassifizierter Fehlerbehandlungsfälle? Wie wird der Wert aus den Daten abgeleitet, wenn ein Großteil des öffentlichen Prozesses von Stimmungen bestimmt wird?

Die Tools tryhard sind sehr informativ!
Ich konnte sehen, dass ich oft return ...,err verwende, aber nur, wenn ich weiß, dass ich eine Funktion aufrufe, die den Fehler bereits umschließt (mit pkg/errors ), meistens in HTTP-Handlern. Ich gewinne an Lesbarkeit mit weniger Codezeilen.
Dann würde ich in diesem HTTP-Handler ein defer fmt.HandleErrorf(&err, "handler xyz") hinzufügen und schließlich mehr Kontext als zuvor hinzufügen.

Ich sehe auch viele Fälle, in denen ich mich überhaupt nicht um den Fehler kümmere fmt.Printf und ich werde es mit try tun.
Wird es zum Beispiel möglich sein, defer try(f.Close()) zu machen?

Vielleicht hilft try also endlich, Kontext hinzuzufügen und Best Practices voranzutreiben, anstatt das Gegenteil.

Ich bin sehr ungeduldig, in echt zu testen!

@flibustenet Der Vorschlag in seiner jetzigen Form lässt defer try(f()) nicht zu (siehe Begründung ). Es gibt alle möglichen Probleme damit.

Wenn wir dieses tryhard -Tool verwenden, um Änderungen in einer Codebasis zu sehen, könnten wir auch das Verhältnis von if err != nil vorher und nachher vergleichen, um zu sehen, ob es üblicher ist, Kontext hinzuzufügen oder den Fehler einfach zurückzugeben?

Ich denke, dass ein hypothetisches riesiges Projekt vielleicht 1000 Stellen sehen kann, an denen try() hinzugefügt wurden, aber es gibt 10000 if err != nil , die Kontext hinzufügen, also obwohl 1000 riesig aussehen, sind es nur 10% des Ganzen .

@Goodwine Ja. Ich werde diese Woche wahrscheinlich nicht dazu kommen, diese Änderung vorzunehmen, aber der Code ist ziemlich geradlinig und in sich abgeschlossen. Probieren Sie es aus (kein Wortspiel beabsichtigt), klonen Sie es und passen Sie es nach Bedarf an.

Wäre defer try(f()) nicht gleichbedeutend mit

defer func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Dies (die if-Version) ist derzeit nicht verboten, oder? Mir scheint, Sie sollten hier keine Ausnahme machen - kann eine Warnung generiert werden? Und es ist nicht klar, ob der obige Verzögerungscode unbedingt falsch ist. Was passiert, wenn close(file) in einer defer -Anweisung fehlschlägt? Sollen wir diesen Fehler melden oder nicht?

Ich habe die Begründung gelesen, die anscheinend über defer try(f) und nicht defer try(f()) spricht. Kann ein Tippfehler sein?

Ein ähnliches Argument kann für go try(f()) werden, was übersetzt bedeutet

go func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Hier macht try nichts Sinnvolles, ist aber harmlos.

@ardan-bkennedy Danke für deine Gedanken. Bei allem Respekt, ich glaube, Sie haben die Absicht dieses Vorschlags falsch dargestellt und mehrere unbegründete Behauptungen aufgestellt .

In Bezug auf einige der Punkte, die @rsc zuvor nicht angesprochen hat:

  • Wir haben nie gesagt, dass die Fehlerbehandlung kaputt ist. Das Design basiert auf der Beobachtung (von der Go-Community!), dass die derzeitige Handhabung in Ordnung, aber in vielen Fällen wortreich ist - dies ist unbestritten. Dies ist eine wesentliche Prämisse des Vorschlags.

  • Dinge einfacher zu machen, kann sie auch leichter verständlich machen - diese beiden schließen sich nicht gegenseitig aus oder implizieren sich gegenseitig. Ich fordere Sie auf, sich diesen Code als Beispiel anzusehen. Durch die Verwendung try wird eine beträchtliche Menge an Boilerplate entfernt, und diese Boilerplate trägt praktisch nichts zur Verständlichkeit des Codes bei. Das Ausklammern von sich wiederholendem Code ist eine standardmäßige und weithin akzeptierte Codierungspraxis zur Verbesserung der Codequalität.

  • Bezüglich "dieser Vorschlag verstößt gegen einen Großteil der Designphilosophie": Wichtig ist, dass wir in Bezug auf "Designphilosophie" nicht dogmatisch werden - das ist oft der Untergang guter Ideen (außerdem denke ich, dass wir ein oder zwei Dinge darüber wissen Gos Designphilosophie). Es gibt eine Menge "religiöser Eifer" (in Ermangelung eines besseren Begriffs) um benannte vs. unbenannte Ergebnisparameter. Mantras wie "Sie dürfen niemals benannte Ergebnisparameter verwenden" sind ohne Kontext bedeutungslos. Sie können als allgemeine Richtlinien dienen, sind aber keine absoluten Wahrheiten. Benannte Ergebnisparameter sind nicht per se "schlecht". Gut benannte Ergebnisparameter können die Dokumentation einer API sinnvoll ergänzen. Kurz gesagt, verwenden wir keine Slogans, um Entscheidungen zum Sprachdesign zu treffen.

  • Es ist ein Punkt dieses Vorschlags, keine neue Syntax einzuführen. Es schlägt nur eine neue Funktion vor. Wir können diese Funktion nicht in die Sprache schreiben, daher ist eine eingebaute Funktion der natürliche Platz dafür in Go. Es ist nicht nur eine einfache Funktion, sondern auch sehr genau definiert. Wir wählen diesen minimalen Ansatz gegenüber umfassenderen Lösungen genau deshalb, weil er eine Sache sehr gut macht und fast nichts willkürlichen Designentscheidungen überlässt. Wir sind auch nicht wild abseits der ausgetretenen Pfade, da andere Sprachen (z. B. Rust) sehr ähnliche Konstrukte haben. Zu behaupten, dass "die Community zustimmen würde, dass die Abstraktion die Kosten verbirgt und die Einfachheit nicht wert ist, die sie zu bieten versucht", ist, anderen Leuten Worte in den Mund zu legen. Während wir die lautstarken Gegner dieses Vorschlags deutlich hören können, gibt es einen erheblichen Prozentsatz (schätzungsweise 40 %) der Menschen, die ihre Zustimmung zur Fortsetzung des Experiments zum Ausdruck gebracht haben. Entrechten wir sie nicht mit Übertreibungen.

Danke.

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Ich bin mir ziemlich sicher, dass dies return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; FWIW sein sollte. 😁

@bakul weil Argumente sofort ausgewertet werden, ist es eigentlich ungefähr gleichbedeutend mit:

<result list> := f()
defer try(<result list>)

Dies kann für manche ein unerwartetes Verhalten sein, da f() nicht auf später verschoben wird, sondern sofort ausgeführt wird. Dasselbe gilt für go try(f()) .

@bakul Das Dokument erwähnt defer try(f) (anstelle von defer try(f()) , da try im Allgemeinen für jeden Ausdruck gilt, nicht nur für einen Funktionsaufruf (Sie können try(err) für sagen Beispiel, wenn err vom Typ error ist).Also kein Tippfehler, aber vielleicht zunächst verwirrend. f steht einfach für einen Ausdruck, der normalerweise eine Funktion ist Anruf.

@deanveloper , @griesemer Egal :-) Danke.

@carl-mastrangelo

_"Ich bin mir ziemlich sicher, dass das return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; sein sollte _

Sie gehen von PHP 7.x aus. Ich war nicht. Aber andererseits weißt du angesichts deines spöttischen Gesichts, dass das nicht der Punkt war. :zwinkern:

Ich bereite eine kurze Demonstration vor, um diese Diskussion während eines Go-Treffens zu zeigen, das morgen stattfindet, und höre einige neue Gedanken, da ich glaube, dass die meisten Teilnehmer an diesem Thread (Beitragende oder Beobachter) diejenigen sind, die tiefer in die Sprache involviert sind, und höchstwahrscheinlich "nicht der durchschnittliche Go-Entwickler" (nur eine Ahnung).

Dabei erinnerte ich mich, dass wir tatsächlich ein Treffen über Fehler und eine Diskussion über zwei Muster hatten:

  1. Erweitern Sie die Fehlerstruktur und unterstützen Sie gleichzeitig die Fehlerschnittstelle mystruct.Error()
  2. Betten Sie den Fehler entweder als Feld oder als anonymes Feld der Struktur ein
type ExtErr struct{
  error
  someOtherField string
}  

Diese werden in einigen Stapeln verwendet, die meine Teams tatsächlich gebaut haben.

Der Vorschlag Q&A heißt es
F: Das letzte an try übergebene Argument muss vom Typ error sein. Warum reicht es nicht aus, dass das eingehende Argument einem Fehler zuordenbar ist?
A: "... Wir können diese Entscheidung bei Bedarf in Zukunft überdenken."

Kann jemand ähnliche Anwendungsfälle kommentieren, damit wir verstehen können, ob diese Notwendigkeit für beide oben genannten Fehlererweiterungsoptionen üblich ist?

@mikeschinkel Ich bin nicht der Carl, den du suchst.

@daved , zu:

Es wird keinen Mangel an Orten geben, an denen dieser Komfort platziert werden kann. Welche Metrik wird gesucht, die darüber hinaus die Substanz des Mechanismus beweist? Gibt es eine Liste klassifizierter Fehlerbehandlungsfälle? Wie wird der Wert aus den Daten abgeleitet, wenn ein Großteil des öffentlichen Prozesses von Stimmungen bestimmt wird?

Die Entscheidung basiert darauf, wie gut dies in realen Programmen funktioniert. Wenn uns Leute zeigen, dass try im Großteil ihres Codes wirkungslos ist, sind das wichtige Daten. Der Prozess wird von dieser Art von Daten angetrieben. Es ist _nicht_ gefühlsgetrieben.

Fehlerkontext

Die wichtigste semantische Frage, die in dieser Ausgabe angesprochen wurde, ist, ob try eine bessere oder schlechtere Annotation von Fehlern mit Kontext fördert.

Die Problemübersicht vom letzten August enthält eine Reihe von beispielhaften CopyFile-Implementierungen in den Abschnitten „Problem“ und „Ziele“. Sowohl damals als auch heute ist es ein ausdrückliches Ziel, dass jede Lösung es _wahrscheinlicher_ macht, dass Benutzer Fehlern den richtigen Kontext hinzufügen. Und wir denken, dass try das tun kann, sonst hätten wir es nicht vorgeschlagen.

Aber bevor wir es versuchen, sollten wir uns vergewissern, dass wir alle auf derselben Seite sind, wenn es um den entsprechenden Fehlerkontext geht. Das kanonische Beispiel ist os.Open. Zitat aus dem Go-Blogbeitrag „ Error handling and Go “:

Es liegt in der Verantwortung der Fehlerimplementierung, den Kontext zusammenzufassen.
Der von os.Open zurückgegebene Fehler wird als „open /etc/passwd: permission denied“ formatiert, nicht nur als „permission denied“.

Siehe auch den Abschnitt über Fehler in Effective Go .

Beachten Sie, dass sich diese Konvention von anderen Sprachen unterscheiden kann, mit denen Sie vertraut sind, und dass sie auch im Go-Code nur uneinheitlich befolgt wird. Ein ausdrückliches Ziel des Versuchs, die Fehlerbehandlung zu rationalisieren, besteht darin, es den Benutzern zu erleichtern, dieser Konvention zu folgen und den entsprechenden Kontext hinzuzufügen, wodurch sie konsistenter befolgt wird.

Es gibt heute viel Code, der der Go-Konvention folgt, aber es gibt auch viel Code, der die entgegengesetzte Konvention annimmt. Es ist allzu üblich, Code wie diesen zu sehen:

f, err := os.Open(file)
if err != nil {
    log.Fatalf("opening %s: %v", file, err)
}

was natürlich zweimal dasselbe ausgibt (viele Beispiele in dieser Diskussion sehen so aus). Ein Teil dieser Bemühungen muss darin bestehen, sicherzustellen, dass alle über die Konvention Bescheid wissen und diese befolgen.

In Code, der der Konvention für den Go-Fehlerkontext folgt, erwarten wir, dass die meisten Funktionen jeder Fehlerrückgabe den gleichen Kontext hinzufügen, sodass im Allgemeinen eine Dekoration gilt. Beispielsweise müssen im CopyFile-Beispiel in jedem Fall Details darüber hinzugefügt werden, was kopiert wurde. Andere spezifische Rückgaben können mehr Kontext hinzufügen, aber normalerweise eher zusätzlich als ersetzend. Wenn wir mit dieser Erwartung falsch liegen, wäre das gut zu wissen. Eindeutige Beweise aus echten Codebasen würden helfen.

Der Gophercon-Check/Griff-Entwurfsentwurf hätte Code verwendet wie:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Dieser Vorschlag hat das überarbeitet, aber die Idee ist die gleiche:

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

und wir möchten einen noch unbenannten Helfer für dieses allgemeine Muster hinzufügen:

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

Kurz gesagt, die Angemessenheit und der Erfolg dieses Ansatzes hängen von diesen Annahmen und logischen Schritten ab:

  1. Die Leute sollten der angegebenen Go-Konvention folgen: „Der Angerufene fügt relevanten Kontext hinzu, den er kennt.“
  2. Daher müssen die meisten Funktionen nur einen Kontext auf Funktionsebene hinzufügen, der das Gesamte beschreibt
    Operation, nicht das spezifische Teilstück, das fehlgeschlagen ist (das Teilstück hat sich bereits selbst gemeldet).
  3. Viel Go-Code fügt heute den Kontext auf Funktionsebene nicht hinzu, weil er zu repetitiv ist.
  4. Das Bereitstellen einer Möglichkeit, den Kontext auf Funktionsebene einmal zu schreiben, wird dies wahrscheinlicher machen
    Entwickler machen das.
  5. Das Endergebnis wird mehr Go-Code sein, der der Konvention folgt und den entsprechenden Kontext hinzufügt.

Wenn es eine Annahme oder einen logischen Schritt gibt, den Sie für falsch halten, möchten wir es wissen. Und der beste Weg, uns das zu sagen, ist, auf Beweise in tatsächlichen Codebasen hinzuweisen. Zeigen Sie uns häufige Muster, bei denen Versuch unangemessen ist oder die Dinge verschlimmert. Zeigen Sie uns Beispiele für Dinge, bei denen der Versuch effektiver war als erwartet. Versuchen Sie zu quantifizieren, wie viel Ihrer Codebasis auf die eine oder andere Seite fällt. Und so weiter. Daten sind wichtig.

Danke.

Vielen Dank an @rsc für die zusätzlichen Informationen zur Best Practice im Fehlerkontext. Dieser Punkt zu Best Practice hat mich besonders angesprochen, verbessert aber die Beziehung von try zum Fehlerkontext erheblich.

Daher müssen die meisten Funktionen nur einen Kontext auf Funktionsebene hinzufügen, der das Gesamte beschreibt
Operation, nicht das spezifische Teilstück, das fehlgeschlagen ist (das Teilstück hat sich bereits selbst gemeldet).

Der Ort, an dem try also nicht hilft, ist, wenn wir auf Fehler reagieren und sie nicht nur kontextualisieren müssen.

Um ein Beispiel von Cleaner, eleganter und falsch anzupassen, hier ihr Beispiel einer Funktion, die in ihrer Fehlerbehandlung subtil falsch ist. Ich habe es mit try und defer -Style Error Wrapping an Go angepasst:

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    try(AddToLeague(guy))
    return guy, nil
}

Diese Funktion ist falsch, denn wenn guy.Team.Add(guy) erfolgreich ist, aber AddToLeague(guy) fehlschlägt, hat das Team ein ungültiges Guy-Objekt, das keiner Liga angehört. Der korrekte Code würde so aussehen, wobei wir guy.Team.Add(guy) und try nicht mehr verwenden können:

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    if err := AddToLeague(guy); err != nil {
        guy.Team.Remove(guy)
        return Guy{}, err
    }
    return guy, nil
}

Oder wenn wir vermeiden möchten, Nullwerte für die Nicht-Fehler-Rückgabewerte angeben zu müssen, können wir return Guy{}, err durch try(err) ersetzen. Unabhängig davon wird die Funktion defer -ed weiterhin ausgeführt und Kontext hinzugefügt, was nett ist.

Auch dies bedeutet, dass try auf die Reaktion auf Fehler setzt, aber nicht darauf, ihnen Kontext hinzuzufügen. Das ist eine Unterscheidung, auf die ich und vielleicht auch andere angespielt haben. Dies ist sinnvoll, da die Art und Weise, wie eine Funktion einem Fehler Kontext hinzufügt, für einen Leser nicht besonders interessant ist, aber die Art und Weise, wie eine Funktion auf Fehler reagiert, wichtig ist. Wir sollten die weniger interessanten Teile unseres Codes weniger ausführlich machen, und genau das tut try .

Sie sind nicht die einzige Person, die angedeutet hat, dass dies kein Problem oder kein Problem ist, das es wert ist, gelöst zu werden. Weitere derartige Kommentare finden Sie unter https://swtch.com/try.html#nonissue . Wir haben diese notiert und möchten sicherstellen, dass wir ein tatsächliches Problem lösen.

@rsc Ich denke auch, dass es kein Problem mit dem aktuellen Fehlercode gibt. Bitte zählen Sie mich dazu.

Tools wie Robert's tryhard helfen uns dabei. Ich habe vorhin darum gebeten, dass die Leute uns wissen lassen, was sie in ihren eigenen Codebasen finden. Diese Informationen sind von entscheidender Bedeutung, um zu beurteilen, ob sich die Änderung lohnt oder nicht. Sie haben eine Vermutung und ich habe eine andere, und das ist in Ordnung. Die Antwort ist, diese Vermutungen durch Daten zu ersetzen.

Ich habe mir https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go angesehen und mir gefällt alter Code besser. Es überrascht mich, dass der try-Funktionsaufruf die aktuelle Ausführung unterbrechen könnte. So funktioniert das aktuelle Go nicht.

Ich vermute, Sie werden feststellen, dass die Meinungen unterschiedlich sind. Ich denke, das ist sehr subjektiv.

Und ich vermute, die Mehrheit der Benutzer beteiligt sich nicht an dieser Debatte. Sie wissen nicht einmal, dass diese Veränderung bevorsteht. Ich bin selbst ziemlich in Go involviert, aber ich nehme an dieser Änderung nicht teil, weil ich keine Freizeit habe.

Ich denke, wir müssten allen bestehenden Go-Benutzern beibringen, jetzt anders zu denken.

Wir müssten auch entscheiden, was mit einigen Benutzern/Unternehmen geschehen soll, die sich weigern, try in ihrem Code zu verwenden. Da werden sich sicher welche finden.

Vielleicht müssten wir gofmt ändern, um den aktuellen Code automatisch neu zu schreiben. Um solche "Rogue"-Benutzer zu zwingen, die neue Try-Funktion zu verwenden. Ist es möglich, gofmt dazu zu bringen?

Wie würden wir mit Kompilierungsfehlern umgehen, wenn Leute go1.13 und früher verwenden, um Code mit try zu erstellen?

Ich habe wahrscheinlich viele andere Probleme übersehen, die wir überwinden müssten, um diese Änderung umzusetzen. Ist es die Mühe wert? Ich glaube nicht.

Alex

@griesemer
Beim mühsamen Probieren einer Datei mit 97 Fehlern hat's keiner gefangen, ich habe festgestellt, dass die 2 Muster nicht übersetzt sind
1 :

    if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
        tx.Rollback()
        return nil, err
    }

Wird nicht ersetzt, wahrscheinlich weil das tx.Rollback() zwischen err := und der Return-Zeile steht,
Was ich annehme, kann nur durch Verzögerung behandelt werden - und wenn alle Fehlerpfade tx.Rollback () benötigen.
Ist das richtig ?

  1. Es schlägt auch nicht vor:
if err := db.Error; err != nil {
        return nil, err
    } else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
        return nil, err
    } else {
        return itemDb, nil
    }

oder

    if err := db.Error; err != nil {
        return nil, err
    } else {
            if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
                return nil, err
            } else {
                return itemDb, nil
            }
        return result, nil
    }

Liegt das an der Schattierung oder würde der Verschachtelungsversuch zu ? Bedeutung - sollte diese Verwendung try oder vorgeschlagen werden als err := ... return err ?

@guybrand Betreff : die beiden Muster, die Sie gefunden haben:

1) Ja, tryhard bemüht sich nicht sehr. Für komplexere Fälle ist eine Typprüfung erforderlich. Wenn tx.Rollback() in allen Pfaden ausgeführt werden soll, könnte defer der richtige Ansatz sein. Andernfalls könnte es der richtige Ansatz sein, die if beizubehalten. Es kommt auf den konkreten Code an.

2) Auch hier: tryhard sucht nicht nach diesem komplexeren Muster. Vielleicht könnte es.

Auch dies ist ein experimentelles Tool, um schnell Antworten zu erhalten. Es richtig zu machen erfordert etwas mehr Arbeit.

@alexhirnmann

Wie würden wir mit Kompilierungsfehlern umgehen, wenn Leute go1.13 und früher verwenden, um Code mit try zu erstellen?

Soweit ich weiß, wird die Version der Sprache selbst durch die go -Sprachversionsanweisung in der go.mod -Datei für jeden zu kompilierenden Codeabschnitt gesteuert.

Die go.mod -Dokumentation während des Flugs beschreibt die go -Sprachversionsanweisung wie folgt:

Die erwartete Sprachversion, die durch die Direktive go festgelegt wird, bestimmt
welche Sprachfeatures beim Kompilieren des Moduls verfügbar sind.
Die in dieser Version verfügbaren Sprachfunktionen stehen zur Verwendung zur Verfügung.
Sprachfeatures, die in früheren Versionen entfernt oder in späteren Versionen hinzugefügt wurden,
wird nicht verfügbar sein. Beachten Sie, dass die Sprachversion keinen Einfluss hat
build-Tags, die von der verwendeten Go-Version bestimmt werden.

Wenn hypothetisch so etwas wie ein neues eingebautes try in etwas wie Go 1.15 landet, dann hätte an diesem Punkt jemand, dessen go.mod -Datei go 1.12 liest, keinen Zugriff auf dieses neue try eingebaut, auch wenn sie mit der Go 1.15-Toolchain kompiliert werden. Mein Verständnis des aktuellen Plans ist, dass sie die in ihrem go.mod deklarierte Go-Sprachversion von go 1.12 ändern müssten, um stattdessen go 1.15 $ zu lesen, wenn sie das neue Go verwenden möchten 1.15 Sprachfunktion von try .

Auf der anderen Seite, wenn Sie Code haben, der try verwendet und dieser Code in einem Modul lebt, dessen go.mod Datei seine Go-Sprachversion als go 1.15 deklariert, aber dann versucht es jemand Erstellen Sie das mit der Go 1.12-Toolchain, an diesem Punkt schlägt die Go 1.12-Toolchain mit einem Kompilierungsfehler fehl. Die Go 1.12-Toolchain weiß nichts über try , aber sie weiß genug, um eine zusätzliche Meldung auszugeben, dass der Code, der nicht kompiliert werden konnte, angeblich Go 1.15 erfordert, basierend auf dem, was in der Datei go.mod steht . Sie können dieses Experiment jetzt tatsächlich mit der heutigen Go 1.12-Toolchain versuchen und die resultierende Fehlermeldung sehen:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

Es gibt eine viel längere Diskussion im Go2-Übergangsvorschlagsdokument .

Das heißt, die genauen Details davon könnten an anderer Stelle besser diskutiert werden (z. B. vielleicht in #30791 oder in diesem kürzlich erschienenen Golang-Nuts-Thread ).

@griesemer , tut mir leid, wenn ich eine spezifischere Anfrage für ein Format verpasst habe, aber ich würde gerne einige Ergebnisse teilen und Zugriff (eine mögliche Erlaubnis) auf den Quellcode einiger Unternehmen haben.
Unten ist ein echtes Beispiel für ein kleines Projekt, ich denke, die beigefügten Ergebnisse geben ein gutes Beispiel, wenn ja, können wir wahrscheinlich eine Tabelle mit ähnlichen Ergebnissen teilen:

Gesamt = Anzahl der Codezeilen
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = Anzahl der Zeilen mit err := (dies vermisst wahrscheinlich err = und myerr := , aber ich denke, in den meisten Fällen deckt es ab)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = Anzahl der Zeilen, die tryhard gefunden hat

Der erste Fall, den ich zum Studium getestet habe, kam zurück:
Gesamt = 5106
Fehler = 111
tryhard = 16

größere Codebasis
Gesamt = 131777
Fehler = 3289
tryhard = 265

Wenn dieses Format akzeptabel ist, teilen Sie uns mit, wie Sie die Ergebnisse erhalten möchten. Ich nehme an, es einfach hierher zu werfen, wäre nicht das richtige Format
Außerdem wäre es wahrscheinlich ein Quickie, die Zeilen versuchsweise zählen zu lassen, gelegentlich von err := (und wahrscheinlich err = , nur 4 auf der Codebasis, die ich zu lernen versucht habe)

Danke.

Von @griesemer in https://github.com/golang/go/issues/32437#issuecomment -503276339

Ich fordere Sie auf, sich diesen Code als Beispiel anzusehen.

In Bezug auf diesen Code ist mir aufgefallen, dass die hier erstellte Ausgabedatei niemals geschlossen zu sein scheint. Darüber hinaus ist es wichtig, Fehler beim Schließen von Dateien zu überprüfen, in die Sie geschrieben haben, da dies möglicherweise das einzige Mal ist, dass Sie darüber informiert werden, dass ein Problem mit einem Schreibvorgang aufgetreten ist.

Ich erwähne dies nicht als Fehlerbericht (obwohl es vielleicht so sein sollte?), sondern um zu sehen, ob try einen Einfluss darauf hat, wie man es beheben könnte. Ich werde alle Möglichkeiten aufzählen, die mir einfallen, um das Problem zu beheben, und überlegen, ob das Hinzufügen von try helfen oder schaden würde. Hier sind einige Möglichkeiten:

  1. Fügen Sie explizite Aufrufe von outf.Close() direkt vor der Rückgabe eines Fehlers hinzu.
  2. Benennen Sie den Rückgabewert und fügen Sie eine Verzögerung hinzu, um die Datei zu schließen, wobei der Fehler aufgezeichnet wird, falls noch keiner vorhanden ist. z.B
func foo() (err error) {
    outf := try(os.Create())
    defer func() {
        cerr := outf.Close()
        if err == nil {
            err = cerr
        }
    }()

    ...
}
  1. Das "Double Close"-Muster, bei dem man defer outf.Close() macht, um die Ressourcenbereinigung sicherzustellen, und try(outf.Close()) vor der Rückkehr, um sicherzustellen, dass keine Fehler auftreten.
  2. Umgestalten, damit eine Hilfsfunktion die geöffnete Datei anstelle eines Pfads verwendet, damit der Aufrufer sicherstellen kann, dass die Datei ordnungsgemäß geschlossen wird. z.B
func foo() error {
    outf := try(os.Create())
    if err := helper(outf); err != nil {
        outf.Close()
        return err
    }
    try(outf.Close())
    return nil
}

Ich denke, in allen Fällen außer Fall Nummer 1 ist try im schlimmsten Fall neutral und normalerweise positiv. Und ich würde Nummer 1 angesichts der Größe und Anzahl der Fehlermöglichkeiten in dieser Funktion als die am wenigsten schmackhafte Option betrachten, sodass das Hinzufügen try die Attraktivität einer negativen Wahl verringern würde.

Ich hoffe, diese Analyse war hilfreich.

Wenn hypothetisch so etwas wie ein neues eingebautes try in etwas wie Go 1.15 landet, dann hätte an diesem Punkt jemand, dessen go.mod -Datei go 1.12 liest, keinen Zugriff

@thepudds danke für die Erklärung. Aber ich verwende keine Module. Deine Erklärung geht also weit über meinen Kopf hinaus.

Alex

@alexhirnmann

Wie würden wir mit Kompilierungsfehlern umgehen, wenn Leute go1.13 und früher verwenden, um Code mit try zu erstellen?

Wenn try hypothetisch in so etwas wie Go 1.15 landen sollte, lautet die sehr kurze Antwort auf Ihre Frage, dass jemand, der Go 1.13 verwendet, um Code mit try zu erstellen, einen Kompilierungsfehler wie diesen sehen würde:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Zumindest soweit ich verstehe, was über den Übergangsvorschlag gesagt wurde).

@alexbrainman Danke für dein Feedback.

Eine große Anzahl von Kommentaren zu diesem Thread haben die Form "das sieht nicht aus wie Go" oder "Go funktioniert so nicht" oder "Ich erwarte nicht, dass das hier passiert". Das ist alles richtig, _existierendes_ Go funktioniert so nicht.

Dies ist vielleicht die erste vorgeschlagene Sprachänderung, die das Gefühl der Sprache auf substanziellere Weise beeinflusst. Wir sind uns dessen bewusst, weshalb wir es so minimal gehalten haben. (Ich kann mir nur schwer vorstellen, welchen Aufruhr ein konkreter Generika-Vorschlag auslösen könnte - sprechen wir von einer Sprachänderung).

Aber zurück zu Ihrem Punkt: Programmierer gewöhnen sich daran, wie eine Programmiersprache funktioniert und sich anfühlt. Wenn ich im Laufe von rund 35 Jahren Programmieren eines gelernt habe, dann gewöhnt man sich an fast jede Sprache, und das geht sehr schnell. Nachdem ich original Pascal als erste Hochsprache erlernt hatte, war es _unvorstellbar_, dass eine Programmiersprache nicht alle ihre Schlüsselwörter großschreibt. Aber es dauerte nur etwa eine Woche, um sich an das „Meer von Wörtern“ zu gewöhnen, das C war, wo „man die Struktur des Codes nicht sehen konnte, weil alles aus Kleinbuchstaben besteht“. Nach diesen ersten Tagen mit C sah der Pascal-Code furchtbar laut aus, und der gesamte eigentliche Code schien in einem Durcheinander von schreienden Schlüsselwörtern begraben zu sein. Schneller Vorlauf zu Go: Als wir die Großschreibung zur Kennzeichnung exportierter Kennungen einführten, war dies eine schockierende Änderung gegenüber dem vorherigen, wenn ich mich recht erinnere, schlüsselwortbasierten Ansatz (das war, bevor Go öffentlich wurde). Jetzt denken wir, dass es eine der besseren Designentscheidungen ist (wobei die konkrete Idee tatsächlich von außerhalb des Go-Teams kommt). Oder betrachten Sie das folgende Gedankenexperiment: Stellen Sie sich vor, Go hätte keine defer -Anweisung und jetzt macht jemand starke Argumente für defer . defer hat keine Semantik wie irgendetwas anderes in der Sprache, die neue Sprache fühlt sich vor defer Go nicht mehr so ​​an. Doch nachdem ich ein Jahrzehnt damit gelebt habe, scheint es total "Go-like".

Der Punkt ist, dass die anfängliche Reaktion auf eine Sprachänderung fast bedeutungslos ist, ohne den Mechanismus tatsächlich in echtem Code auszuprobieren und konkretes Feedback zu sammeln. Natürlich ist der vorhandene Fehlerbehandlungscode in Ordnung und sieht klarer aus als der Ersatz mit try - wir sind seit einem Jahrzehnt darauf trainiert, diese if -Anweisungen wegzudenken. Und natürlich sieht der try -Code seltsam aus und hat eine "seltsame" Semantik, wir haben ihn noch nie zuvor verwendet und wir erkennen ihn nicht sofort als Teil der Sprache.

Aus diesem Grund bitten wir die Leute, sich tatsächlich mit der Änderung auseinanderzusetzen, indem Sie damit in Ihrem eigenen Code experimentieren. dh ihn tatsächlich schreiben, oder tryhard existierenden Code überarbeiten lassen und das Ergebnis betrachten. Ich würde empfehlen, es eine Weile ruhen zu lassen, vielleicht eine Woche oder so. Schau es dir nochmal an und berichte.

Abschließend stimme ich Ihrer Einschätzung zu, dass die Mehrheit der Menschen nichts von diesem Vorschlag weiß oder sich nicht damit beschäftigt hat. Es ist ganz klar, dass diese Diskussion von vielleicht einem Dutzend Leuten dominiert wird. Aber es ist noch früh, dieser Vorschlag liegt erst seit zwei Wochen vor, und es wurde noch keine Entscheidung getroffen. Es gibt genügend Zeit für mehr und unterschiedliche Menschen, sich damit zu beschäftigen.

https://github.com/golang/go/issues/32437#issuecomment -503297387 sagt ziemlich genau, wenn Sie Fehler auf mehr als eine Weise in einer einzigen Funktion umschließen, machen Sie es anscheinend falsch. Inzwischen habe ich eine Menge Code, der so aussieht:

        if err := gen.Execute(tmp, s); err != nil {
                return fmt.Errorf("template error: %v", err)
        }

        if err := tmp.Close(); err != nil {
                return fmt.Errorf("cannot write temp file: %v", err)
        }
        closed = true

        if err := os.Rename(tmp.Name(), *genOutput); err != nil {
                return fmt.Errorf("cannot finalize file: %v", err)
        }
        removed = true

( closed und removed werden von defers verwendet, um gegebenenfalls aufzuräumen)

Ich denke wirklich nicht, dass all dies nur im selben Kontext stehen sollte, der die oberste Mission dieser Funktion beschreibt. Ich glaube wirklich nicht, dass der Benutzer nur sehen sollte

processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state

Wenn die Vorlage vermasselt wird, liegt es meiner Meinung nach in der Verantwortung meines Fehlerbehandlers, dass der Execute-Aufruf der Vorlage "Vorlage wird ausgeführt" oder ein solches kleines zusätzliches Bit hinzufügt. (Das ist nicht der beste Kontext, aber ich wollte statt eines erfundenen Beispiels echten Code kopieren und einfügen.)

Ich denke nicht, dass der Benutzer sehen sollte

processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory

ohne irgendeine Ahnung, _warum_ mein Programm versucht, diese Umbenennung durchzuführen, was ist die Semantik, was ist die Absicht. Ich glaube, das Hinzufügen dieses kleinen Stückchens "Datei kann nicht abgeschlossen werden:" hilft wirklich.

Wenn Sie diese Beispiele nicht überzeugen, stellen Sie sich diese Fehlerausgabe einer Befehlszeilen-App vor:

processing path/to/dir: open /some/path/here: No such file or directory

Was bedeutet das? Ich möchte einen Grund hinzufügen, warum die App versucht hat, dort eine Datei zu erstellen (Sie wussten nicht einmal, dass es eine Erstellung war, nicht nur os.Open! Es ist ENOENT, weil kein Zwischenpfad existiert.). Dies sollte nicht zu _jeder_ Fehlerrückgabe dieser Funktion hinzugefügt werden.

Also, was fehlt mir. Halte ich es "falsch"? Soll ich jedes dieser Dinge in eine separate winzige Funktion schieben, die alle eine Verzögerung verwenden, um alle ihre Fehler einzuschließen?

@guybrand Danke für diese Zahlen . Es wäre gut, einige Erkenntnisse darüber zu haben, warum die tryhard -Zahlen so sind, wie sie sind. Vielleicht gibt es viele spezifische Fehlerdekorationen? Wenn ja, ist das großartig und if -Anweisungen sind die richtige Wahl.

Ich werde das Tool verbessern, wenn ich dazu komme.

Danke, @zeebo für deine Analyse . Ich kenne diesen Code nicht genau, aber es sieht so aus, als ob outf Teil eines loadCmdReader (Zeile 173) ist, das dann in Zeile 204 weitergegeben wird. Vielleicht ist das der Grund outf ist nicht geschlossen. (Entschuldigung, ich habe diesen Code nicht geschrieben).

@tv42 Aus den Beispielen in Ihrem https://github.com/golang/go/issues/32437#issuecomment -503340426 geht hervor, dass Sie, vorausgesetzt, Sie machen es nicht „falsch“, eine if -Anweisung verwenden ist die Art und Weise, diese Fälle zu handhaben, wenn sie alle unterschiedliche Antworten erfordern. try wird nicht helfen, und defer wird es nur schwieriger machen (jeder andere Sprachänderungsvorschlag in diesem Thread, der versucht, diesen Code einfacher zu schreiben, ist so nah an if Aussage, dass es sich nicht lohnt, einen neuen Mechanismus einzuführen). Siehe auch die FAQ des Detailvorschlags.

@griesemer Dann fällt mir nur ein, dass du und @rsc anderer Meinung sind. Oder dass ich tatsächlich „es falsch mache“ und gerne darüber sprechen würde.

Sowohl damals als auch heute ist es ein ausdrückliches Ziel, dass jede Lösung die Wahrscheinlichkeit erhöht, dass Benutzer Fehlern den richtigen Kontext hinzufügen. Und wir denken, dass try das tun kann, sonst hätten wir es nicht vorgeschlagen.

@tv42 @rsc post handelt von der allgemeinen Fehlerbehandlungsstruktur von gutem Code, dem ich zustimme. Wenn Sie einen vorhandenen Code haben, der nicht genau zu diesem Muster passt, und Sie mit dem Code zufrieden sind, lassen Sie ihn in Ruhe.

Verschiebt

Die primäre Änderung vom Gophercon-Check/Handle-Entwurf zu diesem Vorschlag bestand darin, handle zugunsten der Wiederverwendung von defer fallen zu lassen. Jetzt würde der Fehlerkontext durch Code wie diesen verzögerten Aufruf hinzugefügt (siehe meinen früheren Kommentar zum Fehlerkontext):

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Die Realisierbarkeit von defer als Fehleranmerkungsmechanismus in diesem Beispiel hängt von einigen Dingen ab.

  1. _Benannte Fehlerergebnisse._ Es gab viele Bedenken hinsichtlich des Hinzufügens benannter Fehlerergebnisse. Es ist wahr, dass wir in der Vergangenheit davon abgeraten haben, dass dies zu Dokumentationszwecken nicht erforderlich war, aber das ist eine Konvention, die wir in Ermangelung eines stärkeren Entscheidungsfaktors gewählt haben. Und auch in der Vergangenheit überwog ein stärkeres Entscheidungskriterium wie der Verweis auf bestimmte Ergebnisse in der Dokumentation die allgemeine Konvention für unbenannte Ergebnisse. Jetzt gibt es ein zweites stärkeres Entscheidungskriterium, nämlich den Wunsch, sich auf den Fehler in einer Zurückstellung zu beziehen. Das scheint nicht verwerflicher zu sein als die Benennung von Ergebnissen zur Verwendung in der Dokumentation. Eine Reihe von Leuten hat ziemlich negativ darauf reagiert, und ich verstehe ehrlich gesagt nicht warum. Es scheint fast so, als würden Leute Retouren ohne Ausdruckslisten (sogenannte „nackte Retouren“) mit benannten Ergebnissen verwechseln. Es stimmt, dass Rückgaben ohne Ausdruckslisten bei größeren Funktionen zu Verwirrung führen können. Es ist oft sinnvoll, diese Verwirrung zu vermeiden, indem man diese Rückgaben in langen Funktionen vermeidet. Das Malen benannter Ergebnisse mit demselben Pinsel funktioniert nicht.

  2. _Adressausdrücke._ Einige Leute haben Bedenken geäußert, dass die Verwendung dieses Musters erfordern wird, dass Go-Entwickler Adressausdrücke verstehen. Das Speichern eines beliebigen Werts mit Zeigermethoden in einer Schnittstelle erfordert dies bereits, daher scheint dies kein wesentlicher Nachteil zu sein.

  3. _Defer selbst._ Einige Leute haben Bedenken geäußert, überhaupt defer als Sprachkonzept zu verwenden, wiederum weil neue Benutzer damit nicht vertraut sein könnten. Wie bei Adressausdrücken ist defer ein zentrales Sprachkonzept, das irgendwann erlernt werden muss. Die Standard-Redewendungen um Dinge wie defer f.Close() und defer l.mu.Unlock() sind so gebräuchlich, dass es schwer zu rechtfertigen ist, defer als obskure Ecke der Sprache zu vermeiden.

  4. _Leistung._ Wir haben jahrelang darüber diskutiert, wie wir daran arbeiten, gängige Verzögerungsmuster wie eine Verzögerung am Anfang einer Funktion so zu gestalten, dass sie keinen Overhead haben, verglichen mit dem manuellen Einfügen dieses Aufrufs bei jeder Rückkehr. Wir glauben, dass wir wissen, wie das geht, und werden es für die nächste Go-Version untersuchen. Selbst wenn dies nicht der Fall ist, sollte der aktuelle Overhead von etwa 50 ns für die meisten Aufrufe, die einen Fehlerkontext hinzufügen müssen, nicht unerschwinglich sein. Und die wenigen leistungsempfindlichen Aufrufe können weiterhin if-Anweisungen verwenden, bis defer schneller ist.

Die ersten drei Bedenken laufen allesamt auf Einwände gegen die Wiederverwendung bestehender Sprachmerkmale hinaus. Aber die Wiederverwendung bestehender Sprachmerkmale ist genau der Vorteil dieses Vorschlags gegenüber check/handle: Es gibt weniger, was zur Kernsprache hinzugefügt werden muss, weniger neue Teile zu lernen und weniger überraschende Interaktionen.

Dennoch wissen wir zu schätzen, dass die Verwendung von defer auf diese Weise neu ist und dass wir den Leuten Zeit geben müssen, um zu bewerten, ob defer in der Praxis gut genug für die von ihnen benötigten Idiome zur Fehlerbehandlung funktioniert.

Seit wir diese Diskussion im letzten August begonnen haben, mache ich die mentale Übung „Wie würde dieser Code mit Check/Handle aussehen?“. und neuerdings „mit try/defer?“ Jedes Mal, wenn ich neuen Code schreibe. Normalerweise bedeutet die Antwort, dass ich anderen, besseren Code schreibe, wobei der Kontext an einer Stelle (dem Zurückstellen) hinzugefügt wird, anstatt bei jeder Rückkehr, oder ganz weggelassen wird.

Angesichts der Idee, einen zurückgestellten Handler zu verwenden, um bei Fehlern Maßnahmen zu ergreifen, gibt es eine Vielzahl von Mustern, die wir mit einem einfachen Bibliothekspaket aktivieren könnten. Ich habe #32676 eingereicht, um mehr darüber nachzudenken, aber mit der Paket-API in dieser Ausgabe würde unser Code so aussehen:

func CopyFile(src, dst string) (err error) {
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Wenn wir CopyFile debuggen und alle zurückgegebenen Fehler und Stack-Trace sehen möchten (ähnlich wie beim Einfügen eines Debug-Drucks), könnten wir Folgendes verwenden:

func CopyFile(src, dst string) (err error) {
    defer errd.Trace(&err)
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

und so weiter.

Die Verwendung von defer auf diese Weise ist ziemlich leistungsfähig und behält den Vorteil von check/handle, dass Sie „do this on any error at all“ einmal oben in die Funktion schreiben können und sich dann für den Rest nicht darum kümmern müssen der Körper. Dies verbessert die Lesbarkeit in ähnlicher Weise wie frühe Schnellausstiege .

Wird das in der Praxis funktionieren? Wir wollen es herausfinden.

Nachdem ich ein paar Monate lang das mentale Experiment durchgeführt habe, wie defer in meinem eigenen Code aussehen würde, denke ich, dass es wahrscheinlich funktionieren wird. Aber natürlich ist es nicht immer dasselbe, es in echtem Code zu verwenden. Wir müssen experimentieren, um das herauszufinden.

Die Leute können heute mit diesem Ansatz experimentieren, indem sie weiterhin if err != nil -Anweisungen schreiben, aber die defer-Helfer kopieren und sie nach Bedarf verwenden. Wenn Sie dazu geneigt sind, lassen Sie uns bitte wissen, was Sie lernen.

@tv42 , da stimme ich @griesemer zu. Wenn Sie feststellen, dass zusätzlicher Kontext benötigt wird, um eine Verbindung zu glätten, da die Umbenennung ein "Finalisierungs"-Schritt ist, ist nichts falsch daran, if-Anweisungen zu verwenden, um zusätzlichen Kontext hinzuzufügen. Bei vielen Funktionen besteht jedoch kaum Bedarf für einen solchen zusätzlichen Kontext.

@guybrand , Tryhard-Zahlen sind großartig, aber noch besser wären Beschreibungen, warum bestimmte Beispiele nicht konvertiert wurden und außerdem unangemessen gewesen wären, umzuschreiben, um konvertiert werden zu können. Das Beispiel und die Erklärung von @ tv42 sind ein Beispiel dafür.

@griesemer über deine Bedenken bezüglich defer . Ich wollte das emit oder im ersten Vorschlag handle . emit/handle würde aufgerufen werden, wenn err nicht Null ist. Und wird in diesem Moment statt am Ende der Funktion initiiert. Der defer wird am Ende aufgerufen. emit/handle WÜRDE die Funktion basierend darauf beenden, ob err null ist oder nicht. Deshalb würde aufschieben nicht funktionieren.

Daten:

aus einem ~70.000 LOC-Projekt, das ich verhökert habe, um "nackte Fehlerrückgaben" religiös zu eliminieren, haben wir immer noch 612 nackte Fehlerrückgaben. Meist handelt es sich um einen Fall, in dem ein Fehler protokolliert wird, die Nachricht jedoch nur intern wichtig ist (die Nachricht an den Benutzer ist vordefiniert). try() wird jedoch eine größere Einsparung als nur zwei Zeilen pro nackter Rückgabe haben, da wir mit vordefinierten Fehlern einen Handler zurückstellen und try an mehr Stellen verwenden können.

Interessanterweise haben wir im Herstellerverzeichnis von über 620.000 LOC nur 1600 nackte Fehlerrückgaben. Bibliotheken, die wir wählen, neigen dazu, Fehler noch religiöser zu schmücken als wir es tun.

@rsc Wenn später Handler zu try hinzugefügt werden, wird es dann ein Fehler/errc-Paket mit Funktionen wie func Wrap(msg string) func(error) error geben, damit Sie try(f(), errc.Wrap("f failed")) tun können?

@damienfamed75 Danke für deine Erklärungen . Das emit wird also aufgerufen, wenn try einen Fehler findet, und es wird mit diesem Fehler aufgerufen. Das scheint klar genug.

Sie sagen auch, dass emit die Funktion beenden würde, wenn ein Fehler auftritt, und nicht, wenn der Fehler irgendwie behandelt wurde. Wenn Sie die Funktion nicht beenden, wo wird der Code fortgesetzt? Vermutlich mit Rückkehr von try (sonst verstehe ich das emit nicht, das die Funktion nicht beendet). Wäre es in diesem Fall nicht einfacher und klarer, einfach ein if anstelle von try zu verwenden? Die Verwendung emit oder handle würde in diesen Fällen den Kontrollfluss enorm verschleiern, insbesondere weil die Klausel emit in einem völlig anderen Teil (vermutlich früher) in der Funktion stehen kann. (In diesem Sinne, kann man mehr als ein emit haben? Wenn nein, warum nicht? Was passiert, wenn es kein emit gibt? Viele der gleichen Fragen, die das ursprüngliche check geplagt haben handle Designentwurf.)

Nur wenn man von einer Funktion ohne viel zusätzliche Arbeit neben der Fehlerdekoration oder mit immer der gleichen Arbeit zurückkehren möchte, macht es Sinn, try und eine Art Handler zu verwenden. Und dieser Handler-Mechanismus, der ausgeführt wird, bevor eine Funktion zurückkehrt, existiert bereits in defer .

@guybrand (und @griesemer) in Bezug auf Ihr zweites nicht erkanntes Muster, siehe https://github.com/griesemer/tryhard/issues/2

@dave

Wie wird der Wert aus den Daten abgeleitet, wenn ein Großteil des öffentlichen Prozesses von Stimmungen bestimmt wird?

Vielleicht haben ja auch andere hier von einer Erfahrung wie meiner berichtet . Ich hatte erwartet, ein paar Instanzen von try , die von tryhard #$ eingefügt wurden, durchzublättern, zu finden, dass sie mehr oder weniger so aussahen wie das, was bereits in diesem Thread existierte, und weiterzumachen. Stattdessen war ich überrascht, einen Fall zu finden, in dem try zu deutlich besserem Code führte, auf eine Weise, die vorher nicht diskutiert worden war.

Es besteht also zumindest Hoffnung. :)

Wenn Sie tryhard ausprobieren, würde ich Sie ermutigen, sich nicht nur anzusehen, welche Änderungen das Tool vorgenommen hat, sondern auch nach verbleibenden Instanzen von err != nil zu suchen und sich diese anzusehen was es allein ließ und warum.

(Und beachten Sie auch, dass es unter https://github.com/griesemer/tryhard/ einige Probleme und PRs gibt.)

@rsc hier ist mein Einblick, warum ich persönlich das Muster defer HandleFunc(&err, ...) nicht mag. Es liegt nicht daran, dass ich es mit nackten Renditen oder so in Verbindung bringe, es fühlt sich einfach zu "clever" an.

Vor ein paar Monaten (vielleicht einem Jahr?) gab es einen Vorschlag zur Fehlerbehandlung, aber ich habe ihn jetzt aus den Augen verloren. Ich habe vergessen, was angefordert wurde, aber jemand hatte mit etwas in der Art von geantwortet:

func myFunction() (i int, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("wrapping the error: %s", err)
        }
    }()

    // ...
    return 0, err

    // ...
    return someInt, nil
}

Es war zumindest interessant zu sehen. Es war das erste Mal, dass defer zur Fehlerbehandlung verwendet wurde, und jetzt wird es hier gezeigt. Ich sehe es als "clever" und "hacky" an, und zumindest in dem Beispiel, das ich anführe, fühlt es sich nicht wie Go an. Wenn Sie es jedoch in einen richtigen Funktionsaufruf mit etwas wie fmt.HandleErrorf einpacken, fühlt es sich viel besser an. Ich stehe dem aber immer noch negativ gegenüber.

Ein weiterer Grund, warum ich sehe, dass die Leute es nicht mögen, ist, dass wenn man return ..., err schreibt, es so aussieht, als ob err zurückgegeben werden sollte. Aber es wird nicht zurückgegeben, stattdessen wird der Wert vor dem Senden geändert. Ich habe bereits gesagt, dass return in Go immer wie eine "heilige" Operation erschienen ist und dass es sich einfach falsch anfühlt, Code zu ermutigen, der einen zurückgegebenen Wert ändert, bevor er tatsächlich zurückgegeben wird.

OK, Zahlen und Daten sind es dann. :)

Ich habe die Quellen verschiedener Dienste unserer Microservice-Plattform versucht und mit den Ergebnissen von loccount und grep 'if err' verglichen. Ich habe die folgenden Ergebnisse in der Reihenfolge loccount / grep 'if err' | wc / tryhard:

1382 / 64 / 14
108554 / 66 / 5
58401 / 22 / 5
2052/247/39
12024 / 1655 / 1

Einige unserer Microservices machen viel Fehlerbehandlung und andere nur wenig, aber leider konnte tryhard nur in besten 22% der Fälle den Code automatisch verbessern, im schlimmsten Fall weniger als 1%. Jetzt werden wir unsere Fehlerbehandlung nicht manuell umschreiben, daher ist ein Tool wie tryhard unerlässlich, um try() in unsere Codebasis einzuführen. Ich schätze, dass dies ein einfaches vorläufiges Werkzeug ist, aber ich war überrascht, wie selten es helfen konnte.

Aber ich denke, dass ich jetzt, mit der Zahl in der Hand, sagen kann, dass try() für unseren Gebrauch kein Problem wirklich löst, oder zumindest nicht, bis tryhard viel besser wird.

Ich habe auch in unseren Codebasen festgestellt, dass der if err != nil { return err } Anwendungsfall von try() eigentlich sehr selten ist, anders als im Go-Compiler, wo er üblich ist. Bei allem Respekt, aber ich denke, dass die Go-Designer, die sich den Go-Compiler-Quellcode weitaus häufiger als andere Codebasen ansehen, den Nutzen von try() deswegen überschätzen.

@beoran tryhard ist im Moment noch sehr rudimentär. Haben Sie ein Gefühl für die häufigsten Gründe, warum try in Ihrer Codebasis selten wäre? Bsp weil du die Fehler dekorierst? Weil Sie vor der Rückkehr andere Extraarbeiten erledigen? Etwas anderes?

@rsc , @griesemer

Als Beispiele habe ich hier zwei sich wiederholende Beispiele gegeben, die tryHard übersehen hat, eines wird wahrscheinlich als "if Err :=" bleiben, das andere kann aufgelöst werden

Was die Fehlerdekoration betrifft , sind zwei wiederkehrende Muster, die ich im Code sehe, (ich habe die beiden in einem Code-Snippet eingefügt):

if v, err := someFunction(vars...) ; err != nil {
        return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
            strParam, intParam, err)
    } else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
        extraData := DoSomethingAccordingTo(v2,err)
        return formatError(err,extraData)
    } else {

    }

Und oft ist der formatError ein Standard für die App oder Cross-Repos, am häufigsten wiederholt sich die DbError-Formatierung (eine Funktion in allen Apps, die an Dutzenden von Orten verwendet wird), in einigen Fällen (ohne auf „Ist das eine korrektes Muster") und speichert einige Daten im Protokoll (bei fehlgeschlagener SQL-Abfrage möchten Sie den Stack nicht weitergeben) und einen anderen Text für den Fehler.

Mit anderen Worten, wenn ich mit zusätzlichen Daten wie dem Protokollieren von Fehler A und dem Auslösen von Fehler B etwas Kluges machen möchte, zusätzlich zu meiner Erwähnung dieser beiden Optionen zur Erweiterung der Fehlerbehandlung
Dies ist eine weitere Option, um "mehr als nur den Fehler zurückzugeben und ihn von 'jemand anderem' oder 'irgendeiner anderen Funktion' behandeln zu lassen".

Das bedeutet, dass try() wahrscheinlich mehr in "Bibliotheken" als in "ausführbaren Programmen" verwendet wird. Vielleicht werde ich versuchen, den Total/Errs/tryHard-Vergleich auszuführen, der Libs von Runnables ("Apps") unterscheidet.

Ich befand mich genau in der in https://github.com/golang/go/issues/32437#issuecomment -503297387 beschriebenen Situation
In einigen Leveln schließe ich Fehler einzeln ein, ich werde dies nicht mit try ändern, mit $# if err!=nil 1$#$ ist es in Ordnung.
Auf einer anderen Ebene habe ich nur return err es ist ein Schmerz, den gleichen Kontext für alle Rückgaben hinzuzufügen, dann werde ich try und defer verwenden.
Ich mache das sogar schon mit einem speziellen Logger, den ich zu Beginn der Funktion nur im Fehlerfall verwende. Für mich try und Dekoration nach Funktion ist schon spitze.

@thepudds

Wenn try hypothetisch in etwas wie Go 1.15 landen sollte, lautet die sehr kurze Antwort auf Ihre Frage, dass jemand Go 1.13 verwendet

Go 1.13 ist noch nicht einmal freigegeben, also kann ich es nicht verwenden. Und da mein Projekt keine Go-Module verwendet, kann ich nicht auf Go 1.13 aktualisieren. (Ich glaube, dass Go 1.13 von jedem verlangen wird, Go-Module zu verwenden.)

Code mit try zu erstellen, würde einen Kompilierfehler wie diesen sehen:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Zumindest soweit ich verstehe, was über den Übergangsvorschlag gesagt wurde).

Das ist alles hypothetisch. Es fällt mir schwer, mich zu fiktiven Dingen zu äußern. Und vielleicht gefällt Ihnen dieser Fehler, aber ich finde ihn verwirrend und wenig hilfreich.

Wenn try undefiniert ist, würde ich danach suchen. Und ich werde nichts finden. Was sollte ich dann tun?

Und die note: module requires Go 1.15 sind in dieser Situation die schlechteste Hilfe. Warum module ? Warum Go 1.15 ?

@griesemer

Dies ist vielleicht die erste vorgeschlagene Sprachänderung, die das Gefühl der Sprache auf substanziellere Weise beeinflusst. Wir sind uns dessen bewusst, weshalb wir es so minimal gehalten haben. (Ich kann mir nur schwer vorstellen, welchen Aufruhr ein konkreter Generika-Vorschlag auslösen könnte - sprechen wir von einer Sprachänderung).

Ich würde lieber Zeit mit Generika verbringen, als es zu versuchen. Vielleicht ist es ein Vorteil, Generika in Go zu haben.

Aber zurück zu Ihrem Punkt: Programmierer gewöhnen sich daran, wie eine Programmiersprache funktioniert und sich anfühlt. ...

Ich stimme allen Ihren Punkten zu. Aber wir sprechen davon, eine bestimmte Form der if-Anweisung durch den try-Funktionsaufruf zu ersetzen. Dies liegt in der Sprache, die auf Einfachheit und Orthogonalität stolz ist. Ich kann mich an alles gewöhnen, aber was ist der Sinn? Um ein paar Codezeilen zu sparen?

Oder betrachten Sie das folgende Gedankenexperiment: Stellen Sie sich vor, Go hätte keine defer -Anweisung und jetzt macht jemand starke Argumente für defer . defer hat keine Semantik wie irgendetwas anderes in der Sprache, die neue Sprache fühlt sich vor defer Go nicht mehr so ​​an. Doch nachdem ich ein Jahrzehnt damit gelebt habe, scheint es total "Go-like".

Nach vielen Jahren werde ich immer noch von defer body ausgetrickst und über Variablen geschlossen. Aber defer zahlt seinen Preis in höchstem Maße, wenn es um Ressourcenmanagement geht. Ich kann mir Go ohne defer nicht vorstellen. Aber ich bin nicht bereit, einen ähnlichen Preis für try zu bezahlen, weil ich hier keine Vorteile sehe.

Aus diesem Grund bitten wir die Leute, sich tatsächlich mit der Änderung auseinanderzusetzen, indem Sie damit in Ihrem eigenen Code experimentieren. dh ihn tatsächlich schreiben, oder tryhard existierenden Code überarbeiten lassen und das Ergebnis betrachten. Ich würde empfehlen, es eine Weile ruhen zu lassen, vielleicht eine Woche oder so. Schau es dir nochmal an und berichte.

Ich habe versucht, ein kleines Projekt von mir zu ändern (ungefähr 1200 Codezeilen). Und es sieht ähnlich aus wie Ihre Änderung unter https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go Ich sehe meine Meinung nicht ändere das nach einer Woche. Mein Geist ist immer mit etwas beschäftigt, und ich werde es vergessen.

... Aber es ist noch früh, dieser Vorschlag ist erst seit zwei Wochen draußen, ...

Und ich kann sehen, dass es in diesem Thread bereits 504 Nachrichten zu diesem Vorschlag gibt. Wenn ich daran interessiert wäre, diese Veränderung voranzutreiben, würde ich Tage, wenn nicht Wochen brauchen, um das alles zu lesen und zu verstehen. Ich beneide dich nicht um deinen Job.

Vielen Dank, dass Sie sich die Zeit genommen haben, auf meine Nachricht zu antworten. Tut mir leid, wenn ich nicht auf diesen Thread antworte - er ist einfach zu groß für mich, um zu überwachen, ob die Nachricht an mich adressiert ist oder nicht.

Alex

@griesemer Danke für den wunderbaren Vorschlag und Tryhard scheint nützlicher zu sein, als ich erwarte. Ich werde auch schätzen wollen.

@rsc danke für die gut artikulierte Antwort und das Tool.

Ich verfolge diesen Thread schon eine Weile und die folgenden Kommentare von @beoran lassen mich frösteln

Das Ausblenden der Fehlervariablen und der Rückgabe trägt nicht zum besseren Verständnis bei

Habe schon mehrere bad written code gemanagt und ich kann bezeugen, dass es der schlimmste Albtraum für jeden Entwickler ist.

Die Tatsache, dass die Dokumentation sagt, dass A Likes verwendet werden, bedeutet nicht, dass sie befolgt würden, die Tatsache bleibt bestehen, wenn es möglich ist, AA , AB zu verwenden, dann gibt es keine Begrenzung dafür, wie es kann benutzt werden.

To my surprise, people already think the code below is cool ... Ich denke, it's an abomination bei allem Respekt, Entschuldigung an alle, die beleidigt sind.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Warten Sie, bis Sie AsCommit überprüfen und Sie sehen

func AsCommit() error(){
    return try(try(try(tail()).find()).auth())
}

Der Wahnsinn geht weiter und ehrlich gesagt möchte ich nicht glauben, dass dies die Definition von @robpike ist simplicity is complicated (Humor)

Basierend auf dem @rsc- Beispiel

// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// Example 2 
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// Example 3 
try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Bin für Example 2 mit etwas else , bitte beachten Sie jedoch, dass dies möglicherweise nicht der beste Ansatz ist

  • Es ist leicht, den Fehler deutlich zu sehen
  • Am wenigsten möglich, in die abomination zu mutieren, die die anderen gebären können
  • try verhält sich nicht wie eine normale Funktion. ihm eine funktionsähnliche Syntax zu geben, ist wenig sinnvoll. go verwendet if und wenn ich es einfach in try tree := r.LookupTree(treeOid) else { ändern kann, fühlt es sich natürlicher an
  • Fehler können sehr, sehr teuer sein, sie brauchen so viel Sichtbarkeit wie möglich, und ich denke, das ist der Grund, warum go die traditionellen try & catch nicht unterstützt hat
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)

parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()

try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else { 
    // Heal the world 
   // I may return with return keyword 
   // I may not return but set some values to 0 
   // I may remember I need to log only this 
   // I may send a mail to let the cute monkeys know the server is on fire 
}

Noch einmal möchte ich mich dafür entschuldigen, dass ich ein wenig egoistisch bin.

@josharian Ich kann hier nicht zu viel verraten, aber die Gründe sind sehr vielfältig. Wie Sie sagen, dekorieren wir die Fehler und/oder führen auch eine andere Verarbeitung durch, und ein wichtiger Anwendungsfall besteht darin, dass wir sie protokollieren, wobei die Protokollnachricht für jeden Fehler unterschiedlich ist, den eine Funktion zurückgeben kann, oder weil wir if err := foo() ; err != nil { /* various handling*/ ; return err } verwenden

Was ich betonen möchte, ist Folgendes: Der einfache Anwendungsfall, für den try() entworfen wurde, kommt in unserer Codebasis nur sehr selten vor. Für uns bringt es also nicht viel, der Sprache 'try()' hinzuzufügen.

BEARBEITEN: Wenn try() implementiert wird, sollte der nächste Schritt meiner Meinung nach darin bestehen, tryhard viel besser zu machen, damit es weit verbreitet verwendet werden kann, um vorhandene Codebasen zu aktualisieren.

@griesemer Ich werde versuchen, alle Ihre Bedenken aus Ihrer letzten Antwort nacheinander anzusprechen.
Zuerst haben Sie gefragt, ob der Handler die Funktion nicht auf irgendeine Weise zurückgibt oder beendet, was dann passieren würde. Ja, es kann Fälle geben, in denen die Klausel emit / handle eine Funktion nicht zurückgibt oder beendet, sondern dort weitermacht, wo sie aufgehört hat. Wenn wir beispielsweise versuchen, mit einem Lesegerät ein Trennzeichen oder etwas Einfaches zu finden, und wir das EOF erreichen, möchten wir möglicherweise keinen Fehler zurückgeben, wenn wir darauf stoßen. Also habe ich dieses schnelle Beispiel dafür gebaut, wie das aussehen könnte:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err {
        // if this doesn't return then continue from where we left off
        // at the try function that was called last.
        if err != io.EOF {
            return nil, err
        }
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

Oder könnte sogar noch weiter vereinfacht werden:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err != io.EOF {
        return nil, err
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

Die zweite Sorge betraf die Unterbrechung des Kontrollflusses. Und ja, es würde den Fluss stören, aber um fair zu sein, stören die meisten Vorschläge den Fluss etwas, um eine zentrale Fehlerbehandlungsfunktion und dergleichen zu haben. Das ist glaube ich nicht anders.
Als nächstes haben Sie gefragt, ob wir emit / handle mehr als einmal verwendet haben, wobei ich sage, dass es neu definiert wurde.
Wenn Sie emit mehr als einmal verwenden, wird das letzte überschrieben und so weiter. Wenn Sie keine haben, hat try einen Standardhandler, der nur Nullwerte und den Fehler zurückgibt. Das bedeutet, dass dieses Beispiel hier:

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        return nil, err
    }

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Würde dasselbe tun wie in diesem Beispiel:

func writeStuff(filename string) (io.ReadCloser, error) {
    // when not defining a handler then try's default handler kicks in to
    // return nil valued then error as usual.

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Ihre letzte Frage betraf die Deklaration einer Handler-Funktion, die in einem defer aufgerufen wird, wobei ich einen Verweis auf einen error . Dieses Design funktioniert nicht auf die gleiche Weise wie dieser Vorschlag, da ein defer eine Funktion nicht sofort stoppen kann, wenn eine Bedingung selbst gegeben ist.

Ich glaube, ich habe in Ihrer Antwort alles angesprochen, und ich hoffe, dies verdeutlicht meinen Vorschlag noch ein wenig mehr. Wenn es noch Bedenken gibt, lassen Sie es mich wissen, denn ich denke, diese ganze Diskussion mit allen macht ziemlich viel Spaß, um über neue Ideen nachzudenken. Macht weiter so alle mit der großartigen Arbeit!

@velovix , zu https://github.com/golang/go/issues/32437#issuecomment -503314834:

Auch dies bedeutet, dass try auf die Reaktion auf Fehler setzt, aber nicht darauf, ihnen Kontext hinzuzufügen. Das ist eine Unterscheidung, auf die ich und vielleicht auch andere angespielt haben. Dies ist sinnvoll, da die Art und Weise, wie eine Funktion einem Fehler Kontext hinzufügt, für einen Leser nicht besonders interessant ist, aber die Art und Weise, wie eine Funktion auf Fehler reagiert, wichtig ist. Wir sollten die weniger interessanten Teile unseres Codes weniger ausführlich machen, und genau das tut try .

Das ist eine wirklich schöne Art, es auszudrücken. Danke.

@olekukonko , zu https://github.com/golang/go/issues/32437#issuecomment -503508478:

To my surprise, people already think the code below is cool ... Ich denke, it's an abomination bei allem Respekt, Entschuldigung an alle, die beleidigt sind.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Grepping https://swtch.com/try.html , dieser Ausdruck ist in diesem Thread dreimal vorgekommen.
@goodwine brachte es als schlechten Code zur Sprache, stimmte ich zu, und @velovix sagte: „Trotz seiner Hässlichkeit … ist es besser als das, was Sie oft in Try-Catch-Sprachen sehen … weil Sie immer noch erkennen können, welche Teile des Codes möglicherweise umgeleitet werden Kontrollfluss aufgrund eines Fehlers und was nicht kann."

Niemand sagte, es sei "cool" oder etwas, das man als großartigen Code herausstellen könnte. Auch hier ist es immer möglich, schlechten Code zu schreiben .

Ich würde auch einfach sagen re

Fehler können sehr, sehr teuer sein, sie brauchen so viel Sichtbarkeit wie möglich

Fehler in Go sollen nicht teuer sein. Sie sind alltägliche, gewöhnliche Ereignisse und sollen leicht sein. (Dies steht insbesondere im Gegensatz zu einigen Implementierungen von Ausnahmen. Wir hatten einmal einen Server, der viel zu viel CPU-Zeit damit verbrachte, Ausnahmeobjekte vorzubereiten und zu verwerfen, die Stack-Traces für fehlgeschlagene "Datei öffnen" -Aufrufe in einer Schleife enthielten, die eine Liste bekannter überprüfte Speicherorte für eine bestimmte Datei.)

@alexbrainman , es tut mir leid für die Verwirrung darüber, was passiert, wenn ältere Versionen von Go-Build-Code versuchen. Die kurze Antwort ist, dass es wie jedes Mal ist, wenn wir die Sprache ändern: Der alte Compiler wird den neuen Code mit einer meist nicht hilfreichen Meldung (in diesem Fall „undefined: try“) ablehnen. Die Meldung ist nicht hilfreich, da der alte Compiler die neue Syntax nicht kennt und nicht wirklich hilfreicher sein kann. Die Leute würden an diesem Punkt wahrscheinlich eine Websuche nach "go undefined try" durchführen und sich über die neue Funktion informieren.

Im Beispiel von @thepudds hat der Code, der try verwendet, eine go.mod, die die Zeile „go 1.15“ enthält, was bedeutet, dass der Autor des Moduls sagt, dass der Code für die Version der Go-Sprache geschrieben wurde. Dies dient als Signal an ältere go-Befehle, um nach einem Kompilierungsfehler darauf hinzuweisen, dass die nicht hilfreiche Meldung möglicherweise auf eine zu alte Version von Go zurückzuführen ist. Dies ist ausdrücklich ein Versuch, die Nachricht etwas hilfreicher zu gestalten, ohne die Benutzer zu zwingen, auf Websuchen zurückzugreifen. Wenn es hilft, gut; Wenn nicht, scheint die Websuche sowieso ziemlich effektiv zu sein.

@guybrand , re https://github.com/golang/go/issues/32437#issuecomment -503287670 und mit Entschuldigung dafür, dass Sie wahrscheinlich zu spät zu Ihrem Treffen kommen:

Ein allgemeines Problem bei Funktionen, die Nicht-ganz-Fehlertypen zurückgeben, besteht darin, dass für Nicht-Schnittstellen die Umwandlung in Fehler die Nichtigkeit nicht bewahrt. Wenn Sie also beispielsweise Ihren eigenen benutzerdefinierten *MyError-Konkrettyp haben (z. B. einen Zeiger auf eine Struktur) und err == nil als Erfolgssignal verwenden, ist das großartig, bis Sie es haben

func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }

Wenn f einen Nullwert *MyError zurückgibt, gibt g denselben Wert als Nicht-Null-Fehler zurück, was wahrscheinlich nicht beabsichtigt war. Wenn *MyError eine Schnittstelle anstelle eines Strukturzeigers ist, dann bewahrt die Konvertierung die Nichtigkeit, aber trotzdem ist es eine Subtilität.

Bei try könnten Sie denken, dass try nur für Nicht-Null-Werte ausgelöst wird, kein Problem. Zum Beispiel ist dies tatsächlich in Ordnung, soweit ein Nicht-Null-Fehler zurückgegeben wird, wenn f fehlschlägt, und es ist auch in Ordnung, soweit ein Null-Fehler zurückgegeben wird, wenn f erfolgreich ist:

func g() (int, error) {
    return try(f()), nil
}

Das ist eigentlich in Ordnung, aber dann sehen Sie das vielleicht und denken darüber nach, es umzuschreiben

func g() (int, error) {
    return f()
}

was so aussieht, als sollte es dasselbe sein, ist es aber nicht.

Es gibt genügend andere Details des Versuchsvorschlags, die einer sorgfältigen Prüfung und Bewertung in der realen Erfahrung bedürfen, sodass es schien, als ob die Entscheidung über diese besondere Subtilität am besten verschoben werden sollte.

Danke an alle für das bisherige Feedback . An diesem Punkt scheinen wir die wichtigsten Vorteile, Bedenken und möglichen positiven und negativen Auswirkungen von try identifiziert zu haben. Um Fortschritte zu erzielen, müssen diese weiter bewertet werden, indem untersucht wird, was try für tatsächliche Codebasen bedeuten würde. Die Diskussion an diesem Punkt dreht sich um dieselben Punkte und wiederholt sie.

Erfahrung ist jetzt wertvoller als fortgesetzte Diskussionen. Wir möchten die Leute ermutigen, sich die Zeit zu nehmen, damit zu experimentieren, wie try in ihren eigenen Codebasen aussehen würde, und Erfahrungsberichte auf der Feedback-Seite zu schreiben und zu verlinken.

Um allen etwas Zeit zum Atmen und Experimentieren zu geben, werden wir dieses Gespräch unterbrechen und das Thema für die nächsten anderthalb Wochen sperren.

Die Sperrung beginnt um 13 Uhr PDT/4 Uhr EDT (in etwa 3 Stunden ab jetzt), um den Leuten die Möglichkeit zu geben, einen ausstehenden Beitrag einzureichen. Wir werden das Thema für weitere Diskussionen am 1. Juli erneut öffnen.

Bitte seien Sie versichert, dass wir nicht die Absicht haben, neue Sprachfunktionen zu überstürzen, ohne uns die Zeit zu nehmen, sie gut zu verstehen und sicherzustellen, dass sie echte Probleme in echtem Code lösen. Wir werden uns die nötige Zeit nehmen, um dies richtig zu machen, so wie wir es in der Vergangenheit getan haben.

Diese Wiki-Seite ist voll mit Antworten zum Überprüfen/Handhaben. Ich schlage vor, Sie beginnen eine neue Seite.

Auf jeden Fall werde ich keine Zeit mehr haben, im Wiki weiter zu gärtnern.

@networkimprov , danke für deine Hilfe beim Gärtnern. Ich habe einen neuen oberen Abschnitt in https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback erstellt. Ich denke, das sollte besser sein als eine ganz neue Seite.

Ich habe auch Roberts 1p PDT / 4p EDT-Hinweis für die Sperre vermisst, also habe ich sie kurzzeitig etwas zu früh gesperrt. Es ist wieder geöffnet, etwas länger.

Ich hatte vor, dies zu schreiben, und wollte es nur fertigstellen, bevor es gesperrt wird.

Ich hoffe, dass das Go-Team die Kritik nicht sieht und das Gefühl hat, dass sie die Stimmung der Mehrheit widerspiegelt. Es gibt immer die Tendenz, dass die lautstarke Minderheit das Gespräch überwältigt, und ich habe das Gefühl, dass das hier passiert sein könnte. Wenn jeder auf eine Tangente geht, entmutigt es andere, die nur über den Vorschlag so sprechen wollen, wie er ist.

Also - ich möchte meine positive Position für das, was es wert ist, artikulieren.

Ich habe Code, der Defer bereits zum Dekorieren/Annotieren von Fehlern verwendet, sogar zum Ausspucken von Stacktraces, genau aus diesem Grund.

Sehen:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

die alle errorutil.OnError(*error) aufrufen

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

Dies ist in Anlehnung an die zuvor von Russ/Robert erwähnten defer-Helfer.

Es ist ein Muster, das ich bereits verwende, FWIW. Es ist keine Magie. Es ist IMHO völlig go-like.

Ich verwende es auch mit benannten Parametern, und es funktioniert hervorragend.

Ich sage dies, um die Vorstellung zu bestreiten, dass alles, was hier empfohlen wird, magisch ist.

Zweitens wollte ich einige Kommentare zu try(...) als Funktion hinzufügen.
Es hat einen klaren Vorteil gegenüber einem Schlüsselwort, da es um Parameter erweitert werden kann.

Es gibt 2 Erweiterungsmodi, die hier besprochen wurden:

  • Erweitern versuchen, ein Label zu nehmen, zu dem gesprungen werden kann
  • Erweitern versuchen, einen Handler der Form func(error) error zu nehmen

Für jeden von ihnen ist es erforderlich, dass try als Funktion einen einzelnen Parameter übernimmt, und sie kann später erweitert werden, um bei Bedarf einen zweiten Parameter zu übernehmen.

Es ist noch nicht entschieden, ob eine Ausweitung des Versuchs erforderlich ist und wenn ja, in welche Richtung. Folglich besteht die erste Richtung darin, zu versuchen, den größten Teil des „if err != nil { return err }“-Stotterns zu eliminieren, das ich schon immer gehasst habe, das ich aber als die Kosten für die Abwicklung von Geschäften angesehen habe.

Ich persönlich bin froh, dass try eine Funktion ist, die ich inline aufrufen kann zB ich schreiben kann

var u User = db.loadUser(try(strconv.Atoi(stringId)))

Im Gegensatz zu:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

Wie Sie sehen können, habe ich gerade 6 Zeilen auf 1 reduziert. Und 5 dieser Zeilen sind wirklich Boilerplate.
Das ist etwas, mit dem ich mich oft befasst habe, und ich habe eine Menge Go-Code und -Pakete geschrieben - Sie können auf meinem Github nachsehen, um einige der von mir online geposteten oder meiner Go-Codec-Bibliothek zu sehen.

Schließlich haben viele der Kommentare hier nicht wirklich Probleme mit dem Vorschlag gezeigt, so sehr sie ihren eigenen bevorzugten Weg zur Lösung des Problems postuliert haben.

Ich persönlich bin begeistert, dass try(...) eintrifft. Und ich schätze die Gründe, warum try als Funktion die bevorzugte Lösung ist. Mir gefällt eindeutig, dass hier defer verwendet wird, da es gerade noch Sinn macht.

Erinnern wir uns an eines der Grundprinzipien von go – orthogonale Konzepte, die sich gut kombinieren lassen. Dieser Vorschlag nutzt eine Reihe von orthogonalen Konzepten von go (Verzögerung, benannte Rückgabeparameter, integrierte Funktionen, um das zu tun, was nicht über Benutzercode möglich ist, usw.), um den entscheidenden Vorteil zu bieten
Go-Benutzer haben seit Jahren allgemein gefordert, dh die Boilerplate if err != nil { return err } zu reduzieren/eliminieren. Die Go User Surveys zeigen, dass dies ein echtes Problem ist. Das go-Team ist sich bewusst, dass es sich um ein echtes Problem handelt. Ich bin froh, dass die lauten Stimmen einiger weniger die Position des Go-Teams nicht zu sehr verzerren.

Ich hatte eine Frage zu try als implizitem goto if err != nil.

Wenn wir entscheiden, dass dies die Richtung ist, wird es schwierig sein, "try does a return" in "try does a goto" umzuwandeln,
da goto eine Semantik definiert hat, die Sie nicht an nicht zugeordneten Variablen vorbeigehen lassen können?

Danke für deinen Hinweis, @ugorji.

Ich hatte eine Frage zu try als implizitem goto if err != nil.

Wenn wir entscheiden, dass dies die Richtung ist, wird es schwierig sein, "try does a return" in "try does a goto" umzuwandeln,
da goto eine Semantik definiert hat, die Sie nicht an nicht zugeordneten Variablen vorbeigehen lassen können?

Ja, genau richtig. Es gibt einige Diskussionen zu #26058.
Ich denke, 'try-goto' hat mindestens drei Streiks dagegen:
(1) Sie müssen nicht zugeordnete Variablen beantworten,
(2) Sie verlieren Stapelinformationen darüber, welcher Versuch fehlgeschlagen ist, die Sie im Gegensatz dazu im Fall von return+defer immer noch erfassen können, und
(3) Jeder liebt es, auf Goto zu hassen.

Ja, try ist der richtige Weg.
Ich habe einmal versucht, try hinzuzufügen, und es hat mir gefallen.
Patch – https://github.com/ascheglov/go/pull/1
Thema auf Reddit - https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

@griesemer

Fortsetzung von https://github.com/golang/go/issues/32825#issuecomment -507120860 ...

Unter der Prämisse, dass der Missbrauch von try durch Codeüberprüfung, Überprüfung und/oder Community-Standards gemildert wird, sehe ich es als sinnvoll an, die Sprache nicht zu ändern, um die Flexibilität von try einzuschränken

Wenn man dies etwas aufschlüsselt, scheint es zwei Formen des Fehlerpfad-Kontrollflusses zu geben, die ausgedrückt werden: manuell und automatisch. In Bezug auf das Umschließen von Fehlern scheinen drei Formen ausgedrückt zu werden: Direkt, Indirekt und Pass-Through. Dies führt zu insgesamt sechs "Modi" der Fehlerbehandlung.

Die Modi Manual Direct und Automatic Direct scheinen angenehm zu sein:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
    return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}

„Geh
wrap := errors.WrapfFunc("%s konnte nicht verarbeitet werden", Dateiname)

f, err := os.Open(Dateiname)
try(wrap(err))
verzögern f.Close()

info, ähm := f.Stat()
try(wrap(err))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
    return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Allerdings sind die manuellen indirekten und automatischen indirekten Modi aufgrund der hohen Wahrscheinlichkeit subtiler Fehler beide ziemlich unangenehm:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
    return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
    return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Auch hier kann ich verstehen, dass ich sie nicht verbiete, aber das Ermöglichen / Segnen der indirekten Modi ist der Punkt, an dem dies für mich immer noch klare rote Fahnen weckt. Zu diesem Zeitpunkt genug, um der gesamten Prämisse gegenüber nachdrücklich skeptisch zu bleiben.

Versuchen muss keine Funktion sein, um das verdammt zu vermeiden

info := try(try(os.Open(filename)).Stat())

Dateileck.

Ich meine, die try -Anweisung erlaubt keine Verkettung. Und als Bonus sieht es besser aus. Es gibt jedoch Kompatibilitätsprobleme.

@sirkon Da try etwas Besonderes ist, könnte die Sprache verschachtelte try verbieten, wenn dies wichtig ist - selbst wenn try wie eine Funktion aussieht. Wenn dies die einzige Straßensperre für try ist, könnte dies auf verschiedene Weise leicht behoben werden ( go vet oder Sprachbeschränkung). Lassen Sie uns damit weitermachen - wir haben es jetzt schon oft gehört. Danke.

Lassen Sie uns damit weitermachen - wir haben es schon oft gehört

„Das ist so langweilig, lass uns damit weitermachen“

Es gibt noch ein gutes Analogon:

- Ihre Theorie widerspricht den Tatsachen!
- Umso schlimmer für die Tatsachen!

Von Hegel

Ich meine, Sie lösen ein Problem, das in Wirklichkeit nicht existiert. Und das auf die hässliche Art und Weise.

Werfen wir einen Blick darauf, wo dieses Problem tatsächlich auftritt: Umgang mit Nebenwirkungen aus der Außenwelt, das war's. Und dies ist logischerweise einer der einfachsten Teile im Software-Engineering. Und das wichtigste noch dazu. Ich kann nicht verstehen, warum wir eine Vereinfachung für die einfachste Sache brauchen, die uns weniger Zuverlässigkeit kostet.

IMO ist das schwierigste Problem dieser Art die Erhaltung der Datenkonsistenz in verteilten Systemen (und tatsächlich nicht so verteilt). Und die Fehlerbehandlung war kein Problem, mit dem ich in Go gekämpft habe, als ich diese gelöst habe. Das Fehlen von Slice- und Map-Verständnissen, das Fehlen von Summe/Algebra/Varianz/was auch immer für Typen war VIEL ärgerlicher.

Da die Debatte hier unvermindert weitergeht, lassen Sie mich noch einmal wiederholen :

Erfahrung ist jetzt wertvoller als fortgesetzte Diskussionen. Wir möchten die Leute ermutigen, sich die Zeit zu nehmen, damit zu experimentieren, wie try in ihren eigenen Codebasen aussehen würde, und Erfahrungsberichte auf der Feedback-Seite zu schreiben und zu verlinken.

Wenn konkrete Erfahrungen wesentliche Hinweise für oder gegen diesen Vorschlag liefern, würden wir das gerne hier hören. Persönliche Ärgernisse, hypothetische Szenarien, alternative Designs usw. können wir anerkennen, aber sie sind weniger umsetzbar.

Danke.

Ich möchte hier nicht unhöflich sein und schätze all Ihre Moderation, aber die Community hat sich sehr stark dafür ausgesprochen, dass die Fehlerbehandlung geändert wird. Dinge zu ändern oder neuen Code hinzuzufügen, wird _all_ die Leute verärgern, die das gegenwärtige System bevorzugen. Sie können nicht alle glücklich machen, konzentrieren wir uns also auf die 88 %, die wir glücklich machen können (Zahl, die sich aus dem Stimmenverhältnis unten ergibt).

Zum Zeitpunkt des Verfassens dieses Artikels hat der Thread „Lass es in Ruhe“ 1322 Stimmen nach oben und 158 nach unten. Dieser Thread ist bei 158 hoch und 255 runter. Wenn das kein direktes Ende dieses Threads zur Fehlerbehandlung ist, dann sollten wir einen sehr guten Grund haben, das Problem weiter voranzutreiben.

Es ist möglich, immer das zu tun, wonach Ihre Community schreit, und gleichzeitig Ihr Produkt zu zerstören.

Zumindest denke ich, dass dieser spezifische Vorschlag als gescheitert betrachtet werden sollte.

Glücklicherweise wird go nicht vom Komitee entworfen. Wir müssen darauf vertrauen, dass die Hüter der Sprache, die wir alle lieben, angesichts aller ihnen zur Verfügung stehenden Daten weiterhin die beste Entscheidung treffen und keine Entscheidung auf der Grundlage der Volksmeinung der Massen treffen werden. Denken Sie daran - sie benutzen auch go, genau wie wir. Sie spüren die Schmerzpunkte, genau wie wir.

Wenn Sie eine Position haben, nehmen Sie sich die Zeit, sie zu verteidigen, so wie das Go-Team seine Vorschläge verteidigt. Sonst ertränken Sie das Gespräch nur mit nächtlichen Gefühlen, die nicht umsetzbar sind und die Gespräche nicht voranbringen. Und es macht es schwieriger für Leute, die sich engagieren wollen, da die besagten Leute vielleicht einfach warten wollen, bis der Lärm nachlässt.

Als der Vorschlagsprozess begann, machte Russ viel Aufhebens darum, die Notwendigkeit von Erfahrungsberichten zu evangelisieren, um einen Vorschlag zu beeinflussen oder Ihrer Bitte Gehör zu verschaffen. Lasst uns zumindest versuchen, das zu ehren.

Das go-Team hat alle umsetzbaren Rückmeldungen berücksichtigt. Sie haben uns noch nicht enttäuscht. Sehen Sie sich die detaillierten Dokumente an, die für Alias, Module usw. erstellt wurden. Lassen Sie uns ihnen zumindest die gleiche Beachtung schenken und Zeit darauf verwenden, unsere Einwände zu durchdenken, auf ihre Position zu Ihren Einwänden einzugehen und es schwieriger zu machen, dass Ihr Einwand ignoriert wird.

Der Vorteil von Go war schon immer, dass es sich um eine kleine, einfache Sprache mit orthogonalen Konstrukten handelt, die von einer kleinen Gruppe von Leuten entworfen wurde, die den Raum kritisch durchdenken, bevor sie sich auf eine Position festlegen. Lassen Sie uns ihnen helfen, wo wir können, anstatt einfach zu sagen: „Schauen Sie, die Volksabstimmung sagt nein“ – wo viele Leute, die wählen, vielleicht nicht einmal viel Erfahrung mit Go haben oder Go vollständig verstehen. Ich habe Serienposter gelesen, die zugaben, dass sie einige grundlegende Konzepte dieser zugegebenermaßen kleinen und einfachen Sprache nicht kennen. Das macht es schwer, Ihr Feedback ernst zu nehmen.

Wie auch immer, scheiße, dass ich das hier mache – Sie können diesen Kommentar gerne entfernen. Ich werde nicht beleidigt sein. Aber irgendjemand muss das ganz unverblümt sagen!

Diese ganze Sache mit dem zweiten Vorschlag sieht sehr ähnlich aus wie digitale Influencer, die eine Rallye für mich organisieren. Bei Popularitätswettbewerben werden keine technischen Vorzüge bewertet.

Die Leute mögen schweigen, aber sie erwarten immer noch Go 2. Ich persönlich freue mich darauf und auf den Rest von Go 2. Go 1 ist eine großartige Sprache und eignet sich gut für verschiedene Arten von Programmen. Ich hoffe, Go 2 wird das erweitern.

Schließlich werde ich auch meine Präferenz für try als Aussage umkehren. Jetzt unterstütze ich den Vorschlag so wie er ist. Nach so vielen Jahren unter dem Kompatibilitätsversprechen "Go 1" denken die Leute, Go sei in Stein gemeißelt. Aufgrund dieser problematischen Annahme scheint es in meinen Augen jetzt ein viel besserer Kompromiss zu sein, die Sprachsyntax in diesem Fall nicht zu ändern. Edit: Ich freue mich auch auf die Erfahrungsberichte zum Faktencheck.

PS: Ich frage mich, welche Art von Widerstand es geben wird, wenn Generika vorgeschlagen werden.

Wir haben in unserem Unternehmen rund ein Dutzend Tools am Stück geschrieben. Ich habe das Tryhard-Tool gegen unsere Codebasis laufen lassen und 933 potenzielle try()-Kandidaten gefunden. Ich persönlich glaube, dass die Funktion try() eine brillante Idee ist, weil sie mehr als nur Probleme mit Codebausteinen löst.

Es zwingt sowohl den Aufrufer als auch die aufgerufene Funktion/Methode, den Fehler als letzten Parameter zurückzugeben. Das ist nicht erlaubt:

var file= try(parse())

func parse()(err, result) {
}

Es erzwingt eine Möglichkeit, mit Fehlern umzugehen, anstatt die Fehlervariable zu deklarieren und das Muster err!=nil err==nil locker zuzulassen, was die Lesbarkeit behindert und das Risiko von fehleranfälligem Code in IMO erhöht:

func Foo() (err error) {
    var file, ferr = os.Open("file1.txt")
    if ferr == nil {
               defer file.Close()
        var parsed, perr = parseFile(file)
        if perr != nil {
            return
        }
        fmt.Printf("%s", parsed)
    }
    return nil
}

Mit try() ist Code meiner Meinung nach lesbarer, konsistenter und sicherer:

func Foo() (err error) {
    var file = try(os.Open("file.txt"))
        defer file.Close()
    var parsed = try(parseFile(file))
    fmt.Printf(parsed)
    return
}

Ich habe einige Experimente durchgeführt, die denen von @lpar an allen nicht archivierten Go-Repositories von Heroku (öffentlich und privat) ähneln.

Die Ergebnisse sind in diesem Kernstück: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763

cc @davecheney

@ubikenobi Deine sicherere Funktion ~ist~ war undicht.

Außerdem habe ich noch nie einen Wert gesehen, der nach einem Fehler zurückgegeben wurde. Ich könnte mir jedoch vorstellen, dass es sinnvoll ist, wenn es bei einer Funktion nur um den Fehler geht und die anderen zurückgegebenen Werte nicht vom Fehler selbst abhängen (was möglicherweise zu zwei Fehlerrückgaben führt, wobei der zweite die vorherigen Werte "bewacht").

Schließlich bietet err == nil , obwohl es nicht üblich ist, einen legitimen Test für einige frühe Renditen.

@Dave

Vielen Dank für den Hinweis auf Lecks, ich habe vergessen, defer.Close() in beiden Beispielen hinzuzufügen. (jetzt aktualisiert).

Ich sehe auch selten, dass err in dieser Reihenfolge zurückkehrt, aber es ist immer noch gut, diese zur Kompilierzeit abfangen zu können, wenn es sich um Fehler handelt, die nicht beabsichtigt sind.

Ich sehe den Fall err==nil in den meisten Fällen als Ausnahme und nicht als Norm. Es kann in einigen Fällen nützlich sein, wie Sie erwähnt haben, aber was ich nicht mag, sind Entwickler, die ohne triftigen Grund inkonsistent wählen. Glücklicherweise sind in unserer Codebasis die meisten Anweisungen err!=nil, was leicht von der Funktion try() profitieren kann.

  • Ich habe tryhard gegen eine große Go-API laufen lassen, die ich mit einem Team von vier anderen Ingenieuren Vollzeit pflege. In 45580 Zeilen Go-Code identifizierte tryhard 301 Fehler zum Neuschreiben (es wäre also eine +301/-903-Änderung) oder würde etwa 2 % des Codes neu schreiben, vorausgesetzt, jeder Fehler dauert etwa 3 Zeilen. Unter Berücksichtigung von Kommentaren, Leerzeichen, Importen usw. fühlt sich das für mich erheblich an.
  • Ich habe tryhards Linienwerkzeug verwendet, um herauszufinden, wie try meine Arbeit verändern würde, und subjektiv fließt es sehr gut zu mir! Das Verb try fühlt sich für mich klarer an, dass in der aufrufenden Funktion etwas schief gehen könnte, und führt es kompakt aus. Ich bin sehr daran gewöhnt, if err != nil zu schreiben, und es macht mir nichts aus, aber ich hätte auch nichts dagegen, mich zu ändern. Das wiederholte Schreiben und Umgestalten der leeren Variablen vor dem Fehler (dh das wiederholte Zurückgeben der leeren Slice/Map/Variable) ist wahrscheinlich mühsamer als err selbst.
  • Es ist etwas schwierig, allen Diskussionssträngen zu folgen, aber ich bin gespannt, was dies für Wrapping-Fehler bedeutet. Es wäre schön, wenn try variadisch wäre, wenn Sie optional einen Kontext wie try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user) hinzufügen möchten. Bearbeiten: Dieser Punkt ist wahrscheinlich nicht zum Thema; Wenn man sich jedoch Nicht-Try-Umschreibungen ansieht, passiert dies hier.
  • Ich schätze den Gedanken und die Sorgfalt, die hier hineingesteckt werden, sehr! Abwärtskompatibilität und Stabilität sind uns sehr wichtig, und die bisherigen Bemühungen um Go 2 verliefen für die Aufrechterhaltung von Projekten wirklich reibungslos. Danke!

Sollte dies nicht an einer Quelle erfolgen, die von erfahrenen Gophers überprüft wurde, um sicherzustellen, dass die Ersetzungen vernünftig sind? Wie viel von dieser "2%"-Umschreibung hätte mit expliziter Handhabung umgeschrieben werden sollen? Wenn wir das nicht wissen, bleibt LOC eine relativ nutzlose Metrik.

*Das ist genau der Grund, warum sich mein Beitrag heute Morgen auf „Modi“ der Fehlerbehandlung konzentrierte. Es ist einfacher und substanzieller, die Modi der Fehlerbehandlung zu diskutieren, die try erleichtert, und dann mit potenziellen Gefahren des Codes zu ringen, den wir wahrscheinlich schreiben werden, als einen ziemlich willkürlichen Zeilenzähler laufen zu lassen.

@kingishb Wie viele der gefundenen _try_-Spots befinden sich in öffentlichen Funktionen aus Nicht-Hauptpaketen? Typischerweise sollten öffentliche Funktionen paketnative (d. h. verpackte oder dekorierte) Fehler zurückgeben....

@networkimprov Das ist eine zu einfache Formel für meine Sensibilität. Wo das wahr klingt, ist in Bezug auf API-Oberflächen, die inspizierbare Fehler zurückgeben. Normalerweise ist es angemessen, Kontext zu einer Fehlermeldung basierend auf der Relevanz des Kontexts hinzuzufügen, nicht auf seiner Position in der Aufrufliste.

Viele Fehlalarme machen es wahrscheinlich in den aktuellen Metriken durch. Und was ist mit Fehlern, die aufgrund der folgenden empfohlenen Vorgehensweisen auftreten (https://blog.golang.org/errors-are-values)? try würde wahrscheinlich die Verwendung solcher Praktiken reduzieren und in diesem Sinne sind sie die Hauptziele für den Ersatz (wahrscheinlich einer der wenigen Anwendungsfälle, die mich wirklich faszinieren). Auch dies scheint also sinnlos zu sein, die vorhandene Quelle ohne viel mehr Sorgfalt zu kratzen.

Vielen Dank @ubikenobi , @freeformz und @kingishb für das Sammeln Ihrer Daten, sehr geschätzt! Abgesehen davon, wenn Sie tryhard mit der Option -err="" ausführen, wird if auch versuchen, mit Code zu arbeiten, bei dem die Fehlervariable anders als err heißt (z. B. e ). Dies kann je nach Codebasis zu einigen weiteren Fällen führen (aber möglicherweise auch die Wahrscheinlichkeit von Fehlalarmen erhöhen).

@griesemer falls du nach mehr Datenpunkten suchst. Ich habe tryhard gegen zwei unserer Microservices laufen lassen, mit diesen Ergebnissen:

cloc v 1.82 / tryhard
13280 Go-Codezeilen / 148 zum Ausprobieren identifiziert (1 %)

Ein weiterer Dienst:
9768 Go-Codezeilen / 50 zum Ausprobieren identifiziert (0,5 %)

Anschließend untersuchte tryhard eine breitere Palette verschiedener Mikrodienste:

314343 Go-Codezeilen / 1563 zum Ausprobieren identifiziert (0,5 %)

Mache eine schnelle Inspektion. Die Pakettypen, die try optimieren könnten, sind typischerweise Adapter/Service-Wrapper, die den (GRPC)-Fehler, der vom verpackten Service zurückgegeben wird, transparent zurückgeben.

Hoffe das hilft.

Es ist eine absolut schlechte Idee.

  • Wann erscheint err var für defer ? Was ist mit "explizit besser als implizit"?
  • Wir verwenden eine einfache Regel: Sie sollten schnell genau eine Stelle finden, an der Sie einen Fehler zurückgegeben haben. Jeder Fehler wird mit Kontext umschlossen, um zu verstehen, was und wo schief geht. defer erzeugt eine Menge hässlichen und schwer verständlichen Code.
  • @davecheney hat einen großartigen Beitrag über Fehler geschrieben und der Vorschlag ist völlig gegen alles in diesem Beitrag.
  • Wenn Sie schließlich os.Exit verwenden, werden Ihre Fehler deaktiviert.

Ich habe gerade tryhard für ein Paket (mit Anbieter) ausgeführt und es hat 2478 gemeldet, wobei die Codeanzahl von 873934 auf 851178 ist, aber ich bin mir nicht sicher wie ich das interpretieren soll, weil ich nicht weiß, wie viel davon auf Over-Wrapping zurückzuführen ist (wobei die stdlib keine Unterstützung für das Stack-Trace-Fehler-Wrapping bietet) oder wie viel von diesem Code überhaupt von der Fehlerbehandlung handelt.

Was ich jedoch weiß, ist, dass ich allein diese Woche peinlich viel Zeit durch Copy-Pasta wie if err != nil { return nil } und Fehler, die wie error: cannot process ....file: cannot parse ...file: cannot open ...file aussehen, verschwendet habe.

\ Ich würde nicht zu viel Gewicht auf die Anzahl der Stimmen legen, es sei denn, Sie denken, dass es nur ~3000 Go-Entwickler da draußen gibt. Die hohe Stimmenzahl für den anderen Nichtvorschlag ist einfach darauf zurückzuführen, dass das Thema es an die Spitze von HN und Reddit geschafft hat – die Go-Community ist nicht gerade für ihren Mangel an Dogmen und/oder Nein-Sagen bekannt -Man sollte sich über die Stimmenzahlen wundern.

Ich würde auch die Versuche, an Behörden zu appellieren, nicht allzu ernst nehmen, da dieselben Behörden dafür bekannt sind, neue Ideen und Vorschläge abzulehnen, selbst wenn auf ihre eigene Unkenntnis und/oder Missverständnisse hingewiesen wird.
\

Wir haben tryhard -err="" auf unserem größten Dienst (±163.000 Codezeilen einschließlich Tests) ausgeführt – er hat 566 Vorkommen gefunden. Ich vermute, dass es in der Praxis noch praktischer wäre, da ein Teil des Codes mit Blick auf if err != nil geschrieben wurde, also wurde er darum herum entworfen (Rob Pikes Artikel „Fehler sind Werte“ zur Vermeidung von Wiederholungen fällt mir ein).

@griesemer Ich habe dem Kern eine neue Datei hinzugefügt. Es wurde mit -err="" generiert. Ich habe es stichprobenartig überprüft und es gibt ein paar Änderungen. Ich habe heute Morgen auch tryhard aktualisiert, also wurde auch die neuere Version verwendet.

@griesemer Ich denke, tryhard wäre nützlicher, wenn es zusammenpassen könnte:

a) die Anzahl der Aufrufseiten, die einen Fehler ergeben
b) die Anzahl der if err != nil [&& ...] -Handler für einzelne Anweisungen (Kandidaten für on err #32611)
c) die Anzahl derjenigen, die etwas zurückgeben (Kandidaten für defer #32676)
d) die Anzahl derjenigen, die err zurückgeben (Kandidaten für try() )
e) die Anzahl derjenigen, die in exportierten Funktionen von Nicht-Hauptpaketen enthalten sind (wahrscheinlich falsch positiv)

Der Vergleich des gesamten LoC mit Instanzen von return err fehlt irgendwie der Kontext, IMO.

@networkimprov Einverstanden - ähnliche Vorschläge wurden bereits angesprochen. Ich werde versuchen, in den nächsten Tagen etwas Zeit zu finden, um dies zu verbessern.

Hier sind die Statistiken zum Ausführen von Tryhard über unsere interne Codebasis (nur unser Code, keine Abhängigkeiten):

Vor:

  • 882 .go-Dateien
  • 352434 Ort
  • 329909 nicht leerer Ort

Nach Versuch:

  • 2701 Ersetzungen (durchschnittlich 3,1 Ersetzungen / Datei)
  • 345364 Ort (-2,0%)
  • 322838 nicht leere Lok (-2,1%)

Bearbeiten: Jetzt, da @griesemer tryhard aktualisiert hat, um zusammenfassende Statistiken aufzunehmen, hier sind ein paar mehr:

  • 39,2 % der if -Abrechnungen sind if <err> != nil
  • 69,6 % davon sind try -Kandidaten

Beim Durchsehen der Ersetzungen, die tryhard gefunden hat, gibt es sicherlich Codearten, bei denen die Verwendung von try sehr verbreitet wäre, und andere Arten, bei denen sie selten verwendet wird.

Ich habe auch einige Orte bemerkt, die Tryhard nicht transformieren konnte, aber sehr von Tryhard profitieren würde. Hier ist zum Beispiel ein Code, den wir zum Decodieren von Nachrichten gemäß einem einfachen Drahtprotokoll haben (zur Vereinfachung/Klarheit bearbeitet):

func (req *Request) Decode(r Reader) error {
    typ, err := readByte(r)
    if err != nil {
        return err
    }
    req.Type = typ
    req.Body, err = readString(r)
    if err != nil {
        return unexpected(err)
    }

    req.ID, err = readID(r)
    if err != nil {
        return unexpected(err)
    }
    n, err := binary.ReadUvarint(r)
    if err != nil {
        return unexpected(err)
    }
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        if err != nil {
            return unexpected(err)
        }
    }
    return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
    if err == io.EOF {
        return io.ErrUnexpectedEOF
    }
    return err
}

Ohne try haben wir einfach unexpected an den Rückgabepunkten geschrieben, wo es benötigt wird, da es keine große Verbesserung gibt, wenn es an einem Ort gehandhabt wird. Mit try können wir jedoch die Fehlertransformation unexpected mit einem Defer anwenden und dann den Code drastisch kürzen, wodurch er klarer und einfacher zu überfliegen ist:

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@cespare Fantastischer Bericht!

Das vollständig reduzierte Snippet ist im Allgemeinen besser, aber die Klammern sind noch schlechter, als ich erwartet hatte, und das try innerhalb der Schleife ist so schlecht, wie ich erwartet hatte.

Ein Schlüsselwort ist viel besser lesbar und es ist ein bisschen surreal, dass dies ein Punkt ist, an dem sich viele andere unterscheiden. Das Folgende ist lesbar und macht mir keine Sorgen über Feinheiten, da nur ein Wert zurückgegeben wird (obwohl es immer noch in längeren Funktionen und/oder solchen mit viel Verschachtelung auftreten könnte):

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = wrapEOF(err) }()

    req.Type = try readByte(r)
    req.Body = try readString(r)
    req.ID = try readID(r)

    n := try binary.ReadUvarint(r)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        try err
    }
    return nil
}

* Um fair zu sein, Code-Hervorhebung würde viel helfen, aber das scheint nur ein billiger Lippenstift zu sein.

Verstehen Sie, dass Sie den größten Vorteil im Falle von wirklich schlechtem Code erhalten?

Wenn Sie unexpected() verwenden oder den Fehler unverändert zurückgeben, wissen Sie nichts über Ihren Code und Ihre Anwendung.

try kann Ihnen nicht helfen, besseren Code zu schreiben, kann aber noch mehr schlechten Code produzieren.

@cespare Ein Decoder kann auch eine Struktur mit einem darin enthaltenen Fehlertyp sein, wobei die Methoden vor jeder Operation nach err == nil suchen und ein boolesches OK zurückgeben.

Da dies der Prozess ist, den wir für Codecs verwenden, ist try absolut nutzlos, da man für diesen speziellen Fall leicht eine nicht magische, kürzere und prägnantere Redewendung für die Behandlung von Fehlern erstellen kann.

@makhov Mit "wirklich schlechtem Code" meinen Sie vermutlich Code, der keine Fehler umschließt.

Wenn ja, dann können Sie Code nehmen, der so aussieht:

a, b, c, err := someFn()
if err != nil {
  return ..., errors.Wrap(err, ...)
}

Und verwandeln Sie es in semantisch identischen[1] Code, der so aussieht:

a, b, c, err := someFn()
try(errors.Wrap(err, ...))

Der Vorschlag sagt nicht, dass Sie defer für das Umschließen von Fehlern verwenden müssen , sondern erklärt nur, warum das Schlüsselwort handle der vorherigen Iteration des Vorschlags nicht erforderlich ist, da es in Bezug auf defer ohne Sprachänderungen implementiert werden kann.

(Ihr anderer Kommentar scheint auch auf Beispielen oder Pseudocode im Vorschlag zu basieren, im Gegensatz zum Kern dessen, was vorgeschlagen wird.)

Ich habe tryhard auf meiner Codebasis mit 54K LOC ausgeführt, es wurden 1116 Instanzen gefunden.
Ich habe den Unterschied gesehen, und ich muss sagen, dass ich so sehr wenig Konstrukt habe, das sehr von try profitieren würde, weil fast meine gesamte Verwendung des Konstrukttyps if err != nil ein einfacher einstufiger Block ist, der nur das zurückgibt Fehler mit hinzugefügtem Kontext. Ich glaube, ich habe nur ein paar Fälle gefunden, in denen try tatsächlich das Konstrukt des Codes ändern würde.

Mit anderen Worten, ich nehme an, dass try in seiner aktuellen Form mir Folgendes gibt:

  • weniger Tippen (eine Reduzierung von satten ~30 Zeichen pro Vorkommen, gekennzeichnet durch die „**“ unten)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

während es diese Probleme für mich einführt:

  • Noch eine andere Möglichkeit, mit Fehlern umzugehen
  • fehlender visueller Hinweis für die Aufteilung des Ausführungspfads

Wie ich früher in diesem Thread geschrieben habe, kann ich mit try leben, aber nachdem ich es an meinem Code ausprobiert habe, denke ich, dass ich es persönlich lieber nicht in die Sprache einführen lassen würde. meine $.02

nutzloses Feature, es spart Tippen, aber keine große Sache.
Ich wähle lieber den alten Weg.
Schreiben Sie mehr Fehlerbehandler, machen Sie das Programmieren einfach, um Fehler zu beheben.

Nur ein paar Gedanken...

Diese Redewendung ist beim Go nützlich, aber es ist genau das: eine Redewendung, die man muss
Neuankömmlingen beibringen. Ein neuer Go-Programmierer muss das lernen, sonst werden sie
könnte sogar versucht sein, die "versteckte" Fehlerbehandlung umzugestalten. Auch der
Code ist mit dieser Redewendung nicht kürzer (ganz im Gegenteil), es sei denn, Sie vergessen es
die Methoden zu zählen.

Stellen wir uns nun vor, dass try implementiert ist, wie nützlich diese Redewendung sein wird
dieser Anwendungsfall? Angesichts:

  • Try hält die Implementierung näher, anstatt sie über Methoden zu verteilen.
  • Programmierer werden viel häufiger Code mit try lesen und schreiben
    spezifische Redewendung (die selten verwendet wird, außer für alle spezifischen Aufgaben). EIN
    Eine häufiger verwendete Redewendung wird natürlicher und lesbarer, es sei denn, es gibt eine klare Aussage
    Nachteil, was hier eindeutig nicht der Fall ist, wenn wir beide mit an vergleichen
    aufgeschlossen.

Vielleicht wird diese Redewendung also von try als überholt angesehen.

Am 2. Juli 2019, 18:06 Uhr, als [email protected] escreveu:

@cespare https://github.com/cespare Ein Decoder kann auch eine Struktur mit sein
einen Fehlertyp darin, wobei die Methoden zuvor auf err == nil prüfen
jede Operation und Rückgabe eines booleschen ok.

Da dies der Prozess ist, den wir für Codecs verwenden, ist try absolut nutzlos
weil man leicht eine nicht magische, kürzere und prägnantere Redewendung machen kann
für die Behandlung von Fehlern für diesen speziellen Fall.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA#issuecomment-548,438,438
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.

Ausführlichkeit bei der Fehlerbehandlung ist meiner Meinung nach eine gute Sache. Mit anderen Worten, ich sehe keinen starken Anwendungsfall für try.

Ich bin offen für diese Idee, aber ich denke, dass sie einen Mechanismus enthalten sollte, um festzustellen, wo die Ausführungsteilung stattgefunden hat. Xerror/Is wäre in einigen Fällen in Ordnung (zB wenn der Fehler ein ErrNotExists ist, können Sie daraus schließen, dass er bei einem Open passiert ist), aber für andere - einschließlich Legacy-Fehler in Bibliotheken - gibt es keinen Ersatz.

Könnte eine eingebaute ähnliche Wiederherstellung möglicherweise enthalten sein, um Kontextinformationen darüber bereitzustellen, wo sich der Kontrollfluss geändert hat? Möglicherweise, um es billig zu halten, wird anstelle von try() eine separate Funktion verwendet.

Oder vielleicht ein debug.Try mit der gleichen Syntax wie try(), aber mit den hinzugefügten Debug-Informationen? Auf diese Weise könnte try() bei Code mit alten Fehlern genauso nützlich sein, ohne dass Sie gezwungen sind, auf die alte Fehlerbehandlung zurückzugreifen.

Die Alternative wäre, dass try() Kontext umschließt und hinzufügt, aber in den meisten Fällen würde dies die Leistung umsonst reduzieren, daher der Vorschlag zusätzlicher Funktionen.

Bearbeiten: Nachdem ich dies geschrieben hatte, fiel mir ein, dass der Compiler bestimmen könnte, welche Variante von try() verwendet werden soll, basierend darauf, ob irgendwelche Defer-Anweisungen diese kontextbereitstellende Funktion ähnlich wie "recover" verwenden. Ich bin mir jedoch nicht sicher, wie komplex dies ist

@lestrrat Ich würde meine Meinung in diesem Kommentar nicht sagen, aber wenn es eine Möglichkeit gibt, Ihnen zu erklären, wie "try" sich gut auf uns auswirkt, wäre es, dass zwei oder mehr Token in die if-Anweisung geschrieben werden können. Wenn Sie also 200 Bedingungen in eine if-Anweisung schreiben, können Sie viele Zeilen reduzieren.

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

@mattn das ist aber das Ding, _theoretisch_ hast du absolut recht. Ich bin sicher, uns fallen Fälle ein, in denen try wunderbar passen würde.

Ich habe nur Daten geliefert, die im wirklichen Leben zumindest _ich_ fast kein Vorkommen solcher Konstrukte gefunden haben, die von der Übersetzung profitieren würden, um sie in _meinem Code_ auszuprobieren.

Es ist möglich, dass ich Code anders schreibe als der Rest der Welt, aber ich dachte einfach, dass es sich für jemanden lohnt, basierend auf der PoC-Übersetzung einzustimmen, dass einige von uns nicht wirklich viel von der Einführung von try in die Sprache.

Abgesehen davon würde ich Ihren Stil immer noch nicht in meinem Code verwenden. Ich würde es schreiben als

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

also würde ich immer noch ungefähr die gleiche Menge an Tipparbeit pro Instanz dieser n1/n2/....n(n)s sparen

Warum überhaupt ein Schlüsselwort (oder eine Funktion) haben?

Erwartet der aufrufende Kontext n+1 Werte, dann ist alles wie zuvor.

Wenn der aufrufende Kontext n Werte erwartet, tritt das Try-Verhalten ein.

(Dies ist besonders nützlich im Fall n = 1, woher all das schreckliche Durcheinander kommt.)

Meine ide hebt bereits ignorierte Rückgabewerte hervor; Es wäre trivial, dafür visuelle Hinweise anzubieten, falls erforderlich.

@balasanjay Ja, Verpackungsfehler sind der Fall. Aber wir haben auch Protokollierung, unterschiedliche Reaktionen auf verschiedene Fehler (was sollen wir mit Fehlervariablen tun, zB sql.NoRows ?), lesbaren Code und so weiter. Wir schreiben defer f.Close() unmittelbar nach dem Öffnen einer Datei, um es den Lesern klar zu machen. Aus dem gleichen Grund prüfen wir Fehler sofort.

Vor allem verstößt dieser Vorschlag gegen die Regel „ Fehler sind Werte “. So ist Go konzipiert. Und dieser Vorschlag geht direkt gegen die Regel.

try(errors.Wrap(err, ...)) ist ein weiterer schrecklicher Code, weil er sowohl diesem Vorschlag als auch dem aktuellen Go-Design widerspricht.

Ich neige dazu, @lestrrat zuzustimmen
Wie üblich sind foo() und bar() eigentlich:
SomeFunctionWithGoodName(Parm1, Parms2)

dann wäre die vorgeschlagene @mattn- Syntax tatsächlich:

if  try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {


} 

Die Lesbarkeit wird normalerweise ein Durcheinander sein.

Betrachten Sie einen Rückgabewert:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
wird häufiger verwendet, als nur mit einer Konstante wie 1 oder 2 zu vergleichen, und es wird nicht schlimmer, sondern erfordert eine Doppelzuweisungsfunktion:

if  a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {


} 

Wie für alle Anwendungsfälle ("wie sehr hat mir tryhard geholfen"):

  1. Ich denke, Sie würden einen großen Unterschied zwischen Bibliotheken und ausführbaren Dateien sehen. Es wäre interessant, von anderen zu sehen, ob sie diesen Unterschied ebenfalls erhalten
  2. Mein Vorschlag ist, nicht die %save in Zeilen im Code zu vergleichen, sondern die Anzahl der Fehler im Code mit der umgestalteten Anzahl.
    (Meine Meinung dazu war
    $find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
    )

@makhov

dieser Vorschlag verstößt gegen die Regel „Fehler sind Werte“

Nicht wirklich. Fehler sind in diesem Vorschlag immer noch Werte. try() vereinfacht nur den Kontrollfluss, indem es eine Abkürzung für if err != nil { return ...,err } ist. Der Typ error ist schon irgendwie "besonders", da er ein eingebauter Schnittstellentyp ist. Dieser Vorschlag fügt lediglich eine integrierte Funktion hinzu, die den Typ error ergänzt. Hier gibt es nichts Außergewöhnliches.

@ngrilly Vereinfachen? Wie?

func (req *Request) Decode(r Reader) error {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

Wie sollte ich verstehen, dass der Fehler innerhalb der Schleife zurückgegeben wurde? Warum ist es err var zugewiesen, nicht foo ?
Ist es einfacher, es im Hinterkopf zu behalten und es nicht im Code zu behalten?

@dave

die Klammern sind noch schlimmer, als ich erwartet hatte [...] Ein Schlüsselwort ist viel besser lesbar und es ist ein bisschen surreal, dass dies ein Punkt ist, an dem sich viele andere unterscheiden.

Die Wahl zwischen einem Schlüsselwort und einer eingebauten Funktion ist hauptsächlich eine ästhetische und syntaktische Frage. Ich verstehe ehrlich gesagt nicht, warum das für deine Augen so wichtig ist.

PS: Die eingebaute Funktion hat den Vorteil, dass sie abwärtskompatibel ist, in Zukunft mit anderen Parametern erweiterbar ist und die Probleme mit der Operatorpriorität vermeidet. Das Schlüsselwort hat den Vorteil, dass es ein Schlüsselwort ist, und das Signalisieren try ist "besonders".

@makhov

Vereinfachen?

OK. Das richtige Wort ist „kürzen“.

try() verkürzt unseren Code, indem es das Muster if err != nil { return ..., err } durch einen Aufruf der eingebauten Funktion try() ersetzt.

Es ist genau so, als würden Sie ein wiederkehrendes Muster in Ihrem Code identifizieren und es in einer neuen Funktion extrahieren.

Wir haben bereits eingebaute Funktionen wie append(), die wir ersetzen könnten, indem wir den Code jedes Mal selbst "in extenso" schreiben, wenn wir etwas an ein Slice anhängen müssen. Aber weil wir es die ganze Zeit machen, wurde es in die Sprache integriert. try() ist nicht anders.

Wie sollte ich verstehen, dass der Fehler innerhalb der Schleife zurückgegeben wurde?

Das try() in der Schleife verhält sich außerhalb der Schleife genauso wie das try() im Rest der Funktion. Wenn readID() einen Fehler zurückgibt, dann gibt die Funktion den Fehler zurück (nachdem sie if dekoriert hat).

Warum ist es err var zugewiesen, nicht foo?

Ich sehe keine Variable foo in Ihrem Codebeispiel ...

@makhov Ich denke, dass das Snippet unvollständig ist, da der zurückgegebene Fehler nicht benannt ist (ich habe den Vorschlag schnell noch einmal gelesen, konnte aber nicht sehen, ob der Variablenname err der Standardname ist, wenn keiner festgelegt ist).

Zurückgegebene Parameter umbenennen zu müssen, ist einer der Punkte, die Leute, die diesen Vorschlag ablehnen, nicht mögen.

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@pierrec vielleicht könnten wir eine Funktion wie recover() haben, um den Fehler abzurufen, wenn er nicht im benannten Parameter ist?
defer func() {err = unexpected(tryError())}

@makhov Sie können es expliziter machen:

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
    req.Body, err := readString(r)
        try(err)
    req.ID, err := readID(r)
        try(err)

    n, err := binary.ReadUvarint(r)
        try(err)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err := readID(r)
                try(err)
    }
    return nil
}

@pierrec Ok, ändern wir es:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
    defer func() { err = unexpected(???) }()

    req.Type = try(readByte(r))
    …
}

@reusee Und warum ist es besser als das?

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

In welchem ​​Moment haben wir alle entschieden, dass Kürze besser ist als Lesbarkeit?

@flibustenet Vielen Dank für das Verständnis des Problems. Es sieht viel besser aus, aber ich bin mir immer noch nicht sicher, ob wir für diese kleine "Verbesserung" eine kaputte Abwärtskompatibilität brauchen. Es ist sehr ärgerlich, wenn ich eine Anwendung habe, die nicht mehr auf der neuen Version von Go aufbaut:

package main

func main() {
    // ...
   try("a", "b")
    // ...
}

func try(a, b string) {
    // ...
}

@makhov Ich stimme zu, dass dies geklärt werden muss: Tritt der Compilerfehler auf, wenn er die Variable nicht herausfinden kann? Ich dachte, es würde.
Vielleicht muss der Vorschlag diesen Punkt klarstellen? Oder habe ich das im Dokument übersehen?

@flibustenet ja, das ist eine Möglichkeit, try() zu verwenden, aber es scheint mir, dass es keine idiomatische Art ist, try zu verwenden.

@cespare Aus dem, was Sie geschrieben haben, scheint es, dass die Änderung der Rückgabewerte in defer eine Funktion von try ist, aber Sie können dies bereits tun.

https://play.golang.com/p/ZMauFmt9ezJ

(Entschuldigung, wenn ich deine Aussage falsch interpretiert habe)

@jan-g Zu https://github.com/golang/go/issues/32437#issuecomment -507961463: Die Idee, Fehler unsichtbar zu behandeln, ist mehrfach aufgetaucht. Das Problem bei einem solchen impliziten Ansatz besteht darin, dass das Hinzufügen einer Fehlerrückgabe zu einer aufgerufenen Funktion dazu führen kann, dass sich die aufrufende Funktion stillschweigend und unsichtbar anders verhält. Wir wollen unbedingt explizit sein, wenn Fehler überprüft werden. Ein impliziter Ansatz widerspricht auch dem allgemeinen Prinzip in Go, dass alles explizit ist.

@griesemer

Ich habe tryhand in einem meiner Projekte ausprobiert (https://github.com/komuw/meli) und es hat keine Änderung gebracht.

gobin github.com/griesemer/tryhard
     Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard

„Bash
~/go/bin/tryhard -err "" -r
0

most of my err handling looks like;
```Go
import "github.com/pkg/errors"

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

@komuw Stellen Sie zunächst sicher, dass Sie einen Dateinamen oder ein Verzeichnisargument für tryhard angeben, wie in

tryhard -err="" -r .  // <<< note the dot
tryhard -err="" -r filename

Außerdem wird Code, wie Sie ihn in Ihrem Kommentar haben, nicht umgeschrieben, da er eine spezifische Fehlerbehandlung im if -Block durchführt. Bitte lesen Sie die Dokumentation von tryhard , wann es gilt. Danke.

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

Dies ist ein etwas interessantes Beispiel. Meine erste Reaktion, als ich es mir ansah, war zu fragen, ob dies zu stotternden Fehlerzeichenfolgen führen würde wie:

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

Die Antwort ist, dass dies nicht der Fall ist, da die Funktion VolumeCreate (aus einem anderen Repo) lautet:

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

Mit anderen Worten, die zusätzliche Dekoration des Fehlers ist nützlich, da die zugrunde liegende Funktion ihren Fehler nicht dekoriert hat. Diese zugrunde liegende Funktion kann mit try leicht vereinfacht werden.

Vielleicht sollte die VolumeCreate -Funktion wirklich ihre Fehler schmücken. In diesem Fall ist mir jedoch nicht klar, dass die CreateDockerVolume -Funktion zusätzliche Dekoration hinzufügen sollte, da sie keine neuen Informationen bereitzustellen hat.

@neild
Selbst wenn VolumeCreate die Fehler schmücken würde, würden wir immer noch CreateDockerVolume brauchen, um seine Dekoration hinzuzufügen, da VolumeCreate von verschiedenen anderen Funktionen aufgerufen werden kann, und wenn etwas fehlschlägt (und hoffentlich eingeloggt) möchten Sie wissen, was fehlgeschlagen ist - in diesem Fall CreateDockerVolume ,
Dennoch ist Considering VolumeCreate Teil der APIclient-Schnittstelle.

Dasselbe gilt für andere Bibliotheken - os.Open kann den Dateinamen, die Fehlerursache usw. gut schmücken, aber
func ReadConfigFile(...
func WriteDataFile(...
usw. - Aufruf os.Open sind die tatsächlichen fehlerhaften Teile, die Sie sehen möchten, um Ihre Fehler zu protokollieren, zu verfolgen und zu behandeln - insbesondere, aber nicht nur in der Produktionsumgebung.

@neild danke.

Ich will diesen Thread nicht entgleisen lassen, aber...

Vielleicht sollte die VolumeCreate-Funktion wirklich ihre Fehler schmücken.
In diesem Fall ist mir jedoch nicht klar, dass die
CreateDockerVolume-Funktion
sollte zusätzliche Dekoration hinzufügen,

Das Problem ist, dass ich es als Autor der CreateDockerVolume -Funktion nicht kann
wissen, ob der Autor von VolumeCreate seine Fehler so dekoriert hatte i
brauche meine nicht zu dekorieren.
Und selbst wenn ich wüsste, dass sie es getan hätten, könnten sie sich entscheiden, ihre Dekoration zu entfernen
Funktion in einer späteren Version. Und da diese Änderung nicht die API ist, die sie ändert
würde es als patch/minor version veröffentlichen und jetzt meine funktion was war
abhängig von ihrer Funktion mit dekorierten Fehlern haben nicht alle
Infos die ich brauche.
Also im Allgemeinen finde ich mich selbst beim Dekorieren/Verpacken wieder, selbst wenn ich die Bibliothek bin
Aufruf hat bereits gewickelt.

Mir kam ein Gedanke, als ich mit einem Kollegen über try sprach. Vielleicht sollte try nur für die Standardbibliothek in 1.14 aktiviert werden. @crawshaw und @jimmyfrasche machten beide eine kurze Tour durch einige Fälle und gaben einige Perspektiven, aber tatsächlich wäre es wertvoll, den Code der Standardbibliothek so weit wie möglich mit try neu zu schreiben.

Das gibt dem Go-Team Zeit, ein nicht triviales Projekt damit neu zu schreiben, und die Community kann einen Erfahrungsbericht darüber haben, wie es funktioniert. Wir würden wissen, wie oft es verwendet wird, wie oft es mit einem defer gepaart werden muss, ob es die Lesbarkeit des Codes verändert, wie nützlich tryhard ist usw.

Es ist ein bisschen gegen den Geist der Standardbibliothek, die es erlaubt, etwas zu verwenden, was normaler Go-Code nicht kann, aber es gibt uns einen Spielplatz, um zu sehen, wie sich try auf eine vorhandene Codebasis auswirkt.

Entschuldigung, wenn jemand anderes schon daran gedacht hat; Ich ging die verschiedenen Diskussionen durch und sah keinen ähnlichen Vorschlag.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gibt Ihnen eine ziemlich gute Vorstellung davon, wie das aussehen könnte.

Und ich habe vergessen zu sagen: Ich habe an Ihrer Umfrage teilgenommen und für eine bessere Fehlerbehandlung gestimmt, nicht für diese.

Ich meinte, ich würde gerne eine strengere Verarbeitung von unmöglich zu vergessenden Fehlern sehen.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gibt Ihnen eine ziemlich gute Vorstellung davon, wie das aussehen könnte.

Um zusammenzufassen:

  1. 1 Zeile ersetzt allgemein 4 Zeilen (2 Zeilen für diejenigen, die if ... { return err } verwenden)
  2. Die Auswertung der zurückgegebenen Ergebnisse kann ausoptimiert werden - allerdings nur auf dem Fail-Pfad.

Etwa 6.000 Ersetzungen insgesamt, was nur eine kosmetische Änderung zu sein scheint: vorhandene Fehler nicht aufdecken, möglicherweise keine neuen einführen (korrigieren Sie mich, wenn ich mich bei beiden irre.)

Würde ich als Betreuer so etwas mit meinem eigenen Code machen? Es sei denn, ich schreibe das Ersatzwerkzeug selbst. Das macht es für das golang/go -Repository in Ordnung.

PS Ein interessanter Haftungsausschluss in CL:

... Some transformations may be incorrect due to the limitations of the tool (see https://github.com/griesemer/tryhard)...

Wie wäre es beispielsweise xerrors , den ersten Schritt zu tun, um es als Paket eines Drittanbieters zu verwenden?

Versuchen Sie beispielsweise, das folgende Paket zu verwenden.

https://github.com/junpayment/gotry

  • Es kann für Ihren Anwendungsfall kurz sein, weil ich es gemacht habe.

Ich denke jedoch, dass Try an sich eine großartige Idee ist, daher denke ich, dass es auch einen Ansatz gibt, der es tatsächlich mit weniger Einfluss verwendet.

===

Abgesehen davon gibt es zwei Dinge, über die ich besorgt bin.

1. Es gibt die Meinung, dass die Zeile weggelassen werden kann, aber es scheint, dass die Klausel defer (oder handler) nicht berücksichtigt wird.

Zum Beispiel, wenn es um die Fehlerbehandlung im Detail geht.

foo, err: = Foo ()
if err! = nil {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}

Wenn Sie dies einfach durch try ersetzen, sieht es wie folgt aus.

handler: = func (err error) {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}
foo: = try (Foo (), handler)

2. Möglicherweise gibt es andere fehlerhafte Pakete, die versehentlich die Fehlerschnittstelle implementiert haben.

type Bad struct {}
func (bad * Bad) Error () {
  return "i really do not intend to be an error"
}

@junpayment Vielen Dank für Ihr gotry -Paket - ich denke, das ist eine Möglichkeit, ein bisschen ein Gefühl für try zu bekommen, aber es wird ein bisschen lästig sein, alle Try eingeben zu müssen interface{} in der tatsächlichen Verwendung.

Zu deinen beiden Fragen:
1) Ich bin mir nicht sicher, wohin du damit gehst. Schlagen Sie vor, dass try einen Handler wie in Ihrem Beispiel akzeptieren sollte? (und wie wir es in einer früheren internen Version von try hatten?)
2) Ich mache mir keine allzu großen Sorgen über Funktionen, die versehentlich die Fehlerschnittstelle implementieren. Dieses Problem ist nicht neu und scheint, soweit wir wissen, keine ernsthaften Probleme verursacht zu haben.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gibt Ihnen eine ziemlich gute Vorstellung davon, wie das aussehen könnte.

Danke, dass du diese Übung gemacht hast. Das bestätigt mir aber meine Vermutung, der Go-Quellcode selbst hat viele Stellen, an denen try() sinnvoll wäre, weil der Fehler einfach weitergegeben wird. Wie ich jedoch aus den Experimenten mit tryhard kann, die andere und ich oben eingereicht haben, wäre try() für viele andere Codebasen nicht sehr nützlich, da in Anwendungscode Fehler dazu neigen, tatsächlich behandelt zu werden, nicht gerade weitergegeben.

Ich denke, das ist etwas, was die Go-Designer im Hinterkopf behalten sollten, der Go-Compiler und die Laufzeit sind etwas "einzigartiger" Go-Code, anders als der Go-Anwendungscode. Daher denke ich, dass try() verbessert werden sollte, um auch in anderen Fällen nützlich zu sein, in denen der Fehler tatsächlich behandelt werden muss und in denen eine Fehlerbehandlung mit einer defer-Anweisung nicht wirklich wünschenswert ist.

@griesemer

Es wird ein bisschen lästig sein, alle Try-Ergebnisse aus einer tatsächlich verwendeten Schnittstelle{} eingeben und bestätigen zu müssen.

Du hast recht. Diese Methode erfordert, dass der Aufrufer den Typ umwandelt.

Ich bin mir nicht sicher, wohin du damit gehst. Schlagen Sie vor, dass try einen Handler wie in Ihrem Beispiel akzeptieren sollte? (und wie wir es in einer früheren internen Version von try hatten?)

Ich machte einen Fehler. Hätte mit defer statt mit handler erklärt werden sollen. Es tut mir Leid.

Was ich sagen wollte, ist, dass es einen Fall gibt, in dem es nicht zur Codemenge beiträgt, da der Fehlerbehandlungsprozess weggelassen wird, der sowieso in der Zurückstellung beschrieben werden muss.

Es wird erwartet, dass die Auswirkungen ausgeprägter sind, wenn Sie Fehler im Detail behandeln möchten.

Anstatt also die Anzahl der Codezeilen zu reduzieren, können wir den Vorschlag verstehen, der die Fehlerbehandlungsstellen organisiert.

Ich mache mir keine allzu großen Sorgen über Funktionen, die versehentlich die Fehlerschnittstelle implementieren. Dieses Problem ist nicht neu und scheint, soweit wir wissen, keine ernsthaften Probleme verursacht zu haben.

Genau das ist selten der Fall.

@beoran Ich habe eine erste Analyse des Go Corpus (https://github.com/rsc/corpus) durchgeführt. Ich glaube, dass tryhard in seinem derzeitigen Zustand 41,7 % aller err != nil -Überprüfungen im Korpus eliminieren könnte. Wenn ich das Muster "_test.go" ausschließe, steigt diese Zahl auf 51,1 % ( tryhard arbeitet nur mit Funktionen, die Fehler zurückgeben, und es neigt dazu, nicht viele davon in Tests zu finden). Vorsicht, nehmen Sie diese Zahlen mit einem Körnchen Salz, ich habe den Nenner (dh die Anzahl der Stellen im Code, an denen wir err != nil Prüfungen durchführen) erhalten, indem ich eine gehackte Version von tryhard verwendet habe, und zwar im Idealfall Wir würden warten, bis tryhard diese Statistiken selbst gemeldet hat.

Wenn tryhard typbewusst werden würde, könnte es theoretisch Transformationen wie diese durchführen:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

Dies nutzt das Verhalten von errors.Wrap aus, nil zurückzugeben, wenn das übergebene Fehlerargument nil ist. (github.com/pkg/errors ist in dieser Hinsicht ebenfalls nicht einzigartig, die interne Bibliothek, die ich für das Umschließen von Fehlern verwende, behält auch nil -Fehler bei und würde auch mit diesem Muster funktionieren, wie dies bei den meisten Fehlerbehandlungsbibliotheken der Fall wäre post- try , nehme ich an). Die neue Generation von Support-Bibliotheken würde diese Vermehrungshelfer wahrscheinlich auch etwas anders benennen.

Angesichts der Tatsache, dass dies für 50 % der Nicht-Test-Checks von err != nil gelten würde, scheint es vor einer Bibliotheksentwicklung zur Unterstützung des Musters nicht so, als ob der Go-Compiler und die Laufzeit einzigartig sind, wie Sie vermuten .

Zum Beispiel mit CreateDockerVolume https://github.com/golang/go/issues/32437#issuecomment -508199875
Ich habe genau die gleiche Art der Verwendung gefunden. In lib umschließe ich Fehler mit Kontext bei jedem Fehler, bei Verwendung der lib möchte ich try verwenden und Kontext in defer für die gesamte Funktion hinzufügen.

Ich habe versucht, dies nachzuahmen, indem ich am Anfang eine Fehlerbehandlungsfunktion hinzufügte, es funktioniert gut:

func MyLib() error {
    return errors.New("Error from my lib")
}
func MyOtherLib() error {
    return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
    eh := func(err error) error {
        return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
    }

    err := MyLib()
    if err != nil {
        return eh(err)
    }

    err = MyOtherLib()
    if err != nil {
        return eh(err)
    }

    return nil
}

Das wird mit try+defer gut und idiomatisch aussehen

func Caller(a, b int) (err error) {
    defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

    try(MyLib())
    try(MyOtherLib())

    return nil
}

@griesemer

Das Designdokument enthält derzeit die folgenden Aussagen:

Wenn die einschließende Funktion andere benannte Ergebnisparameter deklariert, behalten diese Ergebnisparameter ihren Wert. Wenn die Funktion andere unbenannte Ergebnisparameter deklariert, nehmen sie ihre entsprechenden Nullwerte an (was dasselbe ist wie das Beibehalten des Werts, den sie bereits haben).

Dies impliziert, dass dieses Programm 1 statt 0 ausgeben würde: https://play.golang.org/p/KenN56iNVg7.

Wie mir auf Twitter mitgeteilt wurde, verhält sich try dadurch wie eine nackte Rückgabe, bei der die zurückgegebenen Werte implizit sind; Um herauszufinden, welche tatsächlichen Werte zurückgegeben werden, muss man sich den Code möglicherweise in einem erheblichen Abstand vom Aufruf von try selbst ansehen.

Angesichts der Tatsache, dass diese Eigenschaft nackter Rückgaben (Nicht-Lokalität) im Allgemeinen unbeliebt ist, was denken Sie darüber, dass try immer die Nullwerte der Nicht-Fehler-Argumente zurückgibt (falls es überhaupt zurückgegeben wird)?

Einige Überlegungen:

Dies kann dazu führen, dass einige Muster, die die Verwendung von benannten Rückgabewerten beinhalten, try nicht verwenden können. Zum Beispiel für Implementierungen von io.Writer , die eine Anzahl geschriebener Bytes zurückgeben müssen, selbst in der Situation mit teilweisem Schreiben. Allerdings scheint try in diesem Fall sowieso fehleranfällig zu sein (zB n += try(wrappedWriter.Write(...)) setzt n im Falle einer Fehlerrückgabe nicht auf die richtige Zahl). Es erscheint mir in Ordnung, dass try für diese Art von Anwendungsfällen unbrauchbar wird, da Szenarien, in denen wir sowohl Werte als auch einen Fehler benötigen, meiner Erfahrung nach eher selten sind.

Wenn es eine Funktion mit vielen Verwendungen von try gibt, kann dies zu Code-Bloat führen, wo es viele Stellen in einer Funktion gibt, die die Ausgabevariablen auf Null setzen müssen. Erstens ist der Compiler heutzutage ziemlich gut darin, unnötige Schreibvorgänge zu optimieren. Und zweitens, wenn es sich als notwendig erweist, scheint es eine einfache Optimierung zu sein, alle try generierten Blöcke goto auf ein gemeinsames, funktionsweites Label zu setzen, das die Nicht-Fehler-Ausgabewerte auf Null setzt.

Wie Sie sicherlich wissen, ist tryhard bereits auf diese Weise implementiert, sodass als Nebeneffekt tryhard rückwirkend korrekter wird.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gibt Ihnen eine ziemlich gute Vorstellung davon, wie das aussehen könnte.

Danke, dass du diese Übung gemacht hast. Das bestätigt mir aber meine Vermutung, der Go-Quellcode selbst hat viele Stellen, an denen try() sinnvoll wäre, weil der Fehler einfach weitergegeben wird. Wie ich jedoch aus den Experimenten mit tryhard kann, die andere und ich oben eingereicht haben, wäre try() für viele andere Codebasen nicht sehr nützlich, da in Anwendungscode Fehler dazu neigen, tatsächlich behandelt zu werden, nicht gerade weitergegeben.

Ich würde das anders interpretieren.

Wir hatten keine Generika, daher wird es schwierig sein, Code in freier Wildbahn zu finden, der direkt von Generika profitieren würde, die auf geschriebenem Code basieren. Das bedeutet nicht, dass Generika nicht sinnvoll wären.

Für mich gibt es 2 Muster, die ich im Code für die Fehlerbehandlung verwendet habe

  1. Verwenden Sie Panics innerhalb des Pakets, stellen Sie die Panic wieder her und geben Sie ein Fehlerergebnis in den wenigen exportierten Methoden zurück
  2. Verwenden Sie bei einigen Methoden selektiv einen zurückgestellten Handler, damit ich Fehler mit umfangreichen PC-Informationen zu Stack-Dateien/Zeilennummern und mehr Kontext dekorieren kann

Diese Muster sind nicht weit verbreitet, aber sie funktionieren. 1) wird in der Standardbibliothek in ihren nicht exportierten Funktionen verwendet und 2) wird in den letzten Jahren in meiner Codebasis ausgiebig verwendet, weil ich dachte, dass es eine nette Art ist, die orthogonalen Funktionen zu verwenden, um vereinfachte Fehlerdekoration zu machen, und der Vorschlag empfiehlt und hat den Ansatz gesegnet. Die Tatsache, dass sie nicht weit verbreitet sind, bedeutet nicht, dass sie nicht gut sind. Aber wie bei allem werden Richtlinien des Go-Teams, die dies empfehlen, dazu führen, dass sie in Zukunft mehr in der Praxis verwendet werden.

Ein letzter Hinweis ist, dass das Ausschmücken von Fehlern in jeder Zeile Ihres Codes etwas zu viel sein kann. Es wird einige Stellen geben, an denen es sinnvoll ist, Fehler zu dekorieren, und einige Stellen, an denen dies nicht der Fall ist. Da wir zuvor keine großartigen Richtlinien hatten, entschieden die Leute, dass es sinnvoll sei, Fehler immer zu dekorieren. Aber es bringt vielleicht nicht viel Wert, immer jedes Mal zu dekorieren, wenn eine Datei nicht geöffnet wurde, da es innerhalb des Pakets ausreichen kann, nur einen Fehler wie „unable to open file: conf.json“ zu haben, im Gegensatz zu: „unable um den Benutzernamen zu erhalten: DB-Verbindung kann nicht hergestellt werden: Systemdatei kann nicht geladen werden: Datei kann nicht geöffnet werden: conf.json".

Mit der Kombination der Fehlerwerte und der prägnanten Fehlerbehandlung bekommen wir jetzt bessere Richtlinien für den Umgang mit Fehlern. Die Präferenz scheint zu sein:

  • ein Fehler wird einfach sein, z. B. "Datei kann nicht geöffnet werden: conf.json"
  • Es kann ein Fehlerrahmen angehängt werden, der den Kontext enthält: GetUserName --> GetConnection --> LoadSystemFile.
  • Wenn es zum Kontext beiträgt, können Sie diesen Fehler etwas umbrechen, z. B. MyAppError{error}

Ich neige dazu, das Gefühl zu haben, dass wir die Ziele des Versuchsvorschlags und die hochrangigen Dinge, die er zu lösen versucht, immer wieder übersehen:

  1. Reduzieren Sie die Textbausteine ​​von if err != nil { return err } für Stellen, an denen es sinnvoll ist, den Fehler nach oben zu propagieren, damit er weiter oben im Stack behandelt werden kann
  2. Ermöglicht die vereinfachte Verwendung von Rückgabewerten, bei denen err == nil
  3. Ermöglichen Sie es, die Lösung später zu erweitern, um beispielsweise mehr Fehlerdekoration vor Ort zu ermöglichen, zum Fehlerhandler zu springen, goto anstelle von Rückgabesemantik zu verwenden usw.
  4. Lassen Sie die Fehlerbehandlung zu, um die Logik der Codebasis nicht zu überladen, dh legen Sie sie mit einer Art Fehlerbehandlung etwas beiseite.

Viele Leute haben immer noch 1). Viele Leute haben um 1) herum gearbeitet, weil es vorher keine besseren Richtlinien gab. Aber das bedeutet nicht, dass sich ihre negative Reaktion, nachdem sie damit begonnen haben, nicht in eine positivere umwandeln würde.

Viele Leute können 2) verwenden. Es mag Meinungsverschiedenheiten darüber geben, wie viel, aber ich habe ein Beispiel gegeben, wo es meinen Code viel einfacher macht.

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

In Java, wo Ausnahmen die Norm sind, hätten wir:

User u = db.LoadUser(Integer.parseInt(stringId)))

Niemand würde sich diesen Code ansehen und sagen, dass wir es in 2 Zeilen machen müssen, dh.

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

Wir sollten das hier nicht tun müssen, unter der Richtlinie, dass try nicht inline aufgerufen werden darf und immer in einer eigenen Zeile stehen muss .

Darüber hinaus wird der meiste Code heute Dinge tun wie:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

Nun, jemand, der dies liest, muss diese 10 Zeilen analysieren, die in Java 1 Zeile gewesen wären und die mit dem Vorschlag hier 1 Zeile sein könnten. Ich muss visuell versuchen zu sehen, welche Zeilen hier wirklich relevant sind, wenn ich diesen Code lese. Die Boilerplate erschwert das Lesen und Groken dieses Codes.

Ich erinnere mich, dass ich in meinem früheren Leben an/mit aspektorientierter Programmierung in Java gearbeitet habe. Dort war das Ziel

Dadurch können Verhaltensweisen, die für die Geschäftslogik nicht zentral sind (z. B. Protokollierung), zu einem Programm hinzugefügt werden, ohne den Code, den Kern der Funktionalität, zu überladen. (Zitat aus Wikipedia https://en.wikipedia.org/wiki/Aspect-oriented_programming ).
Die Fehlerbehandlung ist nicht zentral für die Geschäftslogik, aber zentral für die Korrektheit. Die Idee ist die gleiche – wir sollten unseren Code nicht mit Dingen überladen, die für die Geschäftslogik nicht zentral sind, weil „ aber die Fehlerbehandlung sehr wichtig ist “. Ja, das ist es, und ja, wir können es beiseite legen.

In Bezug auf 4) haben viele Vorschläge Fehlerbehandlungsprogramme vorgeschlagen, bei denen es sich um Code an der Seite handelt, der Fehler behandelt, aber die Geschäftslogik nicht durcheinander bringt. Der ursprüngliche Vorschlag enthält das Schlüsselwort handle, und die Leute haben andere Dinge vorgeschlagen. Dieser Vorschlag besagt, dass wir den Verzögerungsmechanismus dafür nutzen können und das, was vorher seine Achillesferse war, einfach schneller machen können. Ich weiß - ich habe dem Go-Team viele Male Lärm über die Leistung des Verzögerungsmechanismus gemacht.

Beachten Sie, dass tryhard diesen Code nicht als etwas kennzeichnet, das vereinfacht werden kann. Aber mit try und neuen Richtlinien möchten die Leute diesen Code vielleicht zu einem Einzeiler vereinfachen und den Fehlerrahmen den erforderlichen Kontext erfassen lassen.

Der Kontext, der in auf Ausnahmen basierenden Sprachen sehr gut verwendet wurde, erfasst, dass versucht wurde, einen Benutzer zu laden, weil die Benutzer-ID nicht existierte oder weil die Zeichenfolgen-ID nicht in einem Format war, das eine Ganzzahl-ID haben könnte daraus geparst.

Kombinieren Sie das mit Error Formatter, und wir können jetzt den Fehlerrahmen und den Fehler selbst ausführlich untersuchen und die Nachricht für Benutzer schön formatieren, ohne den schwer lesbaren a: b: c: d: e: underlying error -Stil, den viele Leute gemacht haben und den wir nicht haben hatte tolle Richtlinien für.

Denken Sie daran, dass all diese Vorschläge zusammen die Lösung liefern, die wir wollen: präzise Fehlerbehandlung ohne unnötige Textbausteine, während sie gleichzeitig eine bessere Diagnose und eine bessere Fehlerformatierung für Benutzer bieten. Dies sind orthogonale Konzepte, aber zusammen werden sie extrem mächtig.

Schließlich ist es angesichts von 3) oben schwierig, ein Schlüsselwort zu verwenden, um dies zu lösen. Per Definition erlaubt ein Schlüsselwort keine Erweiterung, um in Zukunft einen Handler namentlich zu übergeben, oder ermöglicht eine Fehlerdekoration vor Ort oder unterstützt die Goto-Semantik (anstelle der Return-Semantik). Bei einem Schlüsselwort müssen wir zuerst die vollständige Lösung im Auge haben. Und ein Schlüsselwort ist nicht abwärtskompatibel. Das go-Team gab zu Beginn von Go 2 an, dass es versuchen wollte, die Abwärtskompatibilität so weit wie möglich aufrechtzuerhalten. Die try -Funktion behält dies bei, und wenn wir später sehen, dass keine Erweiterung erforderlich ist, kann ein einfaches gofix den Code leicht ändern, um die try -Funktion in ein Schlüsselwort zu ändern.

Nochmal meine 2 Cent!

Am 4.7.19 schrieb Sanjay Menakuru [email protected] :

@griesemer

[ ... ]
Wie mir auf Twitter mitgeteilt wurde, führt dies dazu, dass sich try wie ein Nackter benimmt
return, wobei die zurückgegebenen Werte implizit sind; herauszufinden, was
tatsächliche Werte zurückgegeben werden, muss man sich möglicherweise den Code bei a ansehen
erheblicher Abstand vom Anruf zu try selbst.

Da diese Eigenschaft von nackten Renditen (Nicht-Lokalität) allgemein ist
nicht gefallen, was denkst du darüber, dass try immer die Null zurückgibt
Werte der Nicht-Fehler-Argumente (falls überhaupt zurückgegeben)?

Nackte Rückgaben sind nur zulässig, wenn Rückgabeargumente genannt werden. Es
Scheint, dass Versuch einer anderen Regel folgt?

Ich mag die Gesamtidee, defer , um das Problem zu lösen. Ich frage mich jedoch, ob das Schlüsselwort try der richtige Weg ist, dies zu tun. Was wäre, wenn wir bereits vorhandene Muster wiederverwenden könnten? Etwas, das jeder bereits von Importen kennt:

Explizite Handhabung

res, err := doSomething()
if err != nil {
    return err
}

Explizites Ignorieren

res, _ := doSomething()

Verzögerte Abwicklung

Ähnliches Verhalten wie try .

res, . := doSomething()

@piotrkowalczuk
Dies mag eine schönere Syntax dafür sein, aber ich weiß nicht, wie einfach es wäre, Go anzupassen, um dies sowohl in Go als auch in Syntax-Highlightern legal zu machen.

@balasanjay (und @lootch): Gemäß Ihrem Kommentar hier , ja, das Programm https://play.golang.org/p/KenN56iNVg7 wird 1 drucken.

Da sich try nur um das Fehlerergebnis kümmert, lässt es alles andere in Ruhe. Es könnte andere Rückgabewerte auf ihre Nullwerte setzen, aber es ist nicht offensichtlich, warum das besser wäre. Zum einen könnte es mehr Arbeit verursachen, wenn Ergebniswerte benannt werden, weil sie möglicherweise auf Null gesetzt werden müssen; Der Anrufer wird sie jedoch (wahrscheinlich) ignorieren, wenn ein Fehler aufgetreten ist. Dies ist jedoch eine Designentscheidung, die geändert werden könnte, wenn es gute Gründe dafür gibt.

[Bearbeiten: Beachten Sie, dass diese Frage (ob Nicht-Fehler-Ergebnisse gelöscht werden sollen, wenn ein Fehler auftritt) nicht spezifisch für den try -Vorschlag ist. Jede der vorgeschlagenen Alternativen, die kein ausdrückliches return erfordern, muss dieselbe Frage beantworten.]

In Bezug auf Ihr Beispiel eines Schreibers n += try(wrappedWriter.Write(...)) : Ja, in einer Situation, in der Sie n selbst im Fehlerfall erhöhen müssen, kann man try nicht verwenden - selbst wenn try nullt Nicht-Fehler-Ergebniswerte nicht. Das liegt daran, dass try nur dann etwas zurückgibt, wenn kein Fehler vorliegt: try verhält sich sauber wie eine Funktion (aber eine Funktion, die möglicherweise nicht zum Aufrufer, sondern zum Aufrufer des Aufrufers zurückkehrt). Sehen Sie sich die Verwendung von Provisorien in der Implementierung von try an .

Aber in Fällen wie Ihrem Beispiel müsste man auch mit einer if Anweisung vorsichtig sein und sicherstellen, dass die zurückgegebene Byteanzahl in n eingebaut wird.

Aber vielleicht missverstehe ich deine Sorge.

@griesemer : Ich schlage vor, dass es besser ist, die anderen Rückgabewerte auf ihre Nullwerte zu setzen, weil dann klar ist, was try tun wird, wenn man nur die Callsite inspiziert. Es wird entweder a) nichts tun oder b) von der Funktion mit Nullwerten und dem zu versuchenden Argument zurückkehren.

Wie angegeben, behält try die Werte der benannten Nicht-Fehler-Rückgabewerte bei, und man müsste daher die gesamte Funktion untersuchen, um klar zu sein, welche Werte try zurückgeben.

Dies ist das gleiche Problem mit einer nackten Rückgabe (die gesamte Funktion muss gescannt werden, um zu sehen, welcher Wert zurückgegeben wird) und war vermutlich der Grund für die Einreichung von https://github.com/golang/go/issues/21291. Dies impliziert für mich, dass try in einer großen Funktion mit benannten Rückgabewerten auf der gleichen Grundlage wie nackte Rückgaben (https://github.com/golang/go/wiki/CodeReviewComments #benannte-Ergebnisparameter). Stattdessen schlage ich vor, dass try angegeben wird, um immer die Nullwerte des Nicht-Fehler-Arguments zurückzugeben.

verblüfft und fühle mich in letzter Zeit schlecht für das Go-Team. try ist eine saubere und verständliche Lösung für das spezifische Problem, das es zu lösen versucht: Ausführlichkeit bei der Fehlerbehandlung.

der vorschlag lautet: nach einjähriger diskussion fügen wir diese eingebaute hinzu. Verwenden Sie es, wenn Sie weniger ausführlichen Code wünschen, andernfalls fahren Sie mit dem fort, was Sie tun. Die Reaktion ist ein nicht ganz gerechtfertigter Widerstand gegen ein Opt-in-Feature, für das Teammitglieder klare Vorteile gezeigt haben!

Ich würde das Go-Team weiter ermutigen, try zu einem variadischen Einbau zu machen, wenn dies einfach zu bewerkstelligen ist

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

wird

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

Die nächste ausführliche Sache könnten diese aufeinanderfolgenden Aufrufe von try sein.

Ich stimme nvictor größtenteils zu, mit Ausnahme der variablen Parameter für try . Ich glaube immer noch, dass es einen Platz für einen Handler haben sollte, und der variadische Vorschlag kann die Lesbarkeitsgrenze für mich verschieben.

@nvictor Go ist eine Sprache, die keine nicht-orthogonalen Features mag. Das bedeutet, wenn wir in Zukunft eine bessere Fehlerbehandlungslösung finden, die nicht try ist, wird der Wechsel viel komplizierter (wenn sie nicht rundweg abgelehnt wird, weil unsere aktuelle Lösung ist "gut genug").

Ich denke, es gibt eine bessere Lösung als try , und ich würde es lieber langsam angehen und diese Lösung finden, als mich mit dieser zufrieden zu geben.

Ich wäre jedoch nicht böse, wenn dies hinzugefügt würde. Es ist keine schlechte Lösung, ich denke nur, dass wir vielleicht in der Lage sein werden, eine bessere Lösung zu finden.

In meiner Ansicht möchte ich einen Blockcode ausprobieren, jetzt try wie ein handle err func

Als ich diese Diskussion (und Diskussionen auf Reddit) las, hatte ich nicht immer das Gefühl, dass alle auf derselben Seite waren.

Daher habe ich einen kleinen Blogbeitrag geschrieben, der demonstriert, wie try verwendet werden kann: https://faiface.github.io/post/how-to-use-try/.

Ich habe versucht, mehrere Aspekte dieses Vorschlags zu zeigen, damit jeder sehen kann, was er bewirken kann, und sich eine fundiertere (wenn auch negative) Meinung bilden kann.

Sollte ich etwas Wichtiges vergessen haben, lasst es mich bitte wissen!

@faiface Ich bin mir ziemlich sicher, dass du ersetzen kannst

if err != nil {
    return resps, err
}

mit try(err) .

Ansonsten - toller Artikel!

@DmitriyMV Stimmt! Aber ich schätze, ich werde es so lassen, wie es ist, damit es zumindest ein Beispiel für das klassische if err != nil gibt, wenn auch kein sehr gutes.

Ich habe zwei Bedenken:

  • benannte Rückgaben waren sehr verwirrend, und dies ermutigt sie mit einem neuen und wichtigen Anwendungsfall
  • dies wird davon abhalten, Kontext zu Fehlern hinzuzufügen

Meiner Erfahrung nach ist das Hinzufügen von Kontext zu Fehlern unmittelbar nach jeder Aufrufseite entscheidend für Code, der leicht debuggt werden kann. Und benannte Rückgaben haben bei fast jedem Go-Entwickler, den ich kenne, irgendwann für Verwirrung gesorgt.

Ein kleinerer, stilistischer Aspekt ist, dass es unglücklich ist, wie viele Codezeilen jetzt in try(actualThing()) eingeschlossen werden. Ich kann mir vorstellen, die meisten Zeilen in einer Codebasis in try() zu sehen. Das fühlt sich unglücklich an.

Ich denke, diese Bedenken würden mit einer Optimierung angegangen:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() würde sich ähnlich verhalten wie try() , würde aber das Verhalten der generischen Übergabe von Funktionsrückgabewerten aufgeben und stattdessen die Möglichkeit bieten, Kontext hinzuzufügen. Es würde immer noch eine Rückkehr auslösen.

Dies würde viele der Vorteile von try() beibehalten:

  • es ist ein eingebautes
  • es folgt dem bestehenden Kontrollfluss WRT zum Verzögern
  • Es passt gut zu der bestehenden Praxis, Fehlern Kontext hinzuzufügen
  • Es stimmt mit aktuellen Vorschlägen und Bibliotheken für das Umschließen von Fehlern überein, wie z. B. errors.Wrap(err, "context message")
  • es führt zu einer sauberen Call-Site: Es gibt keine Textbausteine ​​in der a, b, err := myFunc() -Zeile
  • Das Beschreiben von Fehlern mit defer fmt.HandleError(&err, "msg") ist immer noch möglich, muss aber nicht gefördert werden.
  • Die Signatur von check ist etwas einfacher, da sie keine beliebige Anzahl von Argumenten von der Funktion, die sie umschließt, zurückgeben muss.

Das ist gut, ich denke, das Go-Team sollte das wirklich nehmen. Das ist besser als versuchen, klarer !!!

@buchanae Mich würde interessieren, was Sie über meinen Blogbeitrag denken, weil Sie argumentiert haben, dass try davon abhalten wird, Fehlern Kontext hinzuzufügen, während ich argumentieren würde, dass es zumindest in meinem Artikel noch einfacher als gewöhnlich ist.

Ich werde das einfach zum jetzigen Zeitpunkt da draußen werfen. Ich werde noch etwas darüber nachdenken, aber ich dachte, ich poste hier, um zu sehen, was Sie denken. Vielleicht sollte ich dafür ein neues Thema aufmachen? Ich habe dies auch auf #32811 gepostet

Wie wäre es also, stattdessen eine Art generisches C-Makro zu machen, um sich für mehr Flexibilität zu öffnen?

So was:

define returnIf(err error, desc string, args ...interface{}) {
    if (err != nil) {
        return fmt.Errorf("%s: %s: %+v", desc, err, args)
    }
}

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    :returnIf(err, "Error opening src", src)
    defer r.Close()

    w, err := os.Create(dst)
    :returnIf(err, "Error Creating dst", dst)
    defer w.Close()

    ...
}

Im Wesentlichen wird returnIf durch das oben Definierte ersetzt/inliniert. Die Flexibilität besteht darin, dass es an Ihnen liegt, was es tut. Das Debuggen könnte etwas seltsam sein, es sei denn, der Editor ersetzt es auf eine nette Art und Weise im Editor. Dies macht es auch weniger magisch, da Sie die Definition deutlich lesen können. Außerdem können Sie so eine Zeile haben, die möglicherweise bei einem Fehler zurückgegeben werden könnte. Und in der Lage, unterschiedliche Fehlermeldungen zu haben, je nachdem, wo es passiert ist (Kontext).

Bearbeiten: Außerdem wurde vor dem Makro ein Doppelpunkt hinzugefügt, um darauf hinzuweisen, dass dies möglicherweise getan werden kann, um zu verdeutlichen, dass es sich um ein Makro und nicht um einen Funktionsaufruf handelt.

@nvictor

ich würde das go-team weiter ermutigen, try zu einem variadic built-in zu machen

Was würde try(foo(), bar()) zurückgeben, wenn foo und bar nicht dasselbe zurückgeben?

Ich werde das einfach zum jetzigen Zeitpunkt da draußen werfen. Ich werde noch etwas darüber nachdenken, aber ich dachte, ich poste hier, um zu sehen, was Sie denken. Vielleicht sollte ich dafür ein neues Thema aufmachen? Ich habe dies auch auf #32811 gepostet

Wie wäre es also, stattdessen eine Art generisches C-Makro zu machen, um sich für mehr Flexibilität zu öffnen?

@Chillance , IMHO, ich denke, dass ein hygienisches Makrosystem wie Rust (und viele andere Sprachen) den Menschen die Möglichkeit geben würde, mit Ideen wie try oder Generika zu spielen, und dann, nachdem Erfahrungen gesammelt wurden, die besten Ideen werden können Teil der Sprache und Bibliotheken. Aber ich denke auch, dass die Wahrscheinlichkeit, dass so etwas zu Go hinzugefügt wird, sehr gering ist.

@jonbodner Es gibt derzeit einen Vorschlag, Hygienemakros in Go hinzuzufügen. Noch keine vorgeschlagene Syntax oder ähnliches, aber es gab nicht viel _gegen_ die Idee, hygienische Makros hinzuzufügen. #32620

@Allenyn , in Bezug auf den früheren Vorschlag von @buchanae , den Sie gerade zitiert haben :

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

Nach dem, was ich von der Diskussion gesehen habe, ist es meiner Meinung nach ein unwahrscheinliches Ergebnis, dass die Semantik von fmt in eine eingebaute Funktion gezogen wird. (Siehe zum Beispiel die Antwort von @josharian ).

Das heißt, es ist nicht wirklich erforderlich, auch weil das Zulassen einer Handler-Funktion das Ziehen fmt -Semantik direkt in eine integrierte Funktion umgehen kann. Ein solcher Ansatz wurde von @eihigh etwa am ersten Tag der Diskussion hier vorgeschlagen, der dem Vorschlag von @buchanae ähnlich ist und vorgeschlagen hat, das eingebaute try so zu optimieren, dass es stattdessen die folgende Signatur hat:

func try(error, optional func(error) error)

Da diese Alternative try nichts zurückgibt, impliziert diese Signatur:

  • es kann nicht in einen anderen Funktionsaufruf verschachtelt werden
  • es muss am Anfang der Zeile stehen

Ich möchte nicht den Namen bikeshedding auslösen, aber diese Form von try liest sich vielleicht besser mit einem alternativen Namen wie check . Man könnte sich Standard-Bibliothekshelfer vorstellen, die die optionale In-Place-Annotation bequem machen könnten, während defer eine Option für einheitliche Annotationen bleiben könnte, wenn dies gewünscht wird.

Es gab einige verwandte Vorschläge, die später in #32811 ( catch als eingebaute Funktion) und #32611 ( on Schlüsselwort, um on err, <statement> zuzulassen) erstellt wurden. Dies könnten gute Orte sein, um weiter zu diskutieren oder einen Daumen nach oben oder einen Daumen nach unten hinzuzufügen oder mögliche Änderungen an diesen Vorschlägen vorzuschlagen.

@jonbodner Es gibt derzeit einen Vorschlag, Hygienemakros in Go hinzuzufügen. Noch keine vorgeschlagene Syntax oder ähnliches, aber es gab nicht viel _gegen_ die Idee, hygienische Makros hinzuzufügen. #32620

Es ist großartig, dass es einen Vorschlag gibt, aber ich vermute, dass das Kernteam von Go nicht beabsichtigt, Makros hinzuzufügen. Ich würde mich jedoch gerne irren, da dies alle Argumente über Änderungen beenden würde, die derzeit Änderungen am Sprachkern erfordern. Um eine berühmte Marionette zu zitieren: "Tu es. Oder tu es nicht. Es gibt keinen Versuch."

@jonbodner Ich glaube nicht, dass das Hinzufügen von Hygienemakros das Argument beenden würde. Ganz im Gegenteil. Eine häufige Kritik ist, dass try die Rückgabe "versteckt". Makros wären aus dieser Sicht absolut schlechter, weil in einem Makro alles möglich wäre. Und selbst wenn Go benutzerdefinierte hygienische Makros zulassen würde, müssten wir noch darüber diskutieren, ob try ein eingebautes Makro sein sollte, das im Universumsblock vordeklariert ist, oder nicht. Es wäre logisch, dass diejenigen, die gegen try sind, noch mehr gegen hygienische Makros sind ;-)

@ngrilly Es gibt mehrere Möglichkeiten, um sicherzustellen, dass Makros hervorstechen und leicht zu sehen sind. Rust macht es so, dass Makros immer von ! vorangestellt werden (dh try!(...) und println!(...) ).

Ich würde argumentieren, dass wenn hygienische Makros übernommen und leicht zu sehen wären und nicht wie normale Funktionsaufrufe aussehen würden, sie viel besser passen würden. Wir sollten uns für allgemeinere Lösungen entscheiden, anstatt einzelne Probleme zu lösen.

@thepudds Ich stimme zu, dass das Hinzufügen eines optionalen Parameters vom Typ func(error) error nützlich sein könnte (diese Möglichkeit wird im Vorschlag diskutiert, mit einigen Problemen, die gelöst werden müssten), aber ich sehe den Sinn von try nicht try ist ein allgemeineres Tool.

@deanveloper Ja, das ! am Ende von Makros in Rust ist clever. Es erinnert an exportierte Bezeichner, die in Go mit einem Großbuchstaben beginnen :-)

Ich würde zustimmen, hygienische Makros in Go zu haben, wenn und nur wenn wir die Kompilierungsgeschwindigkeit beibehalten und komplexe Probleme in Bezug auf Tools lösen können (Refactoring-Tools müssten die Makros erweitern, um die Semantik des Codes zu verstehen, müssen aber Code mit den nicht erweiterten Makros generieren). . Es ist schwer. In der Zwischenzeit könnte try vielleicht in try! umbenannt werden? ;-)

Eine leichte Idee: Wenn der Körper eines if/for-Konstrukts eine einzelne Anweisung enthält, sind keine geschweiften Klammern erforderlich, vorausgesetzt, diese Anweisung befindet sich in derselben Zeile wie if oder for . Beispiel:

fd, err := os.Open("foo")
if err != nil return err

Beachten Sie, dass derzeit ein error -Typ nur ein gewöhnlicher Schnittstellentyp ist. Der Compiler behandelt es nicht als etwas Besonderes. try ändert das. Wenn der Compiler error als etwas Besonderes behandeln darf, würde ich ein /bin/sh inspiriertes || bevorzugen:

fd, err := os.Open("foo") || return err

Die Bedeutung eines solchen Codes wäre für die meisten Programmierer ziemlich offensichtlich, es gibt keinen versteckten Kontrollfluss, und da dieser Code derzeit illegal ist, wird kein funktionierender Code beschädigt.

Obwohl ich mir vorstellen kann, dass einige von Ihnen entsetzt zurückschrecken.

@bakul Woher wissen Sie in if err != nil return err , wo der Ausdruck err != nil endet und wo die Anweisung return err beginnt? Ihre Idee wäre eine größere Änderung der Sprachgrammatik, viel größer als das, was mit try vorgeschlagen wird.

Ihre zweite Idee sieht in Zig wie catch |err| return err aus. Ich persönlich schrecke nicht vor Entsetzen zurück und würde sagen, warum nicht? Aber man sollte beachten, dass Zig auch ein Schlüsselwort try hat, das eine Abkürzung für catch |err| return err ist und fast dem entspricht, was das Go-Team hier als eingebaute Funktion vorschlägt. Vielleicht reicht also try und wir brauchen das Schlüsselwort catch nicht? ;-)

@ngrilly , Derzeit ist <expr> <statement> nicht gültig, daher glaube ich nicht, dass diese Änderung die Grammatik mehrdeutig machen würde, aber möglicherweise etwas zerbrechlicher ist.

Dies würde genau den gleichen Code wie der try-Vorschlag erzeugen, aber a) die Rückgabe ist hier explizit b) es ist keine Verschachtelung möglich wie bei try und c) dies wäre eine vertraute Syntax für Shell-Benutzer (die weitaus mehr als zig Benutzer sind). Hier gibt es kein catch .

Ich habe dies als Alternative angesprochen, aber um ehrlich zu sein, bin ich vollkommen in Ordnung mit dem, was auch immer die Core-Go-Sprachdesigner entscheiden.

Ich habe eine leicht verbesserte Version von tryhard hochgeladen. Es meldet nun detailliertere Informationen zu den Eingabedateien. Wenn Sie zum Beispiel gegen Tipp des Go-Repos laufen, wird jetzt Folgendes gemeldet:

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

Es gibt noch mehr zu tun, aber das ergibt ein klareres Bild. Insbesondere scheinen 28 % aller if -Anweisungen der Fehlerprüfung zu dienen; dies bestätigt, dass es eine beträchtliche Menge an sich wiederholendem Code gibt. Von diesen Fehlerprüfungen wären 77 % für try geeignet.

$ bemüht .
--- Statistiken ---
2930 (100,0 % von 2930) Funktionsdeklarationen
1408 (48,1 % von 2930) Funktionen geben einen Fehler zurück
10497 (100,0 % von 10497) Aussagen
2265 (21,6 % von 10497) if-Anweisungen
1383 (61,1 % von 2265) wenn!= Null-Anweisungen
0 ( 0,0 % von 1383)name unterscheidet sich von „err“ (verwenden Sie das Flag -l
um Dateipositionen aufzulisten)
645 (46,6 % von 1383) geben zurück ...,Blöcke in wenn!= Null
Aussagen
738 (53,4 % von 1383) komplexerer Fehlerbehandler in if!= Null
Aussagen; Verwendung von try verhindern (Flag -l verwenden, um Dateipositionen aufzulisten)
1 (0,1 % von 1383) nicht leer sonst Blöcke in if!= Null
Aussagen; Verwendung von try verhindern (Flag -l verwenden, um Dateipositionen aufzulisten)
638 (46,1 % von 1383) versuchen Kandidaten (verwenden Sie das Flag -l, um die Datei aufzulisten
Positionen)
$ Go-Mod-Anbieter
$ Tryhard-Anbieter
--- Statistiken ---
37757 (100,0 % von 37757) Funktionsdeklarationen
12557 (33,3 % von 37757) Funktionen geben einen Fehler zurück
88919 (100,0 % von 88919) Aussagen
20143 (22,7 % von 88919) wenn Aussagen
6555 (32,5 % von 20143) wenn!= Null-Anweisungen
109 ( 1,7 % von 6555)name unterscheidet sich von „err“ (verwenden Sie das Flag -l
um Dateipositionen aufzulisten)
5545 (84,6 % von 6555) geben zurück ...,Blöcke in wenn!= Null
Aussagen
1010 (15,4 % von 6555) komplexerer Fehlerbehandler in if!= Null
Aussagen; Verwendung von try verhindern (Flag -l verwenden, um Dateipositionen aufzulisten)
12 ( 0,2 % von 6555) nicht leer sonst Blöcke in if!= Null
Aussagen; Verwendung von try verhindern (Flag -l verwenden, um Dateipositionen aufzulisten)
5427 (82,8 % von 6555) versuchen Kandidaten (verwenden Sie das Flag -l, um die Datei aufzulisten
Positionen)

Aus diesem Grund habe ich im Makrobeispiel einen Doppelpunkt hinzugefügt, damit er herausragt und nicht wie ein Funktionsaufruf aussieht. Muss natürlich kein Doppelpunkt sein. Es ist nur ein Beispiel. Außerdem verbirgt ein Makro nichts. Sie sehen sich nur an, was das Makro tut, und los geht's. Wie wenn es eine Funktion wäre, aber es wird inline sein. Es ist, als hätten Sie eine Suche durchgeführt und durch das Codestück aus dem Makro in Ihren Funktionen ersetzt, in denen die Makroverwendung durchgeführt wurde. Natürlich, wenn Leute Makros von Makros machen und anfangen, die Dinge zu komplizieren, dann machen Sie sich selbst die Schuld dafür, dass Sie den Code komplizierter gemacht haben. :)

@mirtschowski

$ tryhard .
--- stats ---
   2930 (100.0% of    2930) function declarations
   1408 ( 48.1% of    2930) functions returning an error
  10497 (100.0% of   10497) statements
   2265 ( 21.6% of   10497) if statements
   1383 ( 61.1% of    2265) if <err> != nil statements
      0 (  0.0% of    1383) <err> name is different from "err" (use -l flag to list file positions)
    645 ( 46.6% of    1383) return ..., <err> blocks in if <err> != nil statements
    738 ( 53.4% of    1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
      1 (  0.1% of    1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
    638 ( 46.1% of    1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
  37757 (100.0% of   37757) function declarations
  12557 ( 33.3% of   37757) functions returning an error
  88919 (100.0% of   88919) statements
  20143 ( 22.7% of   88919) if statements
   6555 ( 32.5% of   20143) if <err> != nil statements
    109 (  1.7% of    6555) <err> name is different from "err" (use -l flag to list file positions)
   5545 ( 84.6% of    6555) return ..., <err> blocks in if <err> != nil statements
   1010 ( 15.4% of    6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     12 (  0.2% of    6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5427 ( 82.8% of    6555) try candidates (use -l flag to list file
positions)
$

@ av86743 ,

Entschuldigung, habe nicht berücksichtigt, dass "E-Mail-Antworten Markdown nicht unterstützen"

Einige Leute haben kommentiert, dass es nicht fair ist, von Anbietern bereitgestellten Code in tryhard -Ergebnissen zu zählen. Beispielsweise enthält der in der std-Bibliothek bereitgestellte Code die generierten syscall -Pakete, die eine Menge Fehlerprüfungen enthalten und das Gesamtbild verzerren können. Die neueste Version von tryhard schließt jetzt standardmäßig Dateipfade aus, die "vendor" enthalten (dies kann auch mit dem neuen Flag -ignore gesteuert werden). Angewendet auf die std-Bibliothek bei tip:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

Jetzt scheinen 29 % (28,9 %) aller if -Anweisungen für die Fehlerprüfung bestimmt zu sein (also etwas mehr als zuvor), und davon scheinen etwa 70 % Kandidaten für try (etwas weniger als vorher).

Änderung https://golang.org/cl/185177 erwähnt dieses Problem: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src

@griesemer Sie haben "komplexe Fehlerhandler" gezählt, aber keine "Fehlerhandler mit einer einzigen Anweisung".

Wenn die meisten "komplexen" Handler aus einer einzelnen Anweisung bestehen, dann würde on err #32611 etwa so viele Boilerplate-Einsparungen bringen wie try() -- 2 Zeilen vs. 3 Zeilen x 70 %. Und on err fügt den Vorteil eines konsistenten Musters für die überwiegende Mehrheit der Fehler hinzu.

@nvictor

try ist eine saubere und verständliche Lösung für das spezifische Problem, das es zu lösen versucht:
Ausführlichkeit in der Fehlerbehandlung.

Ausführlichkeit bei der Fehlerbehandlung ist kein _Problem_, sondern Gos Stärke.

der vorschlag lautet: nach einjähriger diskussion fügen wir diese eingebaute hinzu. Verwenden Sie es, wenn Sie weniger ausführlichen Code wünschen, andernfalls fahren Sie mit dem fort, was Sie tun. Die Reaktion ist ein nicht ganz gerechtfertigter Widerstand gegen ein Opt-in-Feature, für das Teammitglieder klare Vorteile gezeigt haben!

Dein _Opt-in_ beim Schreiben ist ein _Muss_ für alle Leser, einschließlich Future-You.

klare Vorteile

Wenn das Verschmutzen des Kontrollflusses als „Vorteil“ bezeichnet werden kann, dann ja.

try führt um der Gewohnheiten von Java- und C++-Expats willen Magie ein, die von allen Gophers verstanden werden muss. In der Zwischenzeit spart sich eine Minderheit ein paar Zeilen, um an ein paar Stellen zu schreiben (wie tryhard Läufe gezeigt haben).

Ich würde argumentieren, dass mein einfacheres onErr-Makro mehr Zeilen schreiben würde, und für die Mehrheit:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

_(Beachten Sie, dass ich im Lager „ if err!= nil in Ruhe lassen“ bin und der obige Gegenvorschlag veröffentlicht wurde, um eine einfachere Lösung zu zeigen, die mehr Nörgler glücklich machen kann.)_

Bearbeiten:

Ich würde das Go-Team weiter ermutigen, try zu einem variadischen Einbau zu machen, wenn dies einfach zu bewerkstelligen ist
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

~Kurz zum Schreiben, lang zum Lesen, anfällig für Ausrutscher oder Missverständnisse, schuppig und gefährlich in der Wartungsphase.~

Ich habe mich geirrt. Eigentlich wäre das variadische try viel besser als Nester, wie wir es zeilenweise schreiben könnten:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

und nach dem ersten Fehler try(…) zurückgeben.

Ich denke nicht, dass dieses implizite Fehlerhandle (Syntaxzucker) wie try gut ist, da Sie mehrere Fehler nicht intuitiv behandeln können, insbesondere wenn Sie mehrere Funktionen nacheinander ausführen müssen.

Ich würde so etwas wie Elixir mit Aussage vorschlagen: https://www.openmymind.net/Elixirs-With-Statement/

So etwas wie das unten in Golang:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

Ist diese Art von Verstoß gegen das „Go bevorzugt weniger Funktionen“ und „das Hinzufügen von Funktionen zu Go würde es nicht besser, aber größer machen“? Ich bin nicht sicher...

Ich möchte nur sagen, dass ich persönlich mit dem alten Weg vollkommen zufrieden bin

if err != nil {
    return …, err
}

Und auf keinen Fall möchte ich den Code lesen, der von anderen mit dem try geschrieben wurde ... Der Grund kann zweierlei sein:

  1. Was sich darin verbirgt, ist manchmal auf den ersten Blick schwer zu erraten
  2. try s können verschachtelt sein, dh try( ... try( ... try ( ... ) ... ) ... ) , schwer zu lesen

Wenn Sie der Meinung sind, dass es mühsam ist, Code auf die alte Art zu schreiben, um Fehler zu übergeben, warum nicht einfach kopieren und einfügen, da sie immer die gleiche Arbeit erledigen?

Nun, Sie könnten denken, dass wir nicht immer denselben Job machen wollen, aber dann müssen Sie Ihre "Handler"-Funktion schreiben. Vielleicht verlierst du also nichts, wenn du immer noch auf die alte Art schreibst.

Ist die Leistung von defer bei dieser vorgeschlagenen Lösung nicht ein Problem? Ich habe Funktionen mit und ohne Verzögerung bewertet und es gab erhebliche Auswirkungen auf die Leistung. Ich habe gerade jemanden gegoogelt, der einen solchen Benchmark durchgeführt hat, und einen 16-fachen Preis gefunden. Ich kann mich nicht erinnern, dass meins so schlecht war, aber 4x langsamer klingelt es. Wie kann etwas, das die Laufzeit vieler Funktionen verdoppeln oder verschlechtern könnte, als praktikable allgemeine Lösung angesehen werden?

@eric-hawthorne Die Aufschiebung der Leistung ist ein separates Problem. Try erfordert nicht von Natur aus defer und entfernt nicht die Fähigkeit, Fehler ohne es zu behandeln.

@fabian-f Aber dieser Vorschlag könnte das Ersetzen von Code fördern, in dem jemand die Fehler separat für jeden Fehler inline im Rahmen des Blocks if err != nil dekoriert. Das wäre ein erheblicher Leistungsunterschied.

@eric-hawthorne Zitieren des Designdokuments:

F: Wird die Verwendung von defer für Wrapping-Fehler nicht langsam sein?

A: Derzeit ist eine Defer-Anweisung im Vergleich zu einem gewöhnlichen Kontrollfluss relativ teuer. Wir glauben jedoch, dass es möglich ist, gängige Anwendungsfälle der Zurückstellung für die Fehlerbehandlung in der Leistung mit dem aktuellen „manuellen“ Ansatz vergleichbar zu machen. Siehe auch CL 171758, von dem erwartet wird, dass es die Leistung von defer um etwa 30 % verbessert.

Hier war ein interessanter Vortrag von Rust, der auf Reddit verlinkt ist. Der relevanteste Teil beginnt bei 47:55

Ich habe es mit meinem größten öffentlichen Repo versucht, https://github.com/dpinela/mflg , und Folgendes erhalten:

--- stats ---
    309 (100.0% of     309) func declarations
     36 ( 11.7% of     309) func declarations returning an error
    305 (100.0% of     305) statements
     73 ( 23.9% of     305) if statements
     29 ( 39.7% of      73) if <err> != nil statements
      0 (  0.0% of      29) <err> name is different from "err"
     19 ( 65.5% of      29) return ..., <err> blocks in if <err> != nil statements
     10 ( 34.5% of      29) complex error handler in if <err> != nil statements; cannot use try
      0 (  0.0% of      29) non-empty else blocks in if <err> != nil statements; cannot use try
     15 ( 51.7% of      29) try candidates

Der größte Teil des Codes in diesem Repo verwaltet den Zustand des internen Editors und führt keine E/A durch und hat daher nur wenige Fehlerprüfungen - daher sind die Stellen, an denen try verwendet werden kann, relativ begrenzt. Ich ging weiter und schrieb den Code manuell um, um nach Möglichkeit try zu verwenden. git diff --stat gibt Folgendes zurück:

 application.go                  | 42 +++++++++++-------------------------------
 internal/atomicwrite/write.go   | 35 ++++++++++++++---------------------
 internal/clipboard/clipboard.go | 17 +++--------------
 internal/config/config.go       | 15 +++++++--------
 internal/termesc/term.go        |  5 +----
 render.go                       |  8 ++------
 6 files changed, 38 insertions(+), 84 deletions(-)

(Vollständiges Diff hier .)

Von den 10 Handlern, die tryhard als „komplex“ meldet, sind 5 falsche Negative in internal/atomicwrite/write.go; Sie verwendeten pkg/errors.WithMessage, um den Fehler einzuschließen. Die Verpackung war für alle genau gleich, also habe ich diese Funktion umgeschrieben, um try- und deferred-Handler zu verwenden. Ich endete mit diesem Unterschied (+14, -21 Zeilen):

@@ -20,21 +20,20 @@ const (
 // The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
 // preserved if possible.
 // If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+       defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
        dir := filepath.Dir(filename)
-       if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
-       tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
-       if err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(os.MkdirAll(dir, defaultDirPerms))
+       tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
        name := tf.Name()
-       if err = contentWriter(tf); err != nil {
-               os.Remove(name)
-               tf.Close()
-               return errors.WithMessage(err, errString(filename))
-       }
+       defer func() {
+               if err != nil {
+                       tf.Close()
+                       os.Remove(name)
+               }
+       }()
+       try(contentWriter(tf))
        // Keep existing file's permissions, when possible. This may race with a chmod() on the file.
        perms := defaultPerms
        if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
        }
        // It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
        tf.Chmod(perms)
-       if err = tf.Close(); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
-       if err = os.Rename(name, filename); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(tf.Close())
+       try(os.Rename(name, filename))
        return nil
 }

Beachten Sie die erste Verzögerung, die den Fehler kommentiert - ich konnte sie bequem in eine Zeile einfügen, dank WithMessage, das nil für einen nil-Fehler zurückgibt. Es scheint, dass diese Art von Wrapper mit diesem Ansatz genauso gut funktioniert wie die im Vorschlag vorgeschlagenen.

Zwei der anderen "komplexen" Handler befanden sich in Implementierungen von ReadFrom und WriteTo:

var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
  if err == io.EOF {
    err = nil
  }
  return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
    var n int64
    for _, line := range b.lines {
        nw, err := w.Write([]byte(line))
        n += int64(nw)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

Diese waren wirklich nicht zugänglich, also ließ ich sie in Ruhe.

Zwei andere waren Code wie dieser, bei dem ich einen völlig anderen Fehler als den, den ich überprüft habe, zurückgebe (nicht nur umschließe). Ich habe sie auch unverändert gelassen:

n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
    return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}

Der letzte war in einer Funktion zum Laden einer Konfigurationsdatei, die immer eine (ungleich Null) Konfiguration zurückgibt, selbst wenn ein Fehler auftritt. Es hatte nur diese eine Fehlerprüfung, also hat es nicht viel, wenn überhaupt, von try profitiert:

-func Load() (*Config, error) {
-       c := Config{
+func Load() (c *Config, err error) {
+       defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+       c = &Config{
                TabWidth:    4,
                ScrollSpeed: 1,
                Lang:        make(map[string]LangConfig),
        }
-       f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
-       if err != nil {
-               return &c, errors.WithMessage(err, "error loading config file")
-       }
+       f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
        defer f.Close()
-       _, err = toml.DecodeReader(f, &c)
+       _, err = toml.DecodeReader(f, c)
        if c.TextStyle.Comment == (Style{}) {
                c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
        }
        if c.TextStyle.String == (Style{}) {
                c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
        }
-       return &c, errors.WithMessage(err, "error loading config file")
+       return c, err
 }

Tatsächlich fühlt es sich meiner Meinung nach etwas schwieriger an, sich auf das Verhalten von try zu verlassen, die Werte von Rückgabeparametern beizubehalten - wie eine nackte Rückgabe. Wenn ich keine weiteren Fehlerprüfungen hinzugefügt habe, würde ich in diesem speziellen Fall bei if err != nil bleiben.

TL;DR: try ist nur in einem ziemlich kleinen Prozentsatz (nach Zeilenanzahl) dieses Codes nützlich, aber wo es hilft, hilft es wirklich.

(Noob hier). Eine weitere Idee für mehrere Argumente. Wie wäre es mit:

package trytest

import "fmt"

func errorInner() (string, error) {
   return "", fmt.Errorf("inner error")
}

func errorOuter() (string, error) {
   tryreturn errorInner()
   return "", nil
}

func errorOuterWithArg() (string, error) {
   var toProcess string
   tryreturn toProcess, _ = errorOuter()
   return toProcess + "", nil
}

func errorOuterWithArgStretch() (bool, string, error) {
   var toProcess string
   tryreturn false, ( toProcess,_ = errorOuterWithArg() )
   return true, toProcess + "", nil
}

dh tryreturn löst die Rückgabe aller Werte aus, wenn zuletzt ein Fehler auftritt
Wert, andernfalls wird die Ausführung fortgesetzt.

Die Grundsätze, denen ich zustimme:
-

  • Die Fehlerbehandlung eines Funktionsaufrufs verdient eine eigene Zeile. Go ist im Kontrollfluss absichtlich explizit, und ich denke, das in einen Ausdruck zu packen, steht im Widerspruch zu seiner Explizitheit.
  • Es wäre vorteilhaft, eine Fehlerbehandlungsmethode zu haben, die in eine Zeile passt. (Und benötigen idealerweise nur ein Wort oder wenige Zeichen Textbausteine ​​vor der eigentlichen Fehlerbehandlung). 3 Zeilen Fehlerbehandlung für jeden Funktionsaufruf ist ein Reibungspunkt in der Sprache, der etwas Liebe und Aufmerksamkeit verdient.
  • Jedes eingebaute Element, das zurückkehrt (wie das vorgeschlagene try ), sollte zumindest eine Anweisung sein und idealerweise das Wort return enthalten. Auch hier denke ich, dass der Kontrollfluss in Go explizit sein sollte.
  • Die Fehler von Go sind am nützlichsten, wenn sie zusätzlichen Kontext enthalten (ich füge meinen Fehlern fast immer Kontext hinzu). Eine Lösung für dieses Problem sollte auch Kontext-hinzufügenden Fehlerbehandlungscode unterstützen.

Syntax, die ich unterstütze:
-

  • eine reterr _x_ -Anweisung (syntaktischer Zucker für if err != nil { return _x_ } , explizit benannt, um anzuzeigen, dass es zurückgegeben wird)

Die üblichen Fälle könnten also eine nette kurze, explizite Zeile sein:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

Statt der 3 Zeilen lauten sie jetzt:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

Dinge, mit denen ich nicht einverstanden bin:



    • "Dies ist eine zu kleine Änderung, um die Sprache zu ändern."

      Ich bin anderer Meinung, dies ist eine Verbesserung der Lebensqualität, die die größte Reibungsquelle beseitigt, die ich beim Schreiben von Go-Code habe. Beim Aufruf einer Funktion werden 4 Zeilen benötigt

  • "Es wäre besser, auf eine allgemeinere Lösung zu warten"
    Ich bin anderer Meinung, ich denke, dieses Problem verdient eine eigene dedizierte Lösung. Die verallgemeinerte Version dieses Problems ist das Reduzieren von Boilerplate-Code, und die verallgemeinerte Antwort sind Makros – was gegen das Go-Ethos von explizitem Code verstößt. Wenn Go keine allgemeine Makrofunktion bereitstellt, sollte es stattdessen einige spezifische, sehr weit verbreitete Makros wie reterr bereitstellen (jede Person, die Go schreibt, würde von reterr profitieren).

@Qhesz Beim Versuch ist es nicht viel anders:

func foo() error {
    a, err := bar()
    try(err)

    b, err := baz(a)
    try(wrap(err, "getting the baz of %v", a))

    return nil
}

@reusee Ich schätze diesen Vorschlag, ich wusste nicht, dass er so verwendet werden könnte. Es scheint mir jedoch ein bisschen kratzend zu sein, ich versuche, meinen Finger darauf zu legen, warum.

Ich denke, dass "versuchen" ein seltsames Wort ist, um es auf diese Weise zu verwenden. "try(action())" macht auf Englisch Sinn, während "try(value)" nicht wirklich Sinn macht. Ich fände es besser, wenn es ein anderes Wort wäre.

Auch try(wrap(...)) wertet zuerst wrap(...) aus, richtig? Wie viel davon wird Ihrer Meinung nach vom Compiler optimiert? (Im Vergleich zum einfachen Ausführen if err != nil ?)

Auch #32611 ist ein vage ähnlicher Vorschlag, und die Kommentare enthalten einige aufschlussreiche Meinungen sowohl des Go-Kernteams als auch der Community-Mitglieder, insbesondere zu den Unterschieden zwischen Schlüsselwörtern und integrierten Funktionen.

@Qhesz Ich stimme dir bezüglich der Benennung zu. Vielleicht ist check besser geeignet, da entweder "check(action())" oder "check(err)" gut lesbar ist.

@reusee Was ein bisschen ironisch ist, da der ursprüngliche Entwurfsentwurf check verwendete.

Am 6.7.19 schrieb mirtchovski [email protected] :

$ bemüht .
--- Statistiken ---
2930 (100,0 % von 2930) Funktionsdeklarationen
1408 (48,1 % von 2930) Funktionen geben einen Fehler zurück
[ ... ]

Ich kann nicht umhin, hier schelmisch zu sein: Ist das "Funktionen, die eine zurückgeben
Fehler als letztes Argument"?

Lucio.

Abschließender Gedanke zu meiner obigen Frage: Ich würde immer noch die Syntax try(err, wrap("getting the baz of %v: %v", a, err)) bevorzugen, wobei wrap() nur ausgeführt wird, wenn err nicht nil ist. Statt try(wrap(err, "getting the baz of %v", a)) .

@Qhesz Eine mögliche Implementierung von wrap könnte sein:

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Wenn der Compiler wrap einbetten kann, gibt es keinen Leistungsunterschied zwischen der wrap - und der if err != nil -Klausel.

@reusee Ich glaube du meintest if err == nil ;)

@Qhesz Eine mögliche Implementierung von wrap könnte sein:

func wrap(err error, format string, args ...interface{}) error {
  if err == nil {
      return nil
  }
  return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Wenn der Compiler wrap einbetten kann, gibt es keinen Leistungsunterschied zwischen der wrap - und der if err != nil -Klausel.

%w ist kein gültiges Verb go

(Ich nehme an, er meinte %v...)

Obwohl es vorzuziehen wäre, ein Schlüsselwort zu schreiben, verstehe ich, dass ein Built-in der bevorzugte Weg ist, es zu implementieren.

Ich denke, ich wäre mit diesem Vorschlag an Bord, wenn

  • es war check statt try
  • Ein Teil der Go-Werkzeuge hat erzwungen, dass es nur wie eine Anweisung verwendet werden kann (dh wie eine eingebaute 'Anweisung' behandelt werden kann, nicht wie eine eingebaute 'Funktion'. Es ist nur aus praktischen Gründen eine eingebaute Anweisung, es versucht, eine Anweisung zu sein, ohne zu sein von der Sprache implementiert.) Wenn zum Beispiel nichts zurückgegeben wurde, war so nie innerhalb eines Ausdrucks gültig, wie panic() .
  • ~ vielleicht ein Indikator dafür, dass es sich um ein Makro handelt und den Kontrollfluss beeinflusst, etwas, das es von einem Funktionsaufruf unterscheidet. (zum Beispiel check!(...) wie Rust, aber ich habe keine starke Meinung zur spezifischen Syntax)~ Meine Meinung geändert

Dann wäre das großartig, ich würde es bei jedem Funktionsaufruf verwenden, den ich mache.

Und kleine Entschuldigungen an den Thread, ich habe erst jetzt die Kommentare oben gefunden, die ziemlich genau das umreißen, was ich gerade gesagt habe.

@deanveloper behoben, danke.

@olekukonko @Qhesz %w wurde neu im Tipp hinzugefügt: https://tip.golang.org/pkg/fmt/#Errorf

Ich entschuldige mich dafür, dass ich nicht alles in diesem Thema gelesen habe, aber ich möchte etwas erwähnen, das ich nicht gesehen habe.

Ich sehe zwei verschiedene Fälle, in denen die Fehlerbehandlung von Go1 lästig sein kann: "guter" Code, der korrekt ist, sich aber ein wenig wiederholt; und "schlechter" Code, der falsch ist, aber meistens funktioniert.

Im ersten Fall sollte der if-err-Block wirklich etwas Logik enthalten, und der Wechsel zu einem try-Stilkonstrukt entmutigt diese gute Praxis, da es schwieriger wird, zusätzliche Logik hinzuzufügen.

Im zweiten Fall hat schlechter Code oft die Form:

..., _ := might_error()

oder nur

might_error()

Wo dies auftritt, liegt es normalerweise daran, dass der Autor es nicht für wichtig genug hält, Zeit für die Fehlerbehandlung aufzuwenden, und nur hofft, dass alles funktioniert. Dieser Fall könnte durch etwas sehr nahes Null-Aufwand verbessert werden, wie:

..., XXX := might_error()

wobei XXX ein Symbol ist, das bedeutet "alles hier sollte die Ausführung irgendwie stoppen". Dies würde deutlich machen, dass dies kein produktionsreifer Code ist – der Autor ist sich eines Fehlerfalls bewusst, hat aber nicht die Zeit investiert, um zu entscheiden, was zu tun ist.

Dies schließt natürlich eine Lösung vom Typ returnif handle(err) nicht aus.

Ich bin gegen den Versuch, alles in allem, mit Komplimenten an die Mitwirkenden für das schön minimalistische Design. Ich bin kein großer Go-Experte, war aber ein Early Adopter und habe hier und da Code in Produktion. Ich arbeite in der Serverless-Gruppe in AWS und es sieht so aus, als würden wir später in diesem Jahr einen Go-basierten Service veröffentlichen, dessen erster Check-in im Wesentlichen von mir geschrieben wurde. Ich bin ein ganz alter Mann, mein Weg führte über C, Perl, Java und Ruby. Meine Probleme sind bereits in der sehr nützlichen Zusammenfassung der Debatte aufgetaucht, aber ich denke immer noch, dass sie es wert sind, wiederholt zu werden.

  1. Go ist eine kleine, einfache Sprache und hat dadurch eine unerreichte Lesbarkeit erreicht. Ich bin reflexartig dagegen, etwas hinzuzufügen, es sei denn, der Nutzen ist wirklich qualitativ erheblich. Normalerweise bemerkt man einen rutschigen Abhang erst, wenn man darauf steht, also machen wir nicht den ersten Schritt.
  2. Ich war ziemlich stark von dem obigen Argument über die Erleichterung des Debugging betroffen. Ich mag den visuellen Rhythmus kleiner Code-Strophen im Low-Level-Infrastrukturcode nach dem Motto „Mach A. Prüfe, ob es funktioniert hat. Tun Sie B. Prüfen Sie, ob es funktioniert hat … usw.“ Weil die „Check“-Zeilen dort sind, wo Sie das printf oder den Breakpoint setzen. Vielleicht sind alle anderen klüger, aber ich benutze diese Breakpoint-Sprache regelmäßig.
  3. Unter der Annahme benannter Rückgabewerte entspricht "try" ungefähr if err != nil { return } (glaube ich?). Ich persönlich mag benannte Rückgabewerte, und angesichts der Vorteile von Fehlerdekoratoren vermute ich, dass der Anteil benannter err-Rückgabewerte zunimmt monoton steigend; was die Vorteile von try schwächt.
  4. Anfangs gefiel mir der Vorschlag, gofmt den Einzeiler in der obigen Zeile segnen zu lassen, aber alles in allem werden die IDEs diese Anzeigesprache ohnehin zweifellos übernehmen, und der Einzeiler würde den Debug-Hier-Vorteil opfern.
  5. Es scheint sehr wahrscheinlich, dass einige Formen der Ausdrucksverschachtelung, die "try" enthalten, den Komplizierern in unserem Beruf die Tür öffnen werden, um die gleiche Art von Chaos anzurichten, die sie mit Java-Streams und -Splittern usw. anrichten. Go war erfolgreicher als die meisten anderen Sprachen darin, den Schlauen unter uns Gelegenheiten zu verweigern, ihre Fähigkeiten unter Beweis zu stellen.

Nochmals herzlichen Glückwunsch an die Community für den schönen, sauberen Vorschlag und die konstruktive Diskussion.

Ich habe in den letzten Jahren viel Zeit damit verbracht, in unbekannte Bibliotheken oder Codeteile zu springen und sie zu lesen. Trotz der Langeweile bietet if err != nil eine sehr einfach zu lesende, wenn auch vertikal wortreiche Redewendung. Der Geist dessen, was try() zu erreichen versucht, ist edel, und ich denke, es gibt etwas zu tun, aber dieses Feature fühlt sich falsch priorisiert an und der Vorschlag erblickt zu früh das Licht der Welt (d. h. er sollte kommen nachdem xerr und Generika die Chance hatten, in einer stabilen Version für 6-12 Monate zu marinieren).

Die Einführung try() scheint ein nobler und lohnender Vorschlag zu sein (z. B. 29% - ~40% der if -Auszüge sind für if err != nil -Überprüfung). Oberflächlich betrachtet sieht es so aus, als würden die mit der Fehlerbehandlung verbundenen Reduzierungsbausteine ​​das Entwicklererlebnis verbessern. Der Kompromiss aus der Einführung von try() kommt in Form einer kognitiven Belastung durch die halb subtilen Spezialfälle. Einer der größten Vorteile von Go ist, dass es einfach ist und nur sehr wenig kognitive Belastung erforderlich ist, um etwas zu erledigen (im Vergleich zu C++, wo die Sprachspezifikation groß und nuanciert ist). Das Reduzieren einer quantitativen Metrik (LoC von if err != nil ) im Austausch für die Erhöhung der quantitativen Metrik der mentalen Komplexität ist eine schwer zu schluckende Pille (dh die mentale Steuer auf die wertvollste Ressource, die wir haben, die Gehirnleistung).

Insbesondere die neuen Spezialfälle für die Art und Weise, wie try() mit go , defer behandelt wird, und benannte Rückgabevariablen machen try() magisch genug, um den Code weniger zu machen explizit, so dass alle Autoren oder Leser des Go-Codes diese neuen Sonderfälle kennen müssen, um Go richtig lesen oder schreiben zu können, und eine solche Belastung gab es vorher nicht. Ich finde es gut, dass es für diese Situationen explizite Sonderfälle gibt – insbesondere gegenüber der Einführung einer Form von undefiniertem Verhalten, aber die Tatsache, dass sie überhaupt existieren müssen, zeigt, dass dies im Moment unvollständig ist. Wenn es sich bei den Sonderfällen um etwas anderes als die Fehlerbehandlung handeln würde, könnte dies möglicherweise akzeptabel sein, aber wenn wir bereits über etwas sprechen, das bis zu 40 % aller LoC betreffen könnte, müssen diese Sonderfälle in der gesamten Community geschult werden und das erhöht die Kosten der kognitiven Belastung dieses Vorschlags auf ein Niveau, das hoch genug ist, um Anlass zur Sorge zu geben.

Es gibt ein weiteres Beispiel in Go, wo Sonderfallregeln bereits ein schlüpfriger kognitiver Abhang sind, nämlich gepinnte und nicht gepinnte Variablen. Die Notwendigkeit, Variablen anzuheften, ist in der Praxis nicht schwer zu verstehen, wird jedoch übersehen, da hier ein implizites Verhalten vorliegt und dies zu einer Diskrepanz zwischen dem Autor, dem Leser und dem führt, was mit der kompilierten ausführbaren Datei zur Laufzeit passiert. Selbst bei Linters wie scopelint scheinen viele Entwickler diesen Fallstrick immer noch nicht zu begreifen (oder noch schlimmer, sie wissen es, verpassen es aber, weil ihnen dieser Fallstrick entgeht). Einige der unerwartetsten und am schwierigsten zu diagnostizierenden Laufzeitfehler von funktionierenden Programmen sind auf dieses spezielle Problem zurückzuführen (z. B. N Objekte werden alle mit demselben Wert gefüllt, anstatt über einen Slice zu iterieren und die erwarteten unterschiedlichen Werte zu erhalten). Die Fehlerdomäne von try() unterscheidet sich von gepinnten Variablen, aber es wird Auswirkungen darauf haben, wie Leute als Ergebnis Code schreiben.

IMNSHO, die xerr und Generika-Angebote brauchen 6-12 Monate Zeit, um in der Produktion zu backen, bevor sie versuchen, den Standard von if err != nil zu erobern. Generics werden wahrscheinlich den Weg für eine reichhaltigere Fehlerbehandlung und eine neue idiomatische Art der Fehlerbehandlung ebnen. Sobald die idiomatische Fehlerbehandlung mit Generika auftaucht, dann und nur dann, ist es sinnvoll, eine Diskussion über try() oder was auch immer zu wiederholen.

Ich gebe nicht vor zu wissen, wie sich Generika auf die Fehlerbehandlung auswirken, aber es scheint mir sicher, dass Generika verwendet werden, um reichhaltige Typen zu erstellen, die mit ziemlicher Sicherheit bei der Fehlerbehandlung verwendet werden. Sobald Generika die Bibliotheken durchdrungen und der Fehlerbehandlung hinzugefügt wurden, gibt es möglicherweise eine naheliegende Möglichkeit, die try() wiederzuverwenden, um die Entwicklererfahrung in Bezug auf die Fehlerbehandlung zu verbessern.

Die Bedenken, die ich habe, sind:

  1. try() ist für sich genommen nicht kompliziert, aber es ist ein kognitiver Overhead, wo vorher keiner existierte.
  2. Durch das Einbacken von err != nil in das angenommene Verhalten von try() verhindert die Sprache die Verwendung von err als Möglichkeit, den Status des Stacks nach oben zu kommunizieren.
  3. Ästhetisch fühlt sich try() wie erzwungene Cleverness an, aber nicht clever genug, um den expliziten und offensichtlichen Test zu bestehen, den die meisten Go-Sprachen genießen. Wie die meisten Dinge, die subjektive Kriterien betreffen, ist dies eine Frage des persönlichen Geschmacks und der persönlichen Erfahrung und schwer zu quantifizieren.
  4. Die Fehlerbehandlung mit switch / case Anweisungen und das Umbrechen von Fehlern scheint von diesem Vorschlag unberührt zu bleiben und eine verpasste Gelegenheit zu sein, was mich zu der Annahme veranlasst, dass dieser Vorschlag nur einen Katzensprung davon entfernt ist, ein Unbekanntes-Unbekanntes zu einem Bekannten zu machen -bekannt (oder schlimmstenfalls bekannt-unbekannt).

Schließlich fühlt sich der try() -Vorschlag wie ein neuer Dammbruch an, der eine Flut sprachspezifischer Nuancen zurückhielt, wie wir sie durch das Zurücklassen von C++ entkommen waren.

TL;DR: ist weniger eine #nevertry -Antwort als vielmehr: „Nicht jetzt, noch nicht, und lasst uns das in Zukunft noch einmal in Betracht ziehen, nachdem xerr und Generika im Ökosystem ausgereift sind. "

Das oben verlinkte #32968 ist nicht gerade ein vollständiger Gegenvorschlag, aber es baut auf meiner Meinungsverschiedenheit mit der gefährlichen Verschachtelungsfähigkeit auf, die das Makro try besitzt. Im Gegensatz zu Nr. 32946 ist dies ein ernsthafter Vorschlag, von dem ich hoffe, dass er keine ernsthaften Mängel aufweist (es gehört natürlich Ihnen, ihn zu sehen, zu bewerten und zu kommentieren). Auszug:

  • _Das Makro check ist kein Einzeiler: Es hilft am meisten, wenn sich viele wiederholen
    Überprüfungen mit demselben Ausdruck sollten in unmittelbarer Nähe durchgeführt werden._
  • _Seine implizite Version wird bereits auf Spielplatz kompiliert._

Konstruktionsbeschränkungen (erfüllt)

Es ist integriert, es ist nicht in einer einzigen Zeile verschachtelt, es ermöglicht viel mehr Flüsse als try und hat keine Erwartungen an die Form eines Codes darin. Es fördert keine nackten Renditen.

Anwendungsbeispiel

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

Hoffe, das hilft, viel Spaß!

Ich habe so viel wie möglich gelesen, um diesen Thread zu verstehen. Ich bin dafür, die Dinge so zu lassen, wie sie sind.

Meine Gründe:

  1. Ich und niemand, dem ich Go beigebracht habe, hat _jemals_ die Fehlerbehandlung nicht verstanden
  2. Ich überspringe nie eine Fehlerfalle, weil es so einfach ist, es einfach dann und dort zu tun

Vielleicht verstehe ich den Vorschlag auch falsch, aber normalerweise führt das try -Konstrukt in anderen Sprachen zu mehreren Codezeilen, die möglicherweise alle einen Fehler erzeugen können, und daher erfordern sie Fehlertypen. Zusätzliche Komplexität und oft eine Art Fehlerarchitektur und Designaufwand im Voraus.

In diesen Fällen (und ich habe das selbst getan) werden mehrere Try-Blöcke hinzugefügt. was den Code verlängert und die Implementierung überschattet.

Wenn sich die Go-Implementierung von try von der anderer Sprachen unterscheidet, wird die Verwirrung noch größer.

Mein Vorschlag ist, die Fehlerbehandlung so zu belassen, wie sie ist

Ich weiß, dass sich viele Leute eingemischt haben, aber ich möchte meine Kritik an der Spezifikation so wie sie ist hinzufügen.

Der Teil der Spezifikation, der mich am meisten stört, sind diese beiden Anfragen:

Daher schlagen wir vor, try als aufgerufene Funktion in einer go-Anweisung zu verbieten.
...
Daher schlagen wir vor, try auch als aufgerufene Funktion in einer defer-Anweisung zu verbieten.

Dies wäre die erste eingebaute Funktion, auf die dies zutrifft (Sie können sogar defer und go a panic bearbeiten), da das Ergebnis nicht verworfen werden musste. Das Erstellen einer neuen eingebauten Funktion, die vom Compiler eine besondere Berücksichtigung der Ablaufsteuerung erfordert, scheint eine große Bitte zu sein und bricht die semantische Kohärenz von go. Jedes andere Kontrollfluss-Token in go ist keine Funktion.

Ein Gegenargument zu meiner Beschwerde ist, dass es wahrscheinlich ein Unfall und nicht sehr nützlich ist, defer und go ein panic zu bekommen. Mein Punkt ist jedoch, dass die semantische Kohärenz der Funktionen in go durch diesen Vorschlag gebrochen wird, nicht dass es wichtig ist, dass defer und go immer sinnvoll zu verwenden sind. Es gibt wahrscheinlich viele nicht-integrierte Funktionen, bei denen die Verwendung defer oder go niemals sinnvoll wäre, aber semantisch gibt es keinen expliziten Grund, warum dies nicht möglich ist. Warum kann sich dieses Builtin vom semantischen Funktionsvertrag in go ausnehmen?

Ich weiß, dass @griesemer keine ästhetischen Meinungen zu diesem Vorschlag in die Diskussion einfließen lassen möchte, aber ich denke, ein Grund, warum die Leute diesen Vorschlag ästhetisch abstoßend finden, ist, dass sie spüren, dass er nicht ganz als Funktion zusammenpasst.

Der Vorschlag sagt:

Wir schlagen vor, eine neue funktionsähnliche integrierte Funktion namens try with signature (Pseudo-Code) hinzuzufügen.

func try(expr) (T1, T2, … Tn)

Außer, dass dies keine Funktion ist (was der Vorschlag grundsätzlich zulässt). Es ist effektiv ein einmaliges Makro, das in die Sprachspezifikation eingebaut ist (falls es akzeptiert werden sollte). Es gibt ein paar Probleme mit dieser Signatur.

  1. Was bedeutet es für eine Funktion, einen generischen Ausdruck als Argument zu akzeptieren, ganz zu schweigen von einem aufgerufenen Ausdruck? Jedes andere Mal, wenn das Wort "Ausdruck" in der Spezifikation verwendet wird, bedeutet es so etwas wie eine nicht aufgerufene Funktion. Wie kommt es, dass eine "aufgerufene" Funktion als Ausdruck gedacht werden kann, wenn in jedem anderen Kontext ihre Rückgabewerte das sind, was semantisch aktiv ist? Dh wir stellen uns eine aufgerufene Funktion als ihre Rückgabewerte vor. Die Ausnahmen sind bezeichnenderweise go und defer , die beide rohe Token und keine eingebauten Funktionen sind.

  2. Auch dieser Vorschlag bekommt seine eigene Funktionssignatur falsch, oder es macht zumindest keinen Sinn, die eigentliche Signatur ist:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. Der Vorschlag enthält nicht, was in Situationen passiert, in denen Versuch mit Argumenten aufgerufen wird. Was passiert, wenn try mit Argumenten aufgerufen wird:
try(arg1, arg2,..., err)

Ich denke, der Grund, warum dies nicht angesprochen wird, liegt darin, dass try versucht, ein expr -Argument zu akzeptieren, das tatsächlich eine Anzahl von n Rückgabeargumenten von einer Funktion plus etwas anderes darstellt, was die Tatsache weiter veranschaulicht dass dieser Vorschlag die semantische Kohärenz dessen, was eine Funktion ist, durchbricht.

Meine letzte Beschwerde gegen diesen Vorschlag ist, dass er die semantische Bedeutung eingebauter Funktionen weiter bricht. Ich bin nicht gleichgültig gegenüber der Idee, dass eingebaute Funktionen manchmal von den semantischen Regeln "normaler" Funktionen ausgenommen werden müssen (z. B. dass sie nicht Variablen zugewiesen werden können usw.), aber dieser Vorschlag schafft eine große Anzahl von Ausnahmen von der " normale" Regeln, die Funktionen innerhalb von Golang zu regeln scheinen.

Dieser Vorschlag macht try effektiv zu einer neuen Sache, die go noch nicht hatte, es ist nicht ganz ein Token und es ist nicht ganz eine Funktion, es ist beides, was wie ein schlechter Präzedenzfall zu sein scheint, um durchgängig semantische Kohärenz zu schaffen die Sprache.

Wenn wir ein neues Control-Flow-Ding hinzufügen wollen, behaupte ich, dass es sinnvoller ist, es zu einem rohen Token wie goto et al. zu machen. Ich weiß, wir sollten in dieser Diskussion keine Vorschläge preisgeben, aber als kurzes Beispiel denke ich, dass so etwas viel sinnvoller ist:

f, err := os.Open("/dev/stdout")
throw err

Obwohl dies eine zusätzliche Codezeile hinzufügt, denke ich, dass es jedes Problem anspricht, das ich angesprochen habe, und auch den gesamten Mangel an "alternativen" Funktionssignaturen mit try beseitigt.

edit1 : Hinweis zu Ausnahmen in den Fällen defer und go , in denen builtin nicht verwendet werden kann, da Ergebnisse ignoriert werden, während dies bei try nicht einmal wirklich der Fall sein kann sagte, dass die Funktion Ergebnisse hat.

@nathanjsweet der gesuchte Vorschlag ist #32611 :-)

@nathanjsweet Einiges von dem, was du sagst, stellt sich als nicht der Fall heraus. Die Sprache erlaubt keine Verwendung defer oder go mit den vordeklarierten Funktionen append cap complex imag len make new real . Es erlaubt auch nicht defer oder go mit den spezifikationsdefinierten Funktionen unsafe.Alignof unsafe.Offsetof unsafe.Sizeof .

Danke @nathanjsweet für deinen ausführlichen Kommentar – @ianlancetaylor hat bereits darauf hingewiesen, dass deine Argumente technisch falsch sind. Lassen Sie mich etwas erweitern:

1) Sie erwähnen, dass der Teil der Spezifikation, der try mit go und defer nicht zulässt, Sie am meisten stört, weil try das erste eingebaute wäre wo das stimmt. Das ist nicht richtig. Der Compiler erlaubt bereits zB defer append(a, 1) nicht. Dasselbe gilt für andere Einbauten, die ein Ergebnis erzeugen, das dann auf den Boden fällt. Genau diese Einschränkung würde in dieser Angelegenheit auch für try gelten (außer wenn try kein Ergebnis zurückgibt). (Der Grund, warum wir diese Einschränkungen überhaupt im Designdokument erwähnt haben, ist, so gründlich wie möglich zu sein - sie sind in der Praxis wirklich irrelevant. Wenn Sie das Designdokument genau lesen, heißt es auch nicht, dass wir try nicht verdienen können go oder defer - es suggeriert einfach, dass wir es nicht zulassen; hauptsächlich als praktische Maßnahme. Es ist eine "große Bitte" - um Ihre Worte zu verwenden - um try zu verdienen go und defer , obwohl es praktisch nutzlos ist.)

2) Sie schlagen vor, dass einige Leute try "ästhetisch abstoßend" finden, weil es technisch gesehen keine Funktion ist, und dann konzentrieren Sie sich auf die besonderen Regeln für die Signatur. Betrachten Sie new , make , append , unsafe.Offsetof : Sie alle haben spezielle Regeln, die wir nicht mit einer gewöhnlichen Go-Funktion ausdrücken können. Schauen Sie sich unsafe.Offsetof an, das genau die syntaktischen Anforderungen für sein Argument hat (es muss ein Strukturfeld sein!), die wir für das Argument für try benötigen (es muss ein einzelner Wert vom Typ sein error oder ein Funktionsaufruf, der als letztes Ergebnis error zurückgibt). Wir drücken diese Signaturen nicht formell in der Spezifikation aus, für keine dieser integrierten Funktionen, da sie nicht in den bestehenden Formalismus passen - wenn sie es täten, müssten sie nicht integriert werden. Stattdessen drücken wir ihre Regeln in Prosa aus. Das ist _warum_ sie Einbauten sind, die vom ersten Tag an die Notausstiegsluke in Go sind. Beachten Sie auch, dass das Designdokument diesbezüglich sehr explizit ist.

3) Der Vorschlag behandelt auch, was passiert, wenn try mit Argumenten (mehr als einem) aufgerufen wird: Es ist nicht erlaubt. Das Designdokument gibt ausdrücklich an, dass try einen (einen) eingehenden Argumentausdruck akzeptiert.

4) Sie erklären, dass "dieser Vorschlag die semantische Bedeutung von eingebauten Funktionen bricht". Nirgendwo schränkt Go ein, was ein eingebautes Gerät kann und was es nicht kann. Wir haben hier völlige Freiheit.

Danke.

@griesemer

Beachten Sie auch, dass das Designdokument diesbezüglich sehr explizit ist.

Können Sie darauf hinweisen. Ich war überrascht, dies zu lesen.

Sie geben an, dass "dieser Vorschlag die semantische Bedeutung von integrierten Funktionen bricht". Nirgendwo schränkt Go ein, was ein eingebautes Gerät kann und was es nicht kann. Wir haben hier völlige Freiheit.

Ich denke, das ist ein fairer Punkt. Ich denke jedoch, dass es das gibt, was in den Designdokumenten steht und was sich wie "go" anfühlt (worüber Rob Pike viel spricht). Ich denke, es ist fair zu sagen, dass der try -Vorschlag die Art und Weise erweitert, in der eingebaute Funktionen die Regeln brechen, nach denen wir erwarten, dass sich Funktionen verhalten, und ich habe zugegeben, dass ich verstehe, warum dies für andere eingebaute Funktionen notwendig ist , aber ich denke, in diesem Fall ist die Erweiterung des Regelbruchs:

  1. In mancher Hinsicht kontraintuitiv. Dies ist die erste Funktion, die die Kontrollflusslogik so ändert, dass der Stack nicht abgewickelt wird (wie es panic und os.Exit tun).
  2. Eine neue Ausnahme, wie die Aufrufkonventionen einer Funktion funktionieren. Sie haben das Beispiel von unsafe.Offsetof als einen Fall angegeben, in dem es eine syntaktische Anforderung für einen Funktionsaufruf gibt (es überrascht mich tatsächlich, dass dies einen Kompilierzeitfehler verursacht, aber das ist ein anderes Problem), aber die syntaktische Anforderung ist in diesem Fall eine andere syntaktische Anforderung als die von Ihnen angegebene. unsafe.Offsetof erfordert ein Argument, während try einen Ausdruck erfordert, der in jedem anderen Kontext wie ein von einer Funktion zurückgegebener Wert aussehen würde (dh try(os.Open("/dev/stdout")) ) und sicher angenommen werden könnte in jedem anderen Kontext, um nur einen Wert zurückzugeben (es sei denn, der Ausdruck sah aus wie try(os.Open("/dev/stdout")...) ).

@nathanjsweet schrieb:

Beachten Sie auch, dass das Designdokument diesbezüglich sehr explizit ist.

Können Sie darauf hinweisen. Ich war überrascht, dies zu lesen.

Es steht im Abschnitt "Schlussfolgerungen" des Vorschlags:

In Go sind eingebaute Funktionen der bevorzugte Sprach-Escape-Mechanismus für Operationen, die in gewisser Weise unregelmäßig sind, aber keine spezielle Syntax rechtfertigen.

Ich bin überrascht, dass du es verpasst hast ;-)

@ngrilly Ich meine nicht in diesem Vorschlag, ich meine in der Go-Sprachspezifikation. Ich hatte den Eindruck, dass @griesemer sagte, dass die Go-Sprachspezifikation eingebaute Funktionen als besonders nützlichen Mechanismus zum Brechen syntaktischer Konventionen aufruft.

@nathanjsüß

In mancher Hinsicht kontraintuitiv. Dies ist die erste Funktion, die die Kontrollflusslogik so ändert, dass der Stack nicht abgewickelt wird (wie es Panik und os.Exit tun).

Ich glaube nicht, dass os.Exit den Stapel in irgendeiner sinnvollen Weise abwickelt. Es beendet das Programm sofort, ohne irgendwelche zurückgestellten Funktionen auszuführen. Es scheint mir, dass os.Exit hier draußen das Seltsame ist, da sowohl panic als auch try verzögerte Funktionen ausführen und den Stack nach oben wandern.

Ich stimme zu, dass os.Exit ein Sonderfall ist, aber es muss so sein. os.Exit stoppt alle Goroutinen; Es würde keinen Sinn machen, nur die zurückgestellten Funktionen nur der Goroutine auszuführen, die os.Exit aufruft. Es sollte entweder alle zurückgestellten Funktionen ausführen oder keine. Und es ist viel viel einfacher, keine auszuführen.

tryhard auf unserer Codebasis ausgeführt und das haben wir bekommen:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

Zuerst möchte ich klarstellen, dass wir, weil Go (vor 1.13) keinen Kontext in Fehlern hat, unseren eigenen Fehlertyp implementiert haben, der die error -Schnittstelle implementiert, einige Funktionen so deklariert sind, dass sie foo.Error anstelle von error zurückgeben

Ich war im Lager von "Ja! Machen wir das", und ich denke, es wird ein interessantes Experiment für 1.13- oder 1.14- Betas , aber ich bin besorgt über die _" 47,7 % ... Kandidaten ausprobieren"_. Es bedeutet jetzt, dass es zwei Möglichkeiten gibt, Dinge zu tun, die ich nicht mag. Es gibt jedoch auch 2 Möglichkeiten, einen Zeiger zu erstellen ( new(Foo) vs. &Foo{} ) sowie 2 Möglichkeiten, ein Slice oder eine Map mit make([]Foo) und []Foo{} zu erstellen .

Jetzt bin ich auf dem Lager von "let's _try_ this" :^) und schaue, was die Community denkt. Vielleicht werden wir unsere Codierungsmuster ändern, um faul zu sein und keinen Kontext mehr hinzuzufügen, aber vielleicht ist das in Ordnung, wenn Fehler durch das xerrors Impl, das sowieso kommt, einen besseren Kontext bekommen.

Danke @Goodwine für die Bereitstellung konkreterer Daten!

(Nebenbei habe ich letzte Nacht eine kleine Änderung an tryhard vorgenommen, sodass die Anzahl der "komplexen Fehlerbehandlungsroutinen" in zwei Zählungen aufgeteilt wird: komplexe Behandlungsroutinen und Rückgaben der Form return ..., expr , wobei die letzte ist Der Ergebniswert ist nicht <err> . Dies sollte zusätzliche Einblicke geben.)

Wie wäre es, den Vorschlag so zu ändern, dass er variadisch ist, anstatt dieses seltsame Ausdrucksargument?

Das würde viele Probleme lösen. In dem Fall, in dem die Leute nur den Fehler zurückgeben wollten, würde sich nur das explizite Variadic ... ändern. Z.B:

try(os.Open("/dev/stdout")...)

Leute, die eine flexiblere Situation wünschen, können jedoch Folgendes tun:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

Eine Sache, die diese Idee bewirkt, ist, dass das Wort try weniger angemessen ist, aber die Abwärtskompatibilität bleibt erhalten.

@nathanjsweet schrieb:

Ich meine nicht in diesem Vorschlag, ich meine in der Go-Sprachspezifikation.

Hier sind die Auszüge, nach denen Sie in der Sprachspezifikation gesucht haben:

Im Abschnitt "Ausdrucksanweisungen":

Die folgenden integrierten Funktionen sind im Anweisungskontext nicht zulässig: append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

In den Abschnitten „Go-Anweisungen“ und „Defer-Anweisungen“:

Aufrufe eingebauter Funktionen sind wie bei Ausdrucksanweisungen eingeschränkt.

Im Abschnitt „Integrierte Funktionen“:

Die eingebauten Funktionen haben keine Standard-Go-Typen, daher können sie nur in Aufrufausdrücken erscheinen; sie können nicht als Funktionswerte verwendet werden.

@nathanjsweet schrieb:

Ich hatte den Eindruck, dass @griesemer sagte, dass die Go-Sprachspezifikation eingebaute Funktionen als den besonders nützlichen Mechanismus zum Brechen von syntaktischen Konventionen aufruft.

Eingebaute Funktionen brechen nicht die syntaktischen Konventionen von Go (Klammern, Kommas zwischen Argumenten usw.). Sie verwenden dieselbe Syntax wie benutzerdefinierte Funktionen, erlauben jedoch Dinge, die in benutzerdefinierten Funktionen nicht möglich sind.

@nathanjsweet Das wurde bereits in Betracht gezogen (tatsächlich war es ein Versehen), aber es macht try nicht erweiterbar. Siehe https://go-review.googlesource.com/c/proposal/+/181878 .

Allgemein denke ich, dass Sie Ihre Kritik auf das Falsche konzentrieren: Die Sonderregeln für das try -Argument sind wirklich kein Problem - praktisch jedes eingebaute hat Sonderregeln.

@griesemer danke, dass du daran gearbeitet und dir die Zeit genommen hast, auf die Bedenken der Community zu reagieren. Ich bin sicher, dass Sie an dieser Stelle auf viele der gleichen Fragen geantwortet haben. Mir ist klar, dass es wirklich schwierig ist, diese Probleme zu lösen und gleichzeitig die Abwärtskompatibilität aufrechtzuerhalten. Danke!

@nathanjsweet Zu deinem Kommentar hier :

Sehen Sie sich den Abschnitt „ Schlussfolgerung “ an, in dem die Rolle von integrierten Funktionen in Go prominent angesprochen wird.

In Bezug auf Ihre Kommentare zu try Erweiterung der eingebauten Funktionen auf unterschiedliche Weise: Ja, die Anforderung, die unsafe.Offsetof an sein Argument stellt, ist eine andere als die von try . Aber beide erwarten syntaktisch einen Ausdruck. Beide haben einige zusätzliche Einschränkungen für diesen Ausdruck. Die Anforderung für try passt so einfach in die Go-Syntax, dass keines der Front-End-Parsing-Tools angepasst werden muss. Ich verstehe, dass es sich für Sie ungewöhnlich anfühlt, aber das ist nicht dasselbe wie ein technischer Grund dagegen.

@griesemer das neueste _tryhard_ zählt "komplexe Fehlerhandler", aber keine "Fehlerhandler mit einer einzigen Anweisung". Könnte man das hinzufügen?

@networkimprov Was ist ein Single-Statement-Fehlerbehandler? Ein if -Block, der eine einzelne Non-Return-Anweisung enthält?

@griesemer , ein Single-Statement-Fehlerbehandler ist ein if err != nil -Block, der _jede_ einzelne Anweisung enthält, einschließlich einer Rückgabe.

@networkimprov Fertig. "Komplexe Handler" werden jetzt in "Einzelne Anweisung, dann Verzweigung" und "Komplexe, dann Verzweigung" unterteilt.

Beachten Sie jedoch, dass diese Zählwerte irreführend sein können: Beispielsweise enthalten diese Zählwerte jede if -Anweisung, die eine beliebige Variable gegen Null prüft (wenn -err="" , was jetzt die Standardeinstellung für tryhard ist, tryhard überschätzt die Anzahl der Möglichkeiten für komplexe oder Einzelanweisungs-Handler um ein Vielfaches. Ein Beispiel finden Sie unter archive/tar/common.go , Zeile 701.

@networkimprov tryhard liefert jetzt genauere Zahlen dazu, warum eine Fehlerprüfung kein try Kandidat ist. Die Gesamtzahl der try Zählungen ist unverändert, aber die Anzahl der Gelegenheiten für mehr einzelne und komplexe Handler ist jetzt genauer (und ungefähr 50 % kleiner als zuvor, da vor allen komplexen then -Zweig einer if -Anweisung wurde berücksichtigt, solange if eine <varname> != nil -Prüfung enthielt, unabhängig davon, ob es sich um eine Fehlerprüfung handelte oder nicht).

Wenn jemand try etwas praktischer ausprobieren möchte, habe ich hier einen WASM-Spielplatz mit einer Prototyp-Implementierung erstellt:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

Und falls jemand daran interessiert ist, Code lokal mit try zu kompilieren, habe ich hier einen Go-Fork mit einer meiner Meinung nach voll funktionsfähigen / aktuellen Implementierung: https://github.com/ccbrown/go/pull/1

Ich mag "versuchen". Ich empfinde die Verwaltung des lokalen Fehlerzustands und die Verwendung von := vs = with err zusammen mit den zugehörigen Importen als regelmäßige Ablenkung. Außerdem sehe ich dies nicht als Schaffung von zwei Möglichkeiten, dasselbe zu tun, eher wie zwei Fälle, einen, in dem Sie einen Fehler weitergeben möchten, ohne darauf zu reagieren, und den anderen, in dem Sie ihn explizit in der aufrufenden Funktion behandeln möchten z.B. Protokollierung.

Ich habe tryhard gegen ein kleines internes Projekt laufen lassen, an dem ich vor über einem Jahr gearbeitet habe. Das fragliche Verzeichnis enthält den Code für 3 Server ("Microservices", nehme ich an), einen Crawler, der regelmäßig als Cron-Job ausgeführt wird, und einige Befehlszeilentools. Es hat auch ziemlich umfassende Unit-Tests. (FWIW, die verschiedenen Teile laufen seit über einem Jahr reibungslos, und es hat sich als unkompliziert erwiesen, auftretende Probleme zu debuggen und zu lösen.)

Hier sind die Statistiken:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

Einige Kommentare:
1) 50 % aller if -Anweisungen in dieser Codebasis führten eine Fehlerprüfung durch, und try könnte etwa die Hälfte davon ersetzen. Das bedeutet, dass ein Viertel aller if -Anweisungen in dieser (kleinen) Codebasis eine getippte Version von try sind.

2) Ich sollte anmerken, dass dies für mich überraschend hoch ist, da ich einige Wochen vor Beginn dieses Projekts zufällig von einer Familie interner Hilfsfunktionen ( status.Annotate ) gelesen habe, die eine Fehlermeldung kommentieren, aber beibehalten gRPC-Statuscode. Wenn Sie beispielsweise einen RPC aufrufen und dieser einen Fehler mit dem zugeordneten Statuscode PERMISSION_DENIED zurückgibt, hätte der von dieser Hilfsfunktion zurückgegebene Fehler immer noch den zugeordneten Statuscode PERMISSION_DENIED (und theoretisch, wenn dieser zugeordnete Statuscode alle weitergegeben wurde bis hin zu einem RPC-Handler, dann würde der RPC mit diesem zugehörigen Statuscode fehlschlagen). Ich hatte mir vorgenommen, diese Funktionen für alles an diesem neuen Projekt zu verwenden. Aber anscheinend habe ich bei 50 % aller Fehler einfach einen Fehler ohne Anmerkung weitergegeben. (Bevor ich tryhard ausgeführt habe, hatte ich 10% vorhergesagt).

3) status.Annotate behält nil Fehler bei (dh status.Annotatef(err, "some message: %v", x) gibt nil zurück, wenn err == nil ). Ich habe alle Nicht-Versuchskandidaten der ersten Kategorie durchgesehen, und es scheint, als wären alle für die folgende Umschreibung zugänglich:

```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)                                                     
if err != nil {                                                                                    
  return status.Annotate(err, "failed to create encrypter")                                        
}

// After
enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
try(status.Annotate(err, "failed to create encrypter"))
```

To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.

4) defer -basierte Fehleranmerkung scheint etwas orthogonal zu try zu sein, um ehrlich zu sein, da sie mit und ohne try funktioniert. Aber als ich den Code für dieses Projekt durchgesehen habe, sind mir, da ich mir die Fehlerbehandlung genau angeschaut habe, zufällig mehrere Fälle aufgefallen, in denen vom Aufrufer generierte Fehler sinnvoller wären. Als ein Beispiel sind mir mehrere Instanzen von Code aufgefallen, die gRPC-Clients wie folgt aufrufen:

```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
  return ..., status.Annotate(err, "failed to call SomeMethod")
}
```

This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

There was also code that called standard library functions using the same pattern:

```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
  return status.Annotate(err, "http.NewRequest failed")
}
```

In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

Auf jeden Fall hielt ich es für eine interessante Übung, auf dieses Projekt zurückzukommen und mir genau anzusehen, wie es mit Fehlern umgeht.

Eine Sache, @griesemer : hat tryhard den richtigen Nenner für "Non-try-Kandidaten"?
Bearbeiten: unten beantwortet, ich habe die Statistiken falsch gelesen.

EDIT: Was als Feedback gedacht war, wurde zu einem Vorschlag, den wir hier ausdrücklich nicht tun sollten. Ich habe meinen Kommentar in einen Kern verschoben .

@balasanjay Danke für deinen faktenbasierten Kommentar; das ist sehr hilfreich.

Zu Ihrer Frage zu tryhard : Die "Non-try-Kandidaten" (besserer Titelvorschlag willkommen) sind einfach die Anzahl der Fälle, in denen die if -Anweisung alle Kriterien für eine "Fehlerprüfung" erfüllt hat (dh , hatten wir etwas, das wie eine Zuweisung an eine Fehlervariable <err> aussah, gefolgt von einer if <err> != nil Prüfung in der Quelle), aber wo wir try wegen nicht einfach verwenden können den Code in den Blöcken if . Insbesondere sind dies in der Reihenfolge ihres Erscheinens in der Ausgabe der „Non-try-Kandidaten“ if -Anweisungen, die eine return -Anweisung haben, die am Ende etwas anderes als <err> zurückgibt, if -Anweisungen mit einer einzigen, komplexeren return (oder anderen) Anweisung, if -Anweisungen mit mehreren Anweisungen in der "dann"-Verzweigung und if -Anweisungen mit nicht leerer else -Zweig. Bei einigen dieser if -Anweisungen können mehrere dieser Bedingungen gleichzeitig erfüllt sein, sodass sich diese Zahlen nicht einfach addieren. Sie sollen eine Vorstellung davon vermitteln, was schief gelaufen ist, damit try verwendet werden kann.

Ich habe heute die letzten Anpassungen daran vorgenommen (Sie haben die neueste Version ausgeführt). Vor der letzten Änderung wurden einige dieser Bedingungen auch dann gezählt, wenn keine Fehlerprüfung beteiligt war, was weniger Sinn zu machen schien, da es so aussah, als ob try nicht in vielen Fällen mehr verwendet werden könnte, obwohl $# tatsächlich try machten in diesen Fällen überhaupt keinen Sinn.

Am wichtigsten ist jedoch, dass sich für eine bestimmte Codebasis die Gesamtzahl der try -Kandidaten mit diesen Verfeinerungen nicht geändert hat, da die relevanten Bedingungen für try gleich geblieben sind.

Wenn Sie einen besseren Vorschlag haben, wie und/oder was zu messen ist, würde ich mich freuen, das zu hören. Ich habe mehrere Anpassungen basierend auf Community-Feedback vorgenommen. Danke.

@subfuzion Vielen Dank für Ihren Kommentar, aber wir suchen nicht nach alternativen Vorschlägen. Siehe https://github.com/golang/go/issues/32437#issuecomment -501878888 . Danke.

Im Interesse der Zählung, unabhängig vom Ergebnis:

Zusammen mit meinem Team bin ich der Ansicht, dass das try -Framework, wie es von Rob vorgeschlagen wird, zwar eine vernünftige und interessante Idee ist, aber nicht das Niveau erreicht, auf dem es als Built-in angemessen wäre. Ein Standard-Bibliothekspaket wäre ein viel geeigneterer Ansatz, bis sich Nutzungsmuster in der Praxis etabliert haben. Wenn try auf diese Weise in die Sprache käme, würden wir es an vielen verschiedenen Stellen verwenden.

Generell ist Gos Kombination aus einer sehr stabilen Kernsprache und einer sehr umfangreichen Standardbibliothek es wert, erhalten zu werden. Je langsamer das Sprachteam bei Kernsprachänderungen vorgeht, desto besser. Die x -> stdlib -Pipeline bleibt ein starker Ansatz für solche Dinge.

@griesemer Ah, tut mir leid. Ich habe die Statistiken falsch gelesen, es wird der Zähler "if err != nil Statements" (123) als Nenner verwendet, nicht der Zähler "try Candidates" (64) als Nenner. Ich werde diese Frage streichen.

Danke!

@mattpalmer Nutzungsmuster haben sich seit etwa einem Jahrzehnt etabliert. Genau diese Nutzungsmuster haben das Design von try direkt beeinflusst. Welche Nutzungsmuster meinen Sie?

@griesemer Tut mir leid, das ist meine Schuld - was in meinem Kopf damit begann, zu erklären, was mich an try störte, entwickelte sich zu einem eigenen Vorschlag, um meinen Standpunkt darzulegen, es nicht hinzuzufügen. Das war eindeutig gegen die angegebenen Grundregeln (ganz zu schweigen davon, dass im Gegensatz zu diesem Vorschlag für eine neue eingebaute Funktion ein neuer Operator eingeführt wird). Wäre es hilfreich, den Kommentar zu löschen, um die Konversation übersichtlich zu halten (oder wird das als schlechter Stil angesehen)?

@subfuzion Ich würde mir keine Sorgen machen. Es ist ein kontroverser Vorschlag und es gibt viele Vorschläge. Viele sind ausgefallen

Wir haben dieses Design mehrmals wiederholt und Feedback von vielen Leuten eingeholt, bevor wir uns sicher genug fühlten, es zu veröffentlichen und zu empfehlen, es in die eigentliche Experimentphase zu bringen, aber wir haben das Experiment noch nicht durchgeführt. Es macht durchaus Sinn, zurück ans Reißbrett zu gehen, wenn das Experiment scheitert, oder uns das Feedback schon im Voraus sagt, dass es eindeutig scheitern wird.

@griesemer können Sie die spezifischen Metriken erläutern, die das Team verwenden wird, um den Erfolg oder Misserfolg des Experiments festzustellen?

@Ich und

Ich habe @rsc vor einiger Zeit gefragt (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@rsc
Es wird keinen Mangel an Orten geben, an denen dieser Komfort platziert werden kann. Welche Metrik wird gesucht, die darüber hinaus die Substanz des Mechanismus beweist? Gibt es eine Liste klassifizierter Fehlerbehandlungsfälle? Wie wird der Wert aus den Daten abgeleitet, wenn ein Großteil des öffentlichen Prozesses von Stimmungen bestimmt wird?

Die Antwort war beabsichtigt, aber wenig inspirierend und ohne Substanz (https://github.com/golang/go/issues/32437#issuecomment-503295558):

Die Entscheidung basiert darauf, wie gut dies in realen Programmen funktioniert. Wenn uns Leute zeigen, dass try im Großteil ihres Codes wirkungslos ist, sind das wichtige Daten. Der Prozess wird von dieser Art von Daten angetrieben. Es ist nicht von Gefühlen getrieben.

Zusätzliches Sentiment wurde angeboten (https://github.com/golang/go/issues/32437#issuecomment-503408184):

Ich war überrascht, einen Fall zu finden, in dem try zu deutlich besserem Code führte, auf eine Weise, die vorher nicht diskutiert worden war.

Schließlich beantwortete ich meine eigene Frage: „Gibt es eine Liste klassifizierter Fehlerbehandlungsfälle?“. Es wird effektiv 6 Modi der Fehlerbehandlung geben – Manuell direkt, Manuell Pass-Through, Manuell Indirekt, Automatisch Direkt, Automatisch Pass-Through, Automatisch Indirekt. Derzeit ist es üblich, nur 2 dieser Modi zu verwenden. Die indirekten Modi, die einen erheblichen Aufwand in ihre Erleichterung stecken, erscheinen den meisten erfahrenen Gophers stark unerschwinglich, und diese Besorgnis wird anscheinend ignoriert. (https://github.com/golang/go/issues/32437#issuecomment-507332843).

Außerdem habe ich vorgeschlagen, dass automatisierte Transformationen vor der Transformation überprüft werden, um zu versuchen, den Wert der Ergebnisse sicherzustellen (https://github.com/golang/go/issues/32437#issuecomment-507497656). Glücklicherweise scheinen im Laufe der Zeit mehr der angebotenen Ergebnisse bessere Retrospektiven zu haben, aber dies geht immer noch nicht nüchtern und konzertiert auf die Auswirkungen der indirekten Methoden ein. Schließlich sollten Entwickler (meiner Meinung nach) genauso wie Benutzer als feindselig behandelt werden sollten, als faul behandelt werden.

Es wurde auch auf das Versagen des derzeitigen Ansatzes hingewiesen, wertvolle Kandidaten zu verpassen (https://github.com/golang/go/issues/32437#issuecomment-507505243).

Ich denke, es lohnt sich, laut darüber zu sein, dass dieser Prozess im Allgemeinen fehlt und vor allem unmusikalisch ist.

@iand Die Antwort von @rsc ist immer noch gültig. Ich bin mir nicht sicher, welcher Teil dieser Antwort „ohne Substanz“ ist oder was es braucht, um „inspirierend“ zu sein. Aber lassen Sie mich versuchen, mehr "Substanz" hinzuzufügen:

Der Zweck des Vorschlagsbewertungsprozesses besteht darin, letztendlich festzustellen, „ob eine Änderung den erwarteten Nutzen gebracht oder unerwartete Kosten verursacht hat“ (Schritt 5 im Prozess).

Wir haben Schritt 1 bestanden: Das Go-Team hat konkrete Vorschläge ausgewählt, die es wert erscheinen, angenommen zu werden; dieser Vorschlag ist einer davon. Wir hätten es nicht ausgewählt, wenn wir nicht gründlich darüber nachgedacht und es für sinnvoll befunden hätten. Insbesondere glauben wir, dass es im Go-Code eine beträchtliche Menge an Boilerplates gibt, die sich ausschließlich auf die Fehlerbehandlung beziehen. Der Vorschlag kommt auch nicht aus dem Nichts – wir diskutieren das seit über einem Jahr in verschiedenen Formen.

Aktuell sind wir bei Stufe 2, also noch ein ganzes Stück von einer endgültigen Entscheidung entfernt. Schritt 2 dient dem Sammeln von Feedback und Bedenken – von denen es anscheinend viele gibt. Aber um es hier klarzustellen: Bisher gab es nur einen einzigen Kommentar, der auf einen _technischen_ Mangel am Design hinwies, den wir korrigiert haben. Es gab auch einige Kommentare mit konkreten Daten auf der Grundlage von echtem Code, die darauf hindeuteten, dass try tatsächlich Boilerplates reduzieren und Code vereinfachen würde; und es gab ein paar Kommentare - auch basierend auf Daten zu echtem Code - die zeigten, dass try nicht viel helfen würde. Ein solches konkretes Feedback, das auf tatsächlichen Daten basiert oder auf technische Mängel hinweist, ist umsetzbar und sehr hilfreich. Wir werden dies unbedingt berücksichtigen.

Und dann war da noch die große Menge an Kommentaren, die im Wesentlichen persönliche Gefühle sind. Das ist weniger umsetzbar. Das soll nicht heißen, dass wir es ignorieren. Aber nur weil wir uns an den Prozess halten, heißt das nicht, dass wir „taub“ sind.

Zu diesen Kommentaren: Es gibt vielleicht zwei, vielleicht drei Dutzend lautstarke Gegner dieses Vorschlags – Sie wissen, wer Sie sind. Sie dominieren diese Diskussion mit häufigen Beiträgen, manchmal mehrmals am Tag. Daraus lassen sich kaum neue Erkenntnisse gewinnen. Die gestiegene Anzahl an Beiträgen spiegelt auch keine "stärkere" Stimmung der Community wider; es bedeutet nur, dass diese Leute lauter sind als andere.

@iand Die Antwort von @rsc ist immer noch gültig. Ich bin mir nicht sicher, welcher Teil dieser Antwort „ohne Substanz“ ist oder was es braucht, um „inspirierend“ zu sein. Aber lassen Sie mich versuchen, mehr "Substanz" hinzuzufügen:

@griesemer Ich bin mir sicher, dass es unbeabsichtigt war, aber ich möchte darauf hinweisen, dass keines der von Ihnen zitierten Wörter von mir stammt, sondern von einem späteren Kommentator.

Abgesehen davon hoffe ich, dass der Erfolg von try neben der Reduzierung von Boilerplates und der Vereinfachung auch danach beurteilt wird, ob es uns erlaubt, besseren und klareren Code zu schreiben.

@iand Tatsächlich - das war nur ein Versehen von mir. Entschuldigen Sie.

Wir glauben, dass try es uns ermöglicht, besser lesbaren Code zu schreiben – und viele der Beweise, die wir von echtem Code und unseren eigenen Experimenten mit tryhard erhalten haben, zeigen signifikante Aufräumarbeiten. Aber die Lesbarkeit ist subjektiver und schwieriger zu quantifizieren.

@griesemer

Welche Nutzungsmuster meinen Sie?

Ich beziehe mich auf die Nutzungsmuster, die sich im Laufe der Zeit um try entwickeln werden, nicht auf das bestehende Null-Prüfungs-Muster für die Behandlung von Fehlern. Das Potenzial für Missbrauch und Missbrauch ist eine große Unbekannte, insbesondere angesichts des anhaltenden Zustroms von Programmierern, die semantisch unterschiedliche Versionen von try-catch in anderen Sprachen verwendet haben.

All dies und die Überlegungen zur Langzeitstabilität der Kernsprache führen mich zu der Annahme, dass die Einführung dieses Features auf der Ebene der x-Pakete oder der Standardbibliothek (entweder als Paket errors/try oder als errors.Try() ) wäre es vorzuziehen, es als Builtin einzuführen.

@mattparlmer Korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, dieser Vorschlag müsste sich in der Go-Laufzeit befinden, um g's, m's zu verwenden (notwendig, um den Ausführungsfluss zu überschreiben).

@fabian-f

@mattparlmer Korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, dieser Vorschlag müsste sich in der Go-Laufzeit befinden, um g's, m's zu verwenden (notwendig, um den Ausführungsfluss zu überschreiben).

Das ist nicht der Fall; Wie das Designdokument feststellt, ist es als Syntaxbaumtransformation zur Kompilierzeit implementierbar.

Das ist möglich, weil die Semantik von try vollständig in Form von if und return ausgedrückt werden kann; es "überschreibt den Ausführungsfluss" nicht wirklich mehr als if und return .

Hier ist ein tryhard Bericht aus der Go-Codebasis mit 300.000 Zeilen meines Unternehmens:

Erstlauf:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Wir haben eine Konvention, das errgo-Paket von juju (https://godoc.org/github.com/juju/errgo) zu verwenden, um Fehler zu maskieren und ihnen Stack-Trace-Informationen hinzuzufügen, was die meisten Umschreibungen verhindern würde. Das bedeutet, dass wir try wahrscheinlich nicht übernehmen werden, aus dem gleichen Grund, aus dem wir im Allgemeinen nackte Fehlermeldungen meiden.

Da dies eine hilfreiche Metrik zu sein scheint, habe ich errgo.Mask() Aufrufe (die den Fehler ohne Anmerkung zurückgeben) entfernt und tryhard erneut ausgeführt. Dies ist eine Schätzung, wie viele Fehlerprüfungen neu geschrieben werden könnten, wenn wir errgo nicht verwenden würden:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Ich denke also, dass ~70% der Fehlerrückgaben ansonsten mit try kompatibel wären.

Schließlich scheint meine Hauptbesorgnis über den Vorschlag weder in den Kommentaren, die ich gelesen habe, noch in den Diskussionszusammenfassungen erfasst zu sein:

Dieser Vorschlag erhöht die relativen Kosten der Kommentierung von Fehlern erheblich.

Gegenwärtig sind die Grenzkosten für das Hinzufügen von Kontext zu einem Fehler sehr gering; es ist kaum mehr als die Eingabe des Formatstrings. Wenn dieser Vorschlag angenommen würde, befürchte ich, dass Ingenieure zunehmend die Ästhetik von try bevorzugen würden, sowohl weil ihr Code dadurch "schlanker aussieht" (was leider für einige Leute eine Überlegung ist, nach meiner Erfahrung) und erfordert jetzt einen zusätzlichen Block, um Kontext hinzuzufügen. Sie könnten es mit einem „Lesbarkeits“-Argument rechtfertigen, wie das Hinzufügen von Kontext die Methode um weitere 3 Zeilen erweitert und den Leser vom Hauptpunkt ablenkt. Ich denke, dass Unternehmenscodebasen anders als die Go-Standardbibliothek sind, da es wahrscheinlich einen messbaren Einfluss auf die resultierende Codequalität hat, es einfach zu machen, das Richtige zu tun, Codeüberprüfungen von unterschiedlicher Qualität sind und Teampraktiken unabhängig voneinander variieren . Wie auch immer, wie Sie bereits sagten, konnten wir try nicht immer für unsere Codebasis übernehmen.

Danke für die Überlegung

@mattparlmer

All dies und die Überlegungen zur Langzeitstabilität der Kernsprache führen mich zu der Annahme, dass die Einführung dieses Features auf der Ebene der x-Pakete oder der Standardbibliothek (entweder als Paket errors/try oder als errors.Try() ) wäre es vorzuziehen, es als Builtin einzuführen.

try kann nicht als Bibliotheksfunktion implementiert werden; Es gibt keine Möglichkeit für eine Funktion, von ihrem Aufrufer zurückzukehren (die Aktivierung wurde als #32473 vorgeschlagen), und wie bei den meisten anderen integrierten Funktionen gibt es auch keine Möglichkeit, die Signatur von try in Go auszudrücken. Auch bei Generika dürfte das kaum möglich sein; siehe Design-Doc FAQ , gegen Ende.

Außerdem würde die Implementierung try als Bibliotheksfunktion einen ausführlicheren Namen erfordern, was den Sinn der Verwendung teilweise zunichte macht.

Es kann jedoch – und wurde zweimal – als Quellcode-Präprozessor implementiert werden: siehe https://github.com/rhysd/trygo und https://github.com/lunixbochs/og.

Es sieht so aus, als ob ~ 60 % der Codebasis von Tegola diese Funktion nutzen könnten.

Hier ist die Ausgabe von tryhard für das Tegola-Projekt: (http://github.com/go-spatial/tegola)

--- try candidates ---
      1  tegola/atlas/atlas.go:84
      2  tegola/atlas/map.go:232
      3  tegola/atlas/map.go:238
      4  tegola/atlas/map.go:248
      5  tegola/atlas/map.go:253
      6  tegola/basic/geometry_math.go:248
      7  tegola/basic/geometry_math.go:251
      8  tegola/basic/geometry_math.go:268
      9  tegola/basic/geometry_math.go:276
     10  tegola/basic/json_marshal.go:33
     11  tegola/basic/json_marshal.go:153
     12  tegola/basic/json_marshal.go:276
     13  tegola/cache/azblob/azblob.go:54
     14  tegola/cache/azblob/azblob.go:61
     15  tegola/cache/azblob/azblob.go:67
     16  tegola/cache/azblob/azblob.go:74
     17  tegola/cache/azblob/azblob.go:80
     18  tegola/cache/azblob/azblob.go:105
     19  tegola/cache/azblob/azblob.go:109
     20  tegola/cache/azblob/azblob.go:204
     21  tegola/cache/azblob/azblob.go:259
     22  tegola/cache/file/file.go:42
     23  tegola/cache/file/file.go:56
     24  tegola/cache/file/file.go:110
     25  tegola/cache/file/file.go:116
     26  tegola/cache/file/file.go:129
     27  tegola/cache/redis/redis.go:41
     28  tegola/cache/redis/redis.go:46
     29  tegola/cache/redis/redis.go:51
     30  tegola/cache/redis/redis.go:56
     31  tegola/cache/redis/redis.go:70
     32  tegola/cache/redis/redis.go:79
     33  tegola/cache/redis/redis.go:84
     34  tegola/cache/s3/s3.go:85
     35  tegola/cache/s3/s3.go:102
     36  tegola/cache/s3/s3.go:112
     37  tegola/cache/s3/s3.go:118
     38  tegola/cache/s3/s3.go:123
     39  tegola/cache/s3/s3.go:138
     40  tegola/cache/s3/s3.go:164
     41  tegola/cache/s3/s3.go:172
     42  tegola/cache/s3/s3.go:179
     43  tegola/cache/s3/s3.go:284
     44  tegola/cache/s3/s3.go:340
     45  tegola/cmd/tegola/cmd/cache/format.go:97
     46  tegola/cmd/tegola/cmd/cache/seed_purge.go:94
     47  tegola/cmd/tegola/cmd/cache/seed_purge.go:103
     48  tegola/cmd/tegola/cmd/cache/seed_purge.go:170
     49  tegola/cmd/tegola/cmd/cache/tile_list.go:51
     50  tegola/cmd/tegola/cmd/cache/tile_list.go:64
     51  tegola/cmd/tegola/cmd/cache/tile_name.go:35
     52  tegola/cmd/tegola/cmd/cache/tile_name.go:43
     53  tegola/cmd/tegola/cmd/root.go:58
     54  tegola/cmd/tegola/cmd/root.go:61
     55  tegola/cmd/xyz2svg/cmd/draw.go:62
     56  tegola/cmd/xyz2svg/cmd/draw.go:70
     57  tegola/cmd/xyz2svg/cmd/draw.go:214
     58  tegola/config/config.go:96
     59  tegola/internal/env/parse.go:30
     60  tegola/internal/env/parse.go:69
     61  tegola/internal/env/parse.go:116
     62  tegola/internal/env/parse.go:174
     63  tegola/internal/env/parse.go:221
     64  tegola/internal/env/types.go:67
     65  tegola/internal/env/types.go:86
     66  tegola/internal/env/types.go:105
     67  tegola/internal/env/types.go:124
     68  tegola/internal/env/types.go:143
     69  tegola/maths/makevalid/main.go:189
     70  tegola/maths/makevalid/main.go:207
     71  tegola/maths/makevalid/main.go:221
     72  tegola/maths/makevalid/main.go:295
     73  tegola/maths/makevalid/main.go:504
     74  tegola/maths/makevalid/makevalid.go:77
     75  tegola/maths/makevalid/makevalid.go:89
     76  tegola/maths/makevalid/makevalid.go:118
     77  tegola/maths/makevalid/makevalid_test.go:93
     78  tegola/maths/makevalid/makevalid_test.go:163
     79  tegola/maths/makevalid/plyg/ring.go:518
     80  tegola/maths/triangle.go:1023
     81  tegola/mvt/layer.go:73
     82  tegola/mvt/layer.go:79
     83  tegola/mvt/vector_tile/vector_tile.pb.go:64
     84  tegola/provider/gpkg/gpkg.go:138
     85  tegola/provider/gpkg/gpkg.go:223
     86  tegola/provider/gpkg/gpkg_register.go:46
     87  tegola/provider/gpkg/gpkg_register.go:51
     88  tegola/provider/gpkg/gpkg_register.go:186
     89  tegola/provider/gpkg/gpkg_register.go:227
     90  tegola/provider/gpkg/gpkg_register.go:240
     91  tegola/provider/gpkg/gpkg_register.go:245
     92  tegola/provider/gpkg/gpkg_register.go:256
     93  tegola/provider/gpkg/gpkg_register.go:377
     94  tegola/provider/postgis/postgis.go:112
     95  tegola/provider/postgis/postgis.go:117
     96  tegola/provider/postgis/postgis.go:122
     97  tegola/provider/postgis/postgis.go:127
     98  tegola/provider/postgis/postgis.go:136
     99  tegola/provider/postgis/postgis.go:142
    100  tegola/provider/postgis/postgis.go:148
    101  tegola/provider/postgis/postgis.go:153
    102  tegola/provider/postgis/postgis.go:158
    103  tegola/provider/postgis/postgis.go:163
    104  tegola/provider/postgis/postgis.go:181
    105  tegola/provider/postgis/postgis.go:198
    106  tegola/provider/postgis/postgis.go:264
    107  tegola/provider/postgis/postgis.go:441
    108  tegola/provider/postgis/postgis.go:446
    109  tegola/provider/postgis/postgis.go:529
    110  tegola/provider/postgis/postgis.go:559
    111  tegola/provider/postgis/postgis.go:603
    112  tegola/provider/postgis/util.go:31
    113  tegola/provider/postgis/util.go:36
    114  tegola/provider/postgis/util.go:200
    115  tegola/server/bindata/bindata.go:89
    116  tegola/server/bindata/bindata.go:109
    117  tegola/server/bindata/bindata.go:129
    118  tegola/server/bindata/bindata.go:149
    119  tegola/server/bindata/bindata.go:169
    120  tegola/server/bindata/bindata.go:189
    121  tegola/server/bindata/bindata.go:209
    122  tegola/server/bindata/bindata.go:229
    123  tegola/server/bindata/bindata.go:370
    124  tegola/server/bindata/bindata.go:374
    125  tegola/server/bindata/bindata.go:378
    126  tegola/server/bindata/bindata.go:382
    127  tegola/server/bindata/bindata.go:386
    128  tegola/server/bindata/bindata.go:402
    129  tegola/server/middleware_gzip.go:71
    130  tegola/server/middleware_gzip.go:78
    131  tegola/server/server_test.go:85

--- <err> name is different from "err" ---
      1  tegola/basic/json_marshal.go:276

--- { return ... zero values ..., expr } ---
      1  tegola/basic/geometry_math.go:214
      2  tegola/basic/geometry_math.go:222
      3  tegola/basic/geometry_math.go:230
      4  tegola/cache/azblob/azblob.go:131
      5  tegola/cache/azblob/azblob.go:140
      6  tegola/cache/azblob/azblob.go:149
      7  tegola/cache/azblob/azblob.go:171
      8  tegola/cache/file/file.go:47
      9  tegola/cache/s3/s3.go:92
     10  tegola/cmd/internal/register/maps.go:108
     11  tegola/cmd/tegola/cmd/cache/flags.go:20
     12  tegola/cmd/tegola/cmd/cache/tile_name.go:51
     13  tegola/cmd/tegola/cmd/cache/worker.go:112
     14  tegola/cmd/tegola/cmd/cache/worker.go:123
     15  tegola/cmd/tegola/cmd/root.go:73
     16  tegola/cmd/tegola/cmd/root.go:78
     17  tegola/cmd/xyz2svg/cmd/root.go:60
     18  tegola/provider/gpkg/gpkg.go:90
     19  tegola/provider/gpkg/gpkg.go:95
     20  tegola/provider/gpkg/gpkg_register.go:264
     21  tegola/provider/gpkg/gpkg_register.go:297
     22  tegola/provider/gpkg/gpkg_register.go:302
     23  tegola/provider/gpkg/gpkg_register.go:313
     24  tegola/provider/gpkg/gpkg_register.go:328
     25  tegola/provider/postgis/postgis.go:193
     26  tegola/provider/postgis/postgis.go:208
     27  tegola/provider/postgis/postgis.go:222
     28  tegola/provider/postgis/postgis.go:228
     29  tegola/provider/postgis/postgis.go:234
     30  tegola/provider/postgis/postgis.go:243
     31  tegola/provider/postgis/postgis.go:249
     32  tegola/provider/postgis/postgis.go:255
     33  tegola/provider/postgis/postgis.go:304
     34  tegola/provider/postgis/postgis.go:315
     35  tegola/provider/postgis/postgis.go:319
     36  tegola/provider/postgis/postgis.go:364
     37  tegola/provider/postgis/postgis.go:456
     38  tegola/provider/postgis/postgis.go:520
     39  tegola/provider/postgis/postgis.go:534
     40  tegola/provider/postgis/postgis.go:565
     41  tegola/provider/postgis/util.go:108
     42  tegola/provider/postgis/util.go:113
     43  tegola/server/bindata/bindata.go:29
     44  tegola/server/bindata/bindata.go:245
     45  tegola/server/bindata/bindata.go:271
     46  tegola/server/bindata/bindata.go:396

--- single statement then branch ---
      1  tegola/cache/azblob/azblob.go:241
      2  tegola/cache/file/file.go:87
      3  tegola/cache/s3/s3.go:321
      4  tegola/cmd/internal/register/caches.go:18
      5  tegola/cmd/internal/register/providers.go:43
      6  tegola/cmd/internal/register/providers.go:62
      7  tegola/cmd/internal/register/providers.go:75
      8  tegola/config/config.go:192
      9  tegola/config/config.go:207
     10  tegola/config/config.go:217
     11  tegola/internal/env/dict.go:43
     12  tegola/internal/env/dict.go:121
     13  tegola/internal/env/dict.go:197
     14  tegola/internal/env/dict.go:273
     15  tegola/internal/env/dict.go:348
     16  tegola/internal/env/parse.go:79
     17  tegola/internal/env/parse.go:126
     18  tegola/internal/env/parse.go:184
     19  tegola/internal/env/parse.go:231
     20  tegola/maths/makevalid/plyg/ring.go:541
     21  tegola/maths/maths.go:239
     22  tegola/maths/validate/validate.go:49
     23  tegola/maths/validate/validate.go:53
     24  tegola/maths/validate/validate.go:59
     25  tegola/maths/validate/validate.go:69
     26  tegola/mvt/feature.go:94
     27  tegola/mvt/feature.go:99
     28  tegola/mvt/feature.go:592
     29  tegola/mvt/feature.go:603
     30  tegola/mvt/layer.go:90
     31  tegola/mvt/tile.go:48
     32  tegola/provider/postgis/postgis.go:570
     33  tegola/provider/postgis/postgis.go:586
     34  tegola/tile.go:172

--- complex then branch; cannot use try ---
      1  tegola/cache/azblob/azblob.go:226
      2  tegola/cache/file/file.go:78
      3  tegola/cache/file/file.go:122
      4  tegola/cache/s3/s3.go:195
      5  tegola/cache/s3/s3.go:206
      6  tegola/cache/s3/s3.go:219
      7  tegola/cache/s3/s3.go:307
      8  tegola/provider/gpkg/gpkg.go:39
      9  tegola/provider/gpkg/gpkg.go:45
     10  tegola/provider/gpkg/gpkg.go:131
     11  tegola/provider/gpkg/gpkg.go:154
     12  tegola/provider/gpkg/gpkg_register.go:171
     13  tegola/provider/gpkg/gpkg_register.go:195

--- stats ---
   1294 (100.0% of    1294) func declarations
    246 ( 19.0% of    1294) func declarations returning an error
   2693 (100.0% of    2693) statements
    551 ( 20.5% of    2693) if statements
    238 ( 43.2% of     551) if <err> != nil statements
    131 ( 55.0% of     238) try candidates
      1 (  0.4% of     238) <err> name is different from "err"
--- non-try candidates ---
     46 ( 19.3% of     238) { return ... zero values ..., expr }
     34 ( 14.3% of     238) single statement then branch
     13 (  5.5% of     238) complex then branch; cannot use try
      0 (  0.0% of     238) non-empty else branch; cannot use try

Und das Begleitprojekt: (http://github.com/go-spatial/geom)

--- try candidates ---
      1  geom/bbox.go:202
      2  geom/encoding/geojson/geojson.go:152
      3  geom/encoding/geojson/geojson.go:157
      4  geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
      5  geom/encoding/wkb/internal/tcase/tcase.go:161
      6  geom/encoding/wkb/internal/tcase/tcase.go:172
      7  geom/encoding/wkb/wkb.go:50
      8  geom/encoding/wkb/wkb.go:110
      9  geom/encoding/wkt/internal/token/token.go:176
     10  geom/encoding/wkt/internal/token/token.go:252
     11  geom/internal/parsing/parsing.go:44
     12  geom/internal/parsing/parsing.go:85
     13  geom/internal/rtreego/rtree_test.go:110
     14  geom/multi_line_string.go:34
     15  geom/multi_polygon.go:35
     16  geom/planar/clip/linestring.go:82
     17  geom/planar/clip/linestring.go:181
     18  geom/planar/clip/point.go:23
     19  geom/planar/intersect/xsweep.go:106
     20  geom/planar/makevalid/makevalid.go:92
     21  geom/planar/makevalid/makevalid.go:191
     22  geom/planar/makevalid/setdiff/polygoncleaner.go:283
     23  geom/planar/makevalid/setdiff/polygoncleaner.go:345
     24  geom/planar/makevalid/setdiff/polygoncleaner.go:543
     25  geom/planar/makevalid/setdiff/polygoncleaner.go:554
     26  geom/planar/makevalid/setdiff/polygoncleaner.go:572
     27  geom/planar/makevalid/setdiff/polygoncleaner.go:578
     28  geom/planar/simplify/douglaspeucker.go:84
     29  geom/planar/simplify/douglaspeucker.go:88
     30  geom/planar/simplify.go:13
     31  geom/planar/triangulate/constraineddelaunay/triangle.go:186
     32  geom/planar/triangulate/constraineddelaunay/triangulator.go:134
     33  geom/planar/triangulate/constraineddelaunay/triangulator.go:138
     34  geom/planar/triangulate/constraineddelaunay/triangulator.go:142
     35  geom/planar/triangulate/constraineddelaunay/triangulator.go:173
     36  geom/planar/triangulate/constraineddelaunay/triangulator.go:176
     37  geom/planar/triangulate/constraineddelaunay/triangulator.go:203
     38  geom/planar/triangulate/constraineddelaunay/triangulator.go:248
     39  geom/planar/triangulate/constraineddelaunay/triangulator.go:396
     40  geom/planar/triangulate/constraineddelaunay/triangulator.go:466
     41  geom/planar/triangulate/constraineddelaunay/triangulator.go:553
     42  geom/planar/triangulate/constraineddelaunay/triangulator.go:583
     43  geom/planar/triangulate/constraineddelaunay/triangulator.go:667
     44  geom/planar/triangulate/constraineddelaunay/triangulator.go:672
     45  geom/planar/triangulate/constraineddelaunay/triangulator.go:677
     46  geom/planar/triangulate/constraineddelaunay/triangulator.go:814
     47  geom/planar/triangulate/constraineddelaunay/triangulator.go:818
     48  geom/planar/triangulate/constraineddelaunay/triangulator.go:823
     49  geom/planar/triangulate/constraineddelaunay/triangulator.go:865
     50  geom/planar/triangulate/constraineddelaunay/triangulator.go:870
     51  geom/planar/triangulate/constraineddelaunay/triangulator.go:875
     52  geom/planar/triangulate/constraineddelaunay/triangulator.go:897
     53  geom/planar/triangulate/constraineddelaunay/triangulator.go:901
     54  geom/planar/triangulate/constraineddelaunay/triangulator.go:907
     55  geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
     56  geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
     57  geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
     58  geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
     59  geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
     60  geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
     61  geom/planar/triangulate/delaunaytriangulationbuilder.go:66
     62  geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
     63  geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
     64  geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
     65  geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
     66  geom/slippy/tile.go:133

--- { return ... zero values ..., expr } ---
      1  geom/internal/parsing/parsing.go:125
      2  geom/planar/triangulate/constraineddelaunay/triangulator.go:428
      3  geom/planar/triangulate/constraineddelaunay/triangulator.go:447
      4  geom/planar/triangulate/constraineddelaunay/triangulator.go:460

--- single statement then branch ---
      1  geom/bbox.go:259
      2  geom/encoding/wkb/internal/decode/decode.go:29
      3  geom/encoding/wkb/internal/decode/decode.go:55
      4  geom/encoding/wkb/internal/decode/decode.go:63
      5  geom/encoding/wkb/internal/decode/decode.go:70
      6  geom/encoding/wkb/internal/decode/decode.go:79
      7  geom/encoding/wkb/internal/decode/decode.go:84
      8  geom/encoding/wkb/internal/decode/decode.go:93
      9  geom/encoding/wkb/internal/decode/decode.go:99
     10  geom/encoding/wkb/internal/decode/decode.go:105
     11  geom/encoding/wkb/internal/decode/decode.go:114
     12  geom/encoding/wkb/internal/decode/decode.go:119
     13  geom/encoding/wkb/internal/decode/decode.go:135
     14  geom/encoding/wkb/internal/decode/decode.go:140
     15  geom/encoding/wkb/internal/decode/decode.go:149
     16  geom/encoding/wkb/internal/decode/decode.go:155
     17  geom/encoding/wkb/internal/decode/decode.go:161
     18  geom/encoding/wkb/internal/decode/decode.go:170
     19  geom/encoding/wkb/internal/decode/decode.go:176
     20  geom/encoding/wkb/internal/tcase/token/token.go:162
     21  geom/encoding/wkt/internal/token/token.go:136

--- complex then branch; cannot use try ---
      1  geom/encoding/wkb/internal/tcase/tcase.go:74
      2  geom/encoding/wkt/internal/symbol/symbol.go:125
      3  geom/planar/intersect/xsweep.go:165
      4  geom/planar/makevalid/makevalid.go:85
      5  geom/planar/makevalid/makevalid.go:172
      6  geom/planar/makevalid/triangulate.go:19
      7  geom/planar/makevalid/triangulate.go:28
      8  geom/planar/makevalid/triangulate.go:36
      9  geom/planar/makevalid/triangulate.go:58
     10  geom/planar/triangulate/constraineddelaunay/triangulator.go:358
     11  geom/planar/triangulate/constraineddelaunay/triangulator.go:373
     12  geom/planar/triangulate/constraineddelaunay/triangulator.go:453
     13  geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
     14  geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
     15  geom/planar/triangulate/constraineddelaunay/triangulator.go:1249

--- stats ---
    820 (100.0% of     820) func declarations
    146 ( 17.8% of     820) func declarations returning an error
   1715 (100.0% of    1715) statements
    391 ( 22.8% of    1715) if statements
    111 ( 28.4% of     391) if <err> != nil statements
     66 ( 59.5% of     111) try candidates
      0 (  0.0% of     111) <err> name is different from "err"
--- non-try candidates ---
      4 (  3.6% of     111) { return ... zero values ..., expr }
     21 ( 18.9% of     111) single statement then branch
     15 ( 13.5% of     111) complex then branch; cannot use try
      0 (  0.0% of     111) non-empty else branch; cannot use try

Zum Thema unerwartete Kosten reposte ich dies von # 32611 ...

Ich sehe drei Kostenklassen:

  1. die Kosten für die Spezifikation, die im Designdokument ausgearbeitet ist.
  2. die Werkzeugkosten (dh Softwarerevision), die ebenfalls im Designdokument untersucht werden.
  3. die Kosten für das Ökosystem, die die Community oben und in #32825 ausführlich beschrieben hat.

Re-Nr. 1 & 2, die Kosten von try() sind bescheiden.

Zur Vereinfachung nein. 3 glauben die meisten Kommentatoren, dass try() unseren Code und/oder das Code-Ökosystem, auf das wir angewiesen sind, beschädigen und dadurch unsere Produktivität und Produktqualität verringern würde. Diese weit verbreitete, wohlbegründete Wahrnehmung sollte nicht als „sachlich“ oder „ästhetisch“ abgewertet werden.

Die Kosten für das Ökosystem sind weitaus wichtiger als die Kosten für die Spezifikation oder die Werkzeugausstattung.

@griesemer Es ist offensichtlich unfair zu behaupten, dass "drei Dutzend lautstarke Gegner" den Großteil der Opposition ausmachen. Hunderte von Menschen haben hier und in #32825 Kommentare abgegeben. Sie sagten mir am 12. Juni: "Ich erkenne an, dass etwa 2/3 der Befragten mit dem Vorschlag nicht zufrieden sind." Seitdem haben über 2.000 Menschen mit 90 % positiver Zustimmung für „Lass err != nil in Ruhe“ gestimmt.

@gdey könnten Sie Ihren Post so ändern, dass er nur _Statistiken und Nichtversuchskandidaten_ enthält?

@robfig , @gdey Vielen Dank für die Bereitstellung dieser Daten, insbesondere des Vorher/Nachher-Vergleichs.

@griesemer
Sie haben sicherlich einige Inhalte hinzugefügt, die klarstellen, dass meine (und die anderer) Bedenken direkt angesprochen werden könnten. Meine Frage ist also, ob das Go-Team den wahrscheinlichen Missbrauch der indirekten Modi (dh nackte Rückgaben und/oder Post-Function-Scope-Fehlermutationen per Defer) als Kosten betrachtet, die es wert sind, in Schritt 5 diskutiert zu werden, und ob es sich möglicherweise lohnt Maßnahmen zu seiner Minderung ergreifen. Die derzeitige Stimmung ist, dass dieser äußerst beunruhigende Aspekt des Vorschlags vom Go-Team als cleveres/neuartiges Feature angesehen wird (Diese Bedenken werden durch die Bewertung der automatisierten Transformationen nicht angesprochen und scheinen aktiv gefördert/unterstützt zu werden. - errd , im Gespräch usw.).

Bearbeiten zum Hinzufügen ... Die Sorge um das Go-Team, das das fördert, was erfahrene Gophers als unerschwinglich ansehen, ist das, was ich in Bezug auf Tontaubheit meinte.
... Indirektion ist ein Preis, über den viele von uns aufgrund von erfahrungsbedingtem Schmerz zutiefst besorgt sind. Es ist vielleicht nicht etwas, das leicht (wenn überhaupt vernünftig) gemessen werden kann, aber es ist unaufrichtig, diese Sorge selbst als sentimental zu betrachten. Vielmehr ist die Missachtung der Weisheit geteilter Erfahrung zugunsten einfacher Zahlen ohne solide kontextbezogene Beurteilung die Art von Gefühl, gegen das ich/wir anzugehen versuchen.

@networkimprov Entschuldigung, dass ich nicht klar genug bin. Was ich gesagt habe war:

Es gibt vielleicht zwei, vielleicht drei Dutzend lautstarke Gegner dieses Vorschlags – Sie wissen, wer Sie sind. Sie dominieren diese Diskussion mit häufigen Beiträgen, manchmal mehrmals am Tag.

Ich sprach von tatsächlichen Kommentaren (wie in „häufigen Posts“), nicht von Emojis. Es gibt nur eine relativ kleine Anzahl von Leuten, die hier _wiederholt_ posten, was meiner Meinung nach immer noch richtig ist. Ich habe auch nicht über #32825 gesprochen; Ich sprach von diesem Vorschlag.

Bei den Emojis ist die Situation im Vergleich zu vor einem Monat praktisch unverändert: 1/3 der Emojis zeigen eine positive Meinung an, und 2/3 zeigen eine negative Meinung an.

@griesemer

Ich habe mich an etwas erinnert, als ich meinen obigen Kommentar geschrieben habe: Während das Designdokument besagt, dass try als einfache Syntaxbaumtransformation implementiert werden kann, und in vielen Fällen ist dies offensichtlich der Fall, gibt es einige Fälle, in denen ich dies nicht tue finden Sie eine einfache Möglichkeit, dies zu tun. Angenommen, wir haben Folgendes:

switch x {
case rand.Int():
  a()
case 5, try(strconv.Atoi(y)):
  b()
}

Angesichts der Auswertungsreihenfolge von switch sehe ich nicht, wie ich strconv.Atoi(y) trivial aus der case -Klausel herausheben und dabei die beabsichtigte Semantik beibehalten kann. Das Beste, was mir einfallen könnte, ist, die switch als äquivalente Kette von if / else Anweisungen umzuschreiben, wie folgt:

if x == rand.Int() {
  a()
} else if x == 5 {
  b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
  return _err
} else if x == _v {
  b()
}

(Es gibt andere Situationen, in denen dies vorkommen kann, aber dies ist eines der einfachsten Beispiele und das erste, das mir in den Sinn kommt.)

Tatsächlich habe ich vor der Veröffentlichung dieses Vorschlags an einem AST-Übersetzer gearbeitet, um den check -Operator aus dem Entwurfsentwurf zu implementieren, und bin auf dieses Problem gestoßen. Ich habe jedoch eine gehackte Version der go/* stdlib-Pakete verwendet; Vielleicht ist das Compiler-Frontend so strukturiert, dass dies einfacher wird? Oder habe ich etwas übersehen und es gibt wirklich eine einfache Möglichkeit, dies zu tun?

Siehe auch https://github.com/rhysd/trygo; laut README implementiert es keine try -Ausdrücke und stellt im Wesentlichen die gleichen Bedenken fest, die ich hier anspreche; Ich vermute, das könnte der Grund sein, warum der Autor diese Funktion nicht implementiert hat.

@daved Professioneller Code wird nicht in einem Vakuum entwickelt - es gibt lokale Konventionen, Stilempfehlungen, Codeüberprüfungen usw. (ich habe das bereits gesagt). Daher sehe ich nicht ein, warum Missbrauch "wahrscheinlich" wäre (es ist möglich, aber das gilt für jedes Sprachkonstrukt).

Beachten Sie, dass die Verwendung defer zum Dekorieren von Fehlern mit oder ohne try möglich ist. Es gibt sicherlich gute Gründe für eine Funktion, die viele Fehlerprüfungen enthält, die alle Fehler auf die gleiche Weise schmücken, diese Dekoration einmal durchzuführen, zum Beispiel mit einem defer . Oder verwenden Sie vielleicht eine Wrapper-Funktion, die die Dekoration übernimmt. Oder jeder andere Mechanismus, der der Rechnung und den lokalen Codierungsempfehlungen entspricht. Schließlich sind „Fehler nur Werte“ und es macht absolut Sinn, Code zu schreiben und zu faktorisieren, der sich mit Fehlern befasst.

Naked Returns können problematisch sein, wenn sie undiszipliniert eingesetzt werden. Das bedeutet nicht, dass sie generell schlecht sind. Wenn zum Beispiel die Ergebnisse einer Funktion nur gültig sind, wenn kein Fehler aufgetreten ist, scheint es völlig in Ordnung zu sein, im Fehlerfall eine nackte Rückgabe zu verwenden - solange wir beim Setzen des Fehlers diszipliniert sind (da die anderen Rückgabewerte nicht t egal in diesem Fall). try sorgt genau dafür. Ich sehe hier keinen "Missbrauch".

@dpinela Der Compiler übersetzt bereits eine switch -Anweisung wie Ihre als Folge von if-else-if , daher sehe ich hier kein Problem. Außerdem ist der "Syntaxbaum", den der Compiler verwendet, nicht der "go/ast"-Syntaxbaum. Die interne Darstellung des Compilers ermöglicht viel flexibleren Code, der nicht unbedingt in Go zurückübersetzt werden kann.

@griesemer
Ja, natürlich hat alles, was Sie sagen, eine Grundlage. Der Graubereich ist jedoch nicht so simpel, wie Sie es darstellen. Nackte Rückkehrer werden normalerweise von denen von uns, die andere unterrichten (wir, die danach streben, die Gemeinschaft zu vergrößern/zu fördern), mit großer Vorsicht behandelt. Ich schätze, dass die stdlib es überall verunreinigt hat. Aber wenn man andere unterrichtet, wird immer explizite Rückkehr betont. Lassen Sie den Einzelnen seine eigene Reife erreichen, um sich dem "phantasievolleren" Ansatz zuzuwenden, aber ihn von Anfang an zu fördern, würde sicherlich schwer lesbaren Code (dh schlechte Gewohnheiten) fördern. Das ist wiederum die Tontaubheit, die ich ans Licht zu bringen versuche.

Ich persönlich möchte keine nackten Renditen oder Manipulationen aufgeschobener Werte verbieten. Wenn sie wirklich geeignet sind, bin ich froh, dass diese Fähigkeiten verfügbar sind (obwohl andere erfahrene Benutzer eine strengere Haltung einnehmen könnten). Nichtsdestotrotz ist die Förderung der Anwendung dieser weniger verbreiteten und im Allgemeinen zerbrechlichen Merkmale auf eine so allgegenwärtige Weise die völlig entgegengesetzte Richtung, die ich mir je vorgestellt habe. Ist die ausgeprägte Charakterveränderung des Verzichts auf Magie und prekäre Formen der Indirektion eine beabsichtigte Verschiebung? Sollten wir auch damit beginnen, die Verwendung von DICs und anderen schwer zu debuggenden Mechanismen zu betonen?

ps Ihre Zeit wird sehr geschätzt. Ihr Team und die Sprache haben meinen Respekt und meine Fürsorge. Ich wünsche niemandem Kummer, wenn ich mich zu Wort melde; Ich hoffe, Sie werden die Art meiner/unserer Sorge verstehen und versuchen, die Dinge aus unserer „Frontlinien“-Perspektive zu sehen.

Ich füge ein paar Kommentare zu meiner Ablehnung hinzu.

Für den vorliegenden konkreten Vorschlag:

1) Ich würde es sehr bevorzugen, dass dies ein Schlüsselwort gegenüber einer integrierten Funktion ist, aus zuvor artikulierten Gründen des Kontrollflusses und der Lesbarkeit des Codes.

2) Semantisch ist „versuchen“ ein Blitzableiter. Und, sofern keine Ausnahme ausgelöst wird, sollte "try" besser in etwas wie guard oder ensure umbenannt werden.

3) Abgesehen von diesen beiden Punkten denke ich, dass dies der beste Vorschlag ist, den ich für diese Art von Dingen gesehen habe.

Ein paar weitere Kommentare, die meinen Einwand gegen die Hinzufügung eines try/guard/ensure -Konzepts im Gegensatz dazu artikulieren, if err != nil in Ruhe zu lassen:

1) Dies widerspricht einem der ursprünglichen Mandate von Golang (zumindest wie ich es wahrgenommen habe), explizit, leicht zu lesen/verstehen und mit sehr wenig „Magie“ zu sein.

2) Dies fördert die Faulheit genau in dem Moment, in dem nachgedacht werden muss: "Was kann mein Code bei diesem Fehler am besten tun?". Es gibt viele Fehler, die auftreten können, wenn Sie „Boilerplate“-Dinge wie das Öffnen von Dateien, das Übertragen von Daten über ein Netzwerk usw. trys" verschwindet, da Sie möglicherweise Ihre eigenen Backoff-/Wiederholungs-, Protokollierungs-/Tracing- und/oder Bereinigungsaufgaben implementieren müssen. „Ereignisse mit geringer Wahrscheinlichkeit“ sind im großen Maßstab garantiert.

Hier sind noch mehr rohe tryhard -Statistiken. Dies ist nur leicht validiert, also fühlen Sie sich frei, auf Fehler hinzuweisen. ;-)

Die ersten 20 „Popular Packages“ auf godoc.org

Dies sind die Repositories, die den ersten 20 beliebten Paketen auf https://godoc.org entsprechen, sortiert nach Prozentsatz der Testkandidaten. Dies verwendet die Standardeinstellungen tryhard , die theoretisch vendor -Verzeichnisse ausschließen sollten.

Der Medianwert für Try-Kandidaten in diesen 20 Repos beträgt 58 %.

| Projekt | Ort | wenn stmts | if != nil (% von if) | Versuchskandidaten (% von if != nil) |
|---------|-----|---------------|---------------- -----|---------------
| github.com/google/uuid | 1714 | 12 | 16,7 % | 0,0 % |
| github.com/pkg/errors | 1886 | 10 | 0,0 % | 0,0 % |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9,4 % | 8,9 % |
| github.com/jinzhu/gorm | 15246 | 44 | 11,4 % | 20,0 % |
| github.com/robfig/cron | 1911 | 20 | 35,0 % | 28,6 % |
| github.com/gorilla/websocket | 6959 | 212 | 32,5 % | 39,1 % |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29,7 % | 40,0 % |
| github.com/gomodule/redigo | 7119 | 187 | 34,8 % | 41,5 % |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75,0 % | 43,6 % |
| github.com/lib/pq | 13396 | 239 | 30,1 % | 55,6 % |
| github.com/sirupsen/logrus | 5063 | 29 | 17,2 % | 60,0 % |
| github.com/prometheus/client_golang | 17791 | 194 | 49,0 % | 62,1 % |
| github.com/go-redis/redis | 21182 | 326 | 42,6 % | 73,4 % |
| github.com/mongodb/mongo-go-driver | 86605 | 2097 | 37,8 % | 73,9 % |
| github.com/uber-go/zap | 15363 | 84 | 36,9 % | 74,2 % |
| github.com/golang/protobuf | 42959 | 685 | 22,9 % | 77,1 % |
| github.com/gin-gonic/gin | 14574 | 96 | 53,1 % | 86,3 % |
| github.com/go-pg/pg | 26369 | 831 | 37,7 % | 86,9 % |
| github.com/Shopify/sarama | 36427 | 1369 | 68,2 % | 91,0 % |
| github.com/stretchr/testify | 13496 | 32 | 43,8 % | 92,9 % |

Die Spalte " if stmts " zählt nur if -Anweisungen in Funktionen, die einen Fehler zurückgeben, was tryhard so meldet und was hoffentlich erklärt, warum es für etwas wie gorm so niedrig ist

10 versch. "große" Go-Projekte

Da beliebte Pakete auf godoc.org eher Bibliothekspakete sind, wollte ich auch die Statistiken für einige größere Projekte überprüfen.

Das sind versch. große Projekte, die mir zufällig am Herzen lagen (dh keine wirkliche Logik hinter diesen 10). Dies ist wiederum nach dem Prozentsatz der Versuchskandidaten sortiert.

Der Medianwert für Try-Kandidaten in diesen 10 Repos beträgt 59 %.

| Projekt | Ort | wenn stmts | if != nil (% von if) | Versuchskandidaten (% von if != nil) |
|---------|-----|---------------|---------------- -----|--------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51,9 % | 17,5 % |
| github.com/go-kit/kit | 38949 | 467 | 57,0 % | 51,9 % |
| github.com/boltdb/bolt | 12426 | 228 | 46,1 % | 53,3 % |
| github.com/hashicorp/consul | 249369 | 5477 | 47,6 % | 54,5 % |
| github.com/docker/docker | 251152 | 8690 | 48,7 % | 56,8 % |
| github.com/istio/istio | 429636 | 7564 | 40,4 % | 61,9 % |
| github.com/gohugoio/hugo | 94875 | 1853 | 42,4 % | 64,8 % |
| github.com/etcd-io/etcd | 209603 | 4657 | 38,3 % | 65,5 % |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43,3 % | 66,5 % |
| github.com/cockroachdb/cockroach | 1038529 | 22018 | 39,9 % | 74,0 % |


Diese beiden Tabellen stellen natürlich nur eine Auswahl von Open-Source-Projekten dar, und nur einigermaßen bekannte. Ich habe Leute gesehen, die theoretisiert haben, dass private Codebasen eine größere Vielfalt aufweisen würden, und es gibt zumindest einige Beweise dafür, basierend auf einigen der Zahlen, die verschiedene Leute gepostet haben.

@thepudds , das sieht nicht wie das neueste _tryhard_ aus, das "Nicht-Try-Kandidaten" ergibt.

@networkimprov Ich kann bestätigen, dass dies zumindest für gorm Ergebnisse der letzten tryhard sind. Die "Non-Try-Kandidaten" sind in den obigen Tabellen einfach nicht aufgeführt.

@daved Lassen Sie mich Ihnen zunächst versichern, dass ich/wir Sie laut und deutlich hören. Obwohl wir noch am Anfang des Prozesses stehen und sich viele Dinge ändern können. Lassen Sie uns nicht die Waffe überspringen.

Ich verstehe (und schätze), dass man beim Unterrichten von Go einen konservativeren Ansatz wählen sollte. Danke.

@griesemer FYI, hier sind die Ergebnisse der Ausführung dieser neuesten Version von tryhard auf 233.000 Codezeilen, an denen ich beteiligt war, ein Großteil davon nicht Open Source:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

Ein Großteil des Codes verwendet ein Idiom, das dem folgenden ähnelt:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

Es könnte interessant sein, wenn tryhard erkennen könnte, wann alle derartigen Ausdrücke in einer Funktion einen identischen Ausdruck verwenden – dh wenn es möglich wäre, die Funktion mit einem einzigen gemeinsamen defer -Handler umzuschreiben.

Hier sind die Statistiken für ein kleines GCP-Hilfstool zur Automatisierung der Benutzer- und Projekterstellung:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

Danach habe ich alle Stellen im Code überprüft, die noch mit einer err -Variablen zu tun haben, um zu sehen, ob ich sinnvolle Muster finden konnte.

err s sammeln

An einigen Stellen möchten wir die Ausführung nicht beim ersten Fehler stoppen und stattdessen am Ende des Laufs alle Fehler sehen können, die einmal aufgetreten sind. Vielleicht gibt es einen anderen Weg, dies zu tun, der sich gut in try integrieren lässt, oder Go selbst wird eine Form der Unterstützung für Mehrfachfehler hinzugefügt.

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

Verantwortung für Fehlerdekoration

Nachdem ich diesen Kommentar noch einmal gelesen hatte, gab es plötzlich viele potenzielle try Fälle, die mir auffielen. Sie ähneln sich alle darin, dass die aufrufende Funktion den Fehler einer aufgerufenen Funktion mit Informationen schmückt, die die aufgerufene Funktion dem Fehler bereits hinzugefügt haben könnte:

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

Zitieren Sie den wichtigen Teil aus dem Go-Blog hier noch einmal zur Verdeutlichung:

Es liegt in der Verantwortung der Fehlerimplementierung, den Kontext zusammenzufassen. Der von os.Open zurückgegebene Fehler wird als „open /etc/passwd: permission denied“ formatiert, nicht nur als „permission denied“. Dem von unserem Sqrt zurückgegebenen Fehler fehlen Informationen zum ungültigen Argument.

Vor diesem Hintergrund wird der obige Code nun zu:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

Auf den ersten Blick scheint dies eine geringfügige Änderung zu sein, aber meiner Einschätzung nach könnte es bedeuten, dass try tatsächlich einen Anreiz darstellt, eine bessere und konsistentere Fehlerbehandlung in der Funktionskette nach oben und näher an die Quelle oder das Paket zu bringen.

Schlussbemerkungen

Insgesamt denke ich, dass der Wert, den try langfristig bringt, höher ist als die potenziellen Probleme, die ich derzeit damit sehe, nämlich:

  1. Ein Keyword fühlt sich möglicherweise besser an, da try den Kontrollfluss ändert.
  2. Die Verwendung try bedeutet, dass Sie keinen Debug-Stopper mehr in den Fall return err setzen können.

Da diese Bedenken dem Go-Team bereits bekannt sind, bin ich gespannt, wie sich diese in der "realen Welt" auswirken werden. Vielen Dank für Ihre Zeit beim Lesen und Beantworten all unserer Nachrichten.

Aktualisieren

Eine Funktionssignatur wurde behoben, die zuvor kein error zurückgegeben hat. Danke @magical , dass du das entdeckt hast!

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Nitpicking, aber Sie können try in diesem Beispiel nicht wirklich verwenden, da main kein error .

Dies ist ein Anerkennungskommentar;
Danke @griesemer für die Gartenarbeit und alles, was Sie zu diesem Thema und anderswo geleistet haben.

Falls Sie viele Zeilen wie diese haben (von https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

Sie könnten eine Hilfsfunktion verwenden, die nur dann einen Nicht-Null-Fehler zurückgibt, wenn eine Bedingung wahr ist:

try(condErrorf(!ok, "environment variable not set: %s", key))

Sobald gemeinsame Muster identifiziert sind, denke ich, dass es möglich sein wird, viele von ihnen mit nur wenigen Helfern zu handhaben, zunächst auf Paketebene und vielleicht schließlich bis zur Standardbibliothek. Tryhard ist großartig, es macht einen wunderbaren Job und gibt viele interessante Informationen, aber es gibt noch viel mehr.

Kompakte einzeilige wenn

Als Ergänzung zum einzeiligen if-Vorschlag von @zeebo und anderen könnte die if-Anweisung eine kompakte Form haben, die das != nil und die geschweiften Klammern entfernt:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

Ich denke, das ist einfach, leicht und lesbar. Es gibt zwei Teile:

  1. Lassen Sie if-Anweisungen Fehlerwerte implizit auf nil überprüfen (oder allgemeiner Schnittstellen). IMHO verbessert dies die Lesbarkeit, indem die Dichte verringert wird, und das Verhalten ist ziemlich offensichtlich.
  2. Unterstützung für if variable return ... . Da das return so nahe an der linken Seite steht, scheint es immer noch recht einfach zu sein, den Code zu überfliegen - die zusätzliche Schwierigkeit dabei ist eines der Hauptargumente gegen einzeilige ifs (?) Go hat auch bereits Präzedenzfälle für die Vereinfachung der Syntax, indem beispielsweise Klammern aus seiner if-Anweisung entfernt wurden.

Aktueller Stil:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

Einzeilig, wenn:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

Einzeilig kompakt, wenn:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err return nil, errors.WithMessage(err, "load config dir")

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err return nil, errors.WithMessage(err, "execute main template")

    buf, err := format.Source(b.Bytes())
    if err return nil, errors.WithMessage(err, "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err return nil, errors.WithMessagef(err, "write file %s", target)

    // ...
}

@ eug48 siehe # 32611

Hier sind erprobte Statistiken für ein Monorepo (Zeilen des Go-Codes, ausgenommen Code des Anbieters: 2.282.731):

--- stats ---
 117551 (100.0% of  117551) functions (function literals are ignored)
  35726 ( 30.4% of  117551) functions returning an error
 263725 (100.0% of  263725) statements in functions returning an error
  50690 ( 19.2% of  263725) if statements
  25042 ( 49.4% of   50690) if <err> != nil statements
  12091 ( 48.3% of   25042) try candidates
     36 (  0.1% of   25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3561 ( 14.2% of   25042) { return ... zero values ..., expr }
   3304 ( 13.2% of   25042) single statement then branch
   4966 ( 19.8% of   25042) complex then branch; cannot use try
    296 (  1.2% of   25042) non-empty else branch; cannot use try

Angesichts der Tatsache, dass immer noch Alternativen vorgeschlagen werden, würde ich gerne genauer wissen, welche Funktionalität die breitere Go-Community tatsächlich von einer vorgeschlagenen neuen Fehlerbehandlungsfunktion erwartet.

Ich habe eine Umfrage zusammengestellt, die eine Reihe verschiedener Funktionen auflistet, Teile der Fehlerbehandlungsfunktionalität, die ich von Leuten vorgeschlagen gesehen habe. Ich habe sorgfältig _jede vorgeschlagene Benennung oder Syntax weggelassen_ und natürlich versucht, die Umfrage neutral zu gestalten, anstatt meine eigene Meinung zu vertreten.

Wenn Leute teilnehmen möchten, hier ist der Link, zum Teilen gekürzt:

https://forms.gle/gaCBgxKRE4RMCz7c7

Alle Teilnehmer sollten die zusammenfassenden Ergebnisse sehen können. Vielleicht hilft das, die Diskussion zu fokussieren?

if err := os.Setenv("GO111MODULE", "on"); err != nil {
    return err
}

Der Defer-Handler, der Kontext hinzufügt, funktioniert in diesem Fall nicht, oder doch? Wenn nicht, wäre es schön, es besser sichtbar zu machen, wenn möglich, da es ziemlich schnell passiert, zumal dies bis jetzt der Standard ist.

Oh, und stellen Sie bitte try vor, auch hier wurden viele Anwendungsfälle gefunden.

--- stats ---
    929 (100.0% of     929) functions (function literals are ignored)
    230 ( 24.8% of     929) functions returning an error
   1480 (100.0% of    1480) statements in functions returning an error
    320 ( 21.6% of    1480) if statements
    206 ( 64.4% of     320) if <err> != nil statements
    109 ( 52.9% of     206) try candidates
      2 (  1.0% of     206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     53 ( 25.7% of     206) { return ... zero values ..., expr }
     18 (  8.7% of     206) single statement then branch
     17 (  8.3% of     206) complex then branch; cannot use try
      2 (  1.0% of     206) non-empty else branch; cannot use try

@lpar Sie können gerne Alternativen diskutieren, aber bitte tun Sie dies nicht in dieser Ausgabe. Hier geht es um den try Vorschlag. Der beste Platz wäre eigentlich eine der Mailinglisten, zB go-nuts. Der Problem-Tracker eignet sich wirklich am besten, um ein bestimmtes Problem zu verfolgen und zu diskutieren, anstatt eine allgemeine Diskussion zu führen. Danke.

@fabstu Der defer -Handler funktioniert in Ihrem Beispiel problemlos, sowohl mit als auch ohne try . Erweitern Sie Ihren Code mit umschließender Funktion:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(Beachten Sie, dass das Ergebnis err von return err gesetzt wird; und das von err verwendete return ist dasjenige, das lokal mit if deklariert wird

Oder verwenden Sie ein try , wodurch die lokale Variable err nicht mehr benötigt wird:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

Und höchstwahrscheinlich möchten Sie eine der vorgeschlagenen errors/errd -Funktionen verwenden:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

Und wenn Sie keine Verpackung benötigen, ist es nur:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

@fastu Und schließlich kannst du errors/errd auch ohne try verwenden und dann bekommst du:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

Je mehr ich nachdenke, desto mehr gefällt mir dieser Vorschlag.
Das einzige, was mich stört, ist, überall Named Return zu verwenden. Ist es endlich eine gute Praxis und ich sollte es verwenden (nie versucht)?

Wie auch immer, bevor ich meinen gesamten Code ändere, wird es so funktionieren?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@flibustenet Benannte Ergebnisparameter an sich sind überhaupt keine schlechte Praxis. Die übliche Sorge bei benannten Ergebnissen ist, dass sie naked returns aktivieren; dh man kann einfach return schreiben, ohne die tatsächlichen Ergebnisse _mit den return _ angeben zu müssen. Im Allgemeinen (aber nicht immer!) macht es eine solche Vorgehensweise schwieriger, den Code zu lesen und zu argumentieren, weil man nicht einfach auf die return -Anweisung schauen und daraus schließen kann, was das Ergebnis ist. Man muss den Code für die Ergebnisparameter scannen. Man kann es versäumen, einen Ergebniswert zu setzen, und so weiter. Daher wird in einigen Codebasen von nackten Rückgaben einfach abgeraten.

Aber wie ich bereits erwähnt habe , wenn die Ergebnisse im Falle eines Fehlers ungültig sind, ist es vollkommen in Ordnung, den Fehler zu setzen und den Rest zu ignorieren. Eine nackte Rückgabe ist in solchen Fällen vollkommen in Ordnung, solange das Fehlerergebnis konsequent gesetzt ist. try wird genau das sicherstellen.

Schließlich werden benannte Ergebnisparameter nur benötigt, wenn Sie die Fehlerrückgabe mit defer erweitern möchten. Das Entwurfsdokument erörtert auch kurz die Möglichkeit, eine weitere integrierte Funktion bereitzustellen, um auf das Fehlerergebnis zuzugreifen. Das würde die Notwendigkeit benannter Rücksendungen vollständig beseitigen.

In Bezug auf Ihr Codebeispiel: Dies wird nicht wie erwartet funktionieren, da try _immer_ den _Ergebnisfehler_ (der in diesem Fall unbenannt ist) setzt. Aber Sie deklarieren eine andere lokale Variable err und errd.Wrap arbeitet mit dieser. Es wird nicht von try gesetzt.

Schneller Erfahrungsbericht: Ich schreibe einen HTTP-Request-Handler, der so aussieht:

func Handler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := chi.URLParam(r, "id")

        var err error
        // starts as bad request, then it's an internal server error after we parse inputs
        var statusCode = http.StatusBadRequest

        defer func() {
            if err != nil {
                wrap := xerrors.Errorf("handler fail: %w", err)
                logger.With(zap.Error(wrap)).Error("error")
                http.Error(w, wrap.Error(), statusCode)
            }
        }()
        var c Thingie
        err = unmarshalBody(r, &c)
        if err != nil {
            return
        }
        statusCode = http.StatusInternalServerError
        s, err := DoThing(ctx, c)
        if err != nil {
            return
        }
        d, err := DoThingWithResult(ctx, id, s)
        if err != nil {
            return
        }
        data, err := json.Marshal(detail)
        if err != nil {
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _, err = w.Write(data)
        if err != nil {
            return
        }
}

Auf den ersten Blick sieht es so aus, als wäre dies ein idealer Kandidat für try , da es eine Menge Fehlerbehandlung gibt, bei der es nichts zu tun gibt, außer eine Nachricht zurückzugeben, was alles aufgeschoben werden kann. Aber Sie können try nicht verwenden, weil ein Request-Handler nicht error . Um es zu verwenden, müsste ich den Körper in einen Verschluss mit der Signatur func() error wickeln. Das fühlt sich ... unelegant an und ich vermute, dass Code, der so aussieht, ein ziemlich häufiges Muster ist.

@jonbodner

Das funktioniert (https://play.golang.org/p/NaBZe-QShpu):

package main

import (
    "errors"
    "fmt"

    "golang.org/x/xerrors"
)

func main() {
    var err error
    defer func() {
        filterCheck(recover())
        if err != nil {
            wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
            fmt.Println(wrap)
        }
    }()

    check(retNoErr())

    n, err := intNoErr()
    check(err)

    n, err = intErr()
    check(err)

    check(retNoErr())

    check(retErr())

    fmt.Println(n)
}

func check(err error) {
    if err != nil {
        panic(struct{}{})
    }
}

func filterCheck(r interface{}) {
    if r != nil {
        if _, ok := r.(struct{}); !ok {
            panic(r)
        }
    }
}

var ct int

func intNoErr() (int, error) {
    ct++
    return 0, nil
}

func retNoErr() error {
    ct++
    return nil
}

func intErr() (int, error) {
    ct++
    return 0, errors.New("oops")
}

func retErr() error {
    ct++
    return errors.New("oops")
}

Ah, die erste Ablehnung! Gut. Lassen Sie den Pragmatismus durch sich fließen.

Ließ tryhard auf einigen meiner Codebasen laufen. Leider haben einige meiner Pakete 0 Versuchskandidaten, obwohl sie ziemlich groß sind, weil die darin enthaltenen Methoden eine benutzerdefinierte Fehlerimplementierung verwenden. Beim Erstellen von Servern möchte ich beispielsweise, dass meine Methoden der Geschäftslogikschicht nur SanitizedError s anstelle von error s ausgeben, um sicherzustellen, dass Dinge wie Dateisystempfade oder Systeminformationen zur Kompilierungszeit dies nicht tun Benutzern in Fehlermeldungen durchsickern.

Eine Methode, die dieses Muster verwendet, könnte beispielsweise so aussehen:

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
    if user, err := a.GetUserById(userId); err != nil {
        // (*App).GetUserById returns (*model.User, SanitizedError)
        // This could be a try() candidate.
        return err
    } else if user == nil {
        return NewUserError("The specified user doesn't exist.")
    }

    friends, err := a.Store.GetFriendsOfUser(userId)
    // (*Store).GetFriendsOfUser returns ([]*model.User, error)
    // This could be a SQL error or a network error or who knows what.
    return friends, NewInternalError(err)
}

Gibt es einen Grund, warum wir den aktuellen Vorschlag nicht lockern können, um zu funktionieren, solange der letzte Rückgabewert sowohl der einschließenden Funktion als auch des try-Funktionsausdrucks einen Fehler implementiert und vom gleichen Typ ist? Dies würde immer noch jede konkrete nil -> Interface-Verwirrung vermeiden, aber es würde try in Situationen wie den obigen ermöglichen.

Danke, @jonbodner , für dein Beispiel . Ich würde diesen Code wie folgt schreiben (trotz Übersetzungsfehlern):

func Handler(w http.ResponseWriter, r *http.Request) {
    statusCode, err := internalHandler(w, r)
    if err != nil {
        wrap := xerrors.Errorf("handler fail: %w", err)
        logger.With(zap.Error(wrap)).Error("error")
        http.Error(w, wrap.Error(), statusCode)
    }
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
    ctx := r.Context()
    id := chi.URLParam(r, "id")

    // starts as bad request, then it's an internal server error after we parse inputs
    statusCode = http.StatusBadRequest
    var c Thingie
    try(unmarshalBody(r, &c))

    statusCode = http.StatusInternalServerError
    s := try(DoThing(ctx, c))
    d := try(DoThingWithResult(ctx, id, s))
    data := try(json.Marshal(detail))

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    try(w.Write(data))

    return
}

Es verwendet zwei Funktionen, ist aber viel kürzer (29 Zeilen gegenüber 40 Zeilen) - und ich habe schöne Abstände verwendet - und dieser Code benötigt kein defer . Insbesondere defer zusammen mit dem StatusCode, der auf dem Weg nach unten geändert und in defer verwendet wird, macht es schwieriger als nötig, dem ursprünglichen Code zu folgen. Der neue Code verwendet zwar benannte Ergebnisse und eine nackte Rückgabe (Sie können dies problemlos durch return statusCode, nil ersetzen, wenn Sie möchten), ist jedoch einfacher, da er die Fehlerbehandlung sauber von der "Geschäftslogik" trennt.

Reposte einfach meinen Kommentar in einer anderen Ausgabe https://github.com/golang/go/issues/32853#issuecomment -510340544

Ich denke, wenn wir einen anderen Parameter funcname können, wäre das großartig, sonst wissen wir immer noch nicht, von welcher Funktion der Fehler zurückgegeben wird.

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

@ccbrown Ich frage mich, ob Ihr Beispiel für dieselbe Behandlung wie oben geeignet wäre. dh, wenn es sinnvoll wäre, Code so zu faktorisieren, dass interne Fehler einmal (durch eine einschließende Funktion) umschlossen werden, bevor sie ausgehen (anstatt sie überall einzupacken). Es scheint mir (ohne viel über Ihr System zu wissen), dass dies vorzuziehen wäre, da es die Fehlerumhüllung an einem Ort und nicht überall zentralisieren würde.

Aber zu Ihrer Frage: Ich müsste darüber nachdenken, try dazu zu bringen, einen allgemeineren Fehlertyp zu akzeptieren (und auch einen zurückzugeben). Ich sehe im Moment kein Problem darin (außer dass es komplizierter zu erklären ist) - aber es kann immerhin ein Problem geben.

In diesem Sinne haben wir uns schon früh gefragt, ob try verallgemeinert werden könnte, sodass es nicht nur für error -Typen funktioniert, sondern für jeden Typ, und der Test err != nil würde dann funktionieren zu x != zero werden, wobei x das Äquivalent von err (das letzte Ergebnis) und zero der entsprechende Nullwert für den Typ von x ist. bool gleich false ist und ok != false genau ist das Gegenteil von dem, was wir testen möchten.

@lunny Die vorgeschlagene Version von try akzeptiert keine Handler-Funktion.

@griesemer Ach. Wie schade! Andernfalls kann ich github.com/pkg/errors und alle errors.Wrap entfernen.

@ccbrown Ich frage mich, ob Ihr Beispiel für dieselbe Behandlung wie oben geeignet wäre. dh, wenn es sinnvoll wäre, Code so zu faktorisieren, dass interne Fehler einmal (durch eine einschließende Funktion) umschlossen werden, bevor sie ausgehen (anstatt sie überall einzupacken). Es scheint mir (ohne viel über Ihr System zu wissen), dass dies vorzuziehen wäre, da es die Fehlerumhüllung an einem Ort und nicht überall zentralisieren würde.

@griesemer Die Rückgabe von try error stattdessen an eine einschließende Funktion würde es ermöglichen, zu vergessen, jeden Fehler als internen Fehler, Benutzerfehler, Autorisierungsfehler usw. try wäre es nicht wert, diese Prüfungen zur Kompilierzeit gegen Prüfungen zur Laufzeit einzutauschen.

Ich möchte sagen, dass mir das Design von try gefällt, aber es gibt immer noch if Anweisung im defer Handler, während Sie try verwenden. Ich glaube nicht, dass das einfacher wäre als if -Anweisungen ohne try - und defer -Handler. Vielleicht wäre es viel besser, nur try zu verwenden.

@ccbrown Verstanden . Im Nachhinein denke ich, dass Ihr Entspannungsvorschlag kein Problem darstellen sollte. Ich glaube, wir könnten try entspannen, um mit jedem Schnittstellentyp (und übereinstimmenden Ergebnistyp) zu arbeiten, nicht nur error , solange der relevante Test x != nil bleibt . Etwas zum Nachdenken. Dies könnte frühzeitig oder rückwirkend erfolgen, da dies meines Erachtens eine abwärtskompatible Änderung wäre.

Das @jonbodner- Beispiel und die Art und Weise, wie @griesemer es umgeschrieben hat, ist genau die Art von Code, die ich habe, wo ich wirklich gerne try verwenden würde.

Stört sich niemand an dieser Art der Verwendung von try:

data := try(json.Marshal(detail))

Ungeachtet der Tatsache, dass der Marshalling-Fehler dazu führen kann, dass die richtige Zeile im geschriebenen Code gefunden wird, fühle ich mich einfach unwohl, wenn ich weiß, dass dies ein nackter Fehler ist, der zurückgegeben wird, ohne dass Zeilennummer/Anruferinformationen enthalten sind. Die Kenntnis der Quelldatei, des Funktionsnamens und der Zeilennummer ist normalerweise das, was ich bei der Behandlung von Fehlern einbeziehe. Vielleicht verstehe ich aber auch etwas falsch.

@griesemer Ich hatte nicht vor, hier Alternativen zu diskutieren. Gerade weil alle immer wieder Alternativen vorschlagen, halte ich eine Umfrage, um herauszufinden, was die Leute eigentlich wollen, für eine gute Idee. Ich habe hier nur darüber gepostet, um zu versuchen, so viele Leute wie möglich anzusprechen, die an der Möglichkeit interessiert sind, die Go-Fehlerbehandlung zu verbessern.

@trende-jp Ich hänge wirklich vom Kontext dieser Codezeile ab - allein kann es nicht sinnvoll beurteilt werden. Wenn dies der einzige Aufruf von json.Marshal ist und Sie den Fehler erweitern möchten, ist eine if -Anweisung möglicherweise am besten. Wenn es viele json.Marshal Aufrufe gibt, könnte das Hinzufügen von Kontext zum Fehler mit einem defer gut gemacht werden; oder vielleicht indem Sie alle diese Aufrufe in eine lokale Schließung einschließen, die den Fehler zurückgibt. Es gibt eine Vielzahl von Möglichkeiten, wie dies bei Bedarf berücksichtigt werden könnte (dh wenn es viele solcher Aufrufe in derselben Funktion gibt). „Fehler sind Werte“ gilt auch hier: Verwenden Sie Code, um die Fehlerbehandlung handhabbar zu machen.

try wird nicht alle Ihre Fehlerbehandlungsprobleme lösen - das ist nicht die Absicht. Es ist einfach ein weiteres Werkzeug in der Toolbox. Und es ist auch keine wirklich neue Maschinerie, sondern eine Art syntaktischer Zucker für ein Muster, das wir im Laufe von fast einem Jahrzehnt häufig beobachtet haben. Wir haben einige Beweise dafür, dass es in einigen Codes wirklich gut funktionieren würde und dass es in anderem Code auch nicht viel hilfreich wäre.

@trende-jp

Kann es nicht mit defer gelöst werden?

defer fmt.HandleErrorf(&err, "decoding %q", path)

Zeilennummern in Fehlermeldungen können auch gelöst werden, wie ich in meinem Blog gezeigt habe: How to use 'try' .

@trende-jp @faiface Zusätzlich zur Zeilennummer könnten Sie den Decorator-String in einer Variablen speichern. Auf diese Weise können Sie den spezifischen Funktionsaufruf isolieren, der fehlschlägt.

Ich denke wirklich, dass dies absolut keine eingebaute Funktion sein sollte .

Es wurde ein paar Mal erwähnt, dass panic() und recover() auch den Kontrollfluss verändern. Sehr gut, fügen wir nicht mehr hinzu.

@networkimprov schrieb https://github.com/golang/go/issues/32437#issuecomment -498960081:

Es liest sich nicht wie Go.

Ich könnte nicht mehr zustimmen.

Wenn überhaupt, glaube ich, dass jeder Mechanismus zur Lösung des Grundproblems (und ich bin mir nicht sicher, ob es einen gibt) durch ein Schlüsselwort (oder ein Schlüsselsymbol ?) ausgelöst werden sollte.

Wie würden Sie sich fühlen, wenn go func() go(func()) wäre?

Wie wäre es mit Bang(!) anstelle der Funktion try ? Dies könnte eine Funktionskette ermöglichen:

func foo() {
    f := os.Open!("")
    defer f.Close()
    // etc
}

func bar() {
    count := mustErr!().Read!()
}

@sylr

Wie würden Sie sich fühlen, wenn go func() go(func()) wäre?

Komm schon, das wäre ziemlich akzeptabel.

@sylr Danke, aber wir erbitten keine alternativen Vorschläge zu diesem Thread. Siehe auch dies zum Fokussieren.

In Bezug auf Ihren Kommentar : Go ist eine pragmatische Sprache - die Verwendung einer integrierten hier ist eine pragmatische Wahl. Es hat mehrere Vorteile gegenüber der Verwendung eines Schlüsselworts, wie ausführlich im Designdokument erklärt. Beachten Sie, dass try einfach syntaktischer Zucker für ein allgemeines Muster ist (im Gegensatz zu go , das ein Hauptmerkmal von Go implementiert und nicht mit anderen Go-Mechanismen implementiert werden kann), wie append , copy , etc. Die Verwendung eines eingebauten ist eine gute Wahl.

(Aber wie ich bereits gesagt habe, wenn _das_ das einzige ist, was verhindert, dass try akzeptabel ist, können wir erwägen, es zu einem Schlüsselwort zu machen.)

Ich habe gerade über einen Teil meines eigenen Codes nachgedacht und wie das mit try aussehen würde:

slurp, err := ioutil.ReadFile(path)
if err != nil {
    return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

Könnte werden:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

Ich bin mir nicht sicher, ob das besser ist. Es scheint Code viel schwieriger zu lesen zu machen. Aber vielleicht ist es einfach eine Frage der Gewöhnung.

@gbbr Du hast hier die Wahl. Du könntest es schreiben als:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

was Ihnen immer noch eine Menge Textbausteine ​​erspart, aber es viel klarer macht. Dies ist try nicht eigen. Nur weil Sie alles in einen einzigen Ausdruck quetschen können, bedeutet das nicht, dass Sie es tun sollten. Das gilt allgemein.

@griesemer Dieses Beispiel _ist_ inhärent zum Ausprobieren, Sie dürfen keinen Code verschachteln, der heute möglicherweise fehlschlägt - Sie sind gezwungen, Fehler mit Kontrollfluss zu behandeln. Ich möchte etwas von https://github.com/golang/go/issues/32825#issuecomment -507099786 / https://github.com/golang/go/issues/32825#issuecomment -507136111 aufklären, an das Sie antwortete https://github.com/golang/go/issues/32825#issuecomment -507358397. Später wurde das gleiche Problem erneut in https://github.com/golang/go/issues/32825#issuecomment -508813236 und https://github.com/golang/go/issues/32825#issuecomment -508937177 - dem letzten - diskutiert davon erkläre ich:

Gut, dass Sie mein zentrales Argument gegen try gelesen haben: Die Implementierung ist nicht restriktiv genug. Ich glaube, dass entweder die Implementierung zu allen Vorschlägen passen sollte, Anwendungsbeispiele, die prägnant und leicht lesbar sind.

_Oder_ der Vorschlag sollte Beispiele enthalten, die der Implementierung entsprechen, damit alle Personen, die darüber nachdenken, dem ausgesetzt werden können, was unweigerlich im Go-Code erscheinen wird. Zusammen mit all den Eckfällen, denen wir bei der Fehlersuche bei weniger als ideal geschriebener Software begegnen können, die in jeder Sprache / Umgebung auftritt. Es sollte Fragen beantworten wie Stack-Traces mit mehreren Verschachtelungsebenen aussehen, sind die Orte der Fehler leicht erkennbar? Was ist mit Methodenwerten, anonymen Funktionsliteralen? Welche Art von Stack-Trace erzeugen die folgenden, wenn die Zeile mit den Aufrufen von fn() fehlschlägt?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

Ich bin mir bewusst, dass viel vernünftiger Code geschrieben werden wird, aber wir stellen jetzt ein Werkzeug bereit, das es noch nie zuvor gegeben hat: die Möglichkeit, möglicherweise Code ohne klaren Kontrollfluss zu schreiben. Ich möchte also rechtfertigen, warum wir es überhaupt zulassen, ich möchte niemals meine Zeit mit dem Debuggen dieser Art von Code verschwenden. Weil ich weiß, dass ich es tun werde, hat mich die Erfahrung gelehrt, dass jemand es tun wird, wenn Sie es ihm erlauben. Dieser Jemand ist oft ein uninformiertes Ich.

Go bietet anderen Entwicklern und mir die wenigsten Möglichkeiten, Zeit zu verschwenden, indem wir uns darauf beschränken, dieselben profanen Konstrukte zu verwenden. Ich möchte das nicht ohne einen überwältigenden Nutzen verlieren. Ich glaube nicht, dass "weil try als Funktion implementiert ist" ein überwältigender Vorteil ist. Können Sie einen Grund nennen, warum das so ist?

Es wäre nützlich, einen Stack-Trace zu haben, der zeigt, wo das obige fehlschlägt, vielleicht ein zusammengesetztes Literal mit Feldern hinzuzufügen, die diese Funktion in die Mischung aufrufen? Ich frage danach, weil ich weiß, wie Stack-Traces heute für diese Art von Problem aussehen. Go bietet keine leicht verdaulichen Spalteninformationen in den Stack-Informationen, sondern nur die hexadezimale Funktionseintragsadresse. Mehrere Dinge beunruhigen mich darüber, wie z. B. die Konsistenz des Stack-Trace über Architekturen hinweg. Betrachten Sie beispielsweise diesen Code:

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
    fmt.Println(&bar{
        a: 1,
        c: 1,
        d: 1,
        b: &bar{
            a: 1,
            c: 1,
            d: dopanic(true) + dopanic(false),
        },
    })
}

Beachten Sie, wie der erste Playground beim linken Dopanic fehlschlägt, der zweite rechts, aber beide drucken einen identischen Stack-Trace:
https://play.golang.org/p/SYs1r4hBS7O
https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
    /tmp/sandbox709874298/prog.go:7
main.main()
    /tmp/sandbox709874298/prog.go:27 +0x40

Ich hätte erwartet, dass der zweite +0x41 oder ein Offset nach 0x40 ist, was verwendet werden könnte, um den tatsächlichen Anruf zu bestimmen, der innerhalb der Panik fehlgeschlagen ist. Selbst wenn wir die korrekten hexadezimalen Offsets erhalten haben, kann ich ohne zusätzliches Debuggen nicht feststellen, wo der Fehler aufgetreten ist. Heute ist dies ein Grenzfall, mit dem die Menschen selten konfrontiert werden. Wenn Sie eine verschachtelbare Version von try veröffentlichen, wird dies zur Norm, da sogar der Vorschlag eine try() + try() strconv enthält, die zeigt, dass es sowohl möglich als auch akzeptabel ist, try auf diese Weise zu verwenden.

1) Welche Änderungen an Stack-Traces planen Sie angesichts der obigen Informationen (falls vorhanden), damit ich immer noch feststellen kann, wo mein Code fehlgeschlagen ist?

2) Ist Try-Nesting erlaubt, weil Sie glauben, dass es so sein sollte? Wenn ja, worin sehen Sie die Vorteile von Try Nesting und wie können Sie Missbrauch verhindern? Ich denke, tryhard sollte angepasst werden, um verschachtelte trys durchzuführen, wo Sie es für akzeptabel halten, damit die Leute eine fundiertere Entscheidung darüber treffen können, wie es sich auf ihren Code auswirkt, da wir derzeit nur die besten / strengsten Verwendungsbeispiele erhalten. Dies wird uns eine Vorstellung davon geben, welche Art von vet Beschränkungen auferlegt werden, bis jetzt haben Sie gesagt, dass der Tierarzt die Verteidigung gegen unangemessene Versuche sein wird, aber wie wird das zustande kommen?

3) Ist try nesting eine Folge der Implementierung? Wenn ja, scheint dies nicht ein sehr schwaches Argument für die bemerkenswerteste Sprachänderung seit der Veröffentlichung von Go zu sein?

Ich denke, diese Änderung muss beim Try-Nesting mehr berücksichtigt werden. Jedes Mal, wenn ich darüber nachdenke, taucht irgendwo ein neuer Schmerzpunkt auf, und ich mache mir große Sorgen, dass all die potenziellen Negative nicht auftauchen, bis sie in freier Wildbahn aufgedeckt werden. Nesting bietet auch eine einfache Möglichkeit, Ressourcen zu verlieren, wie in https://github.com/golang/go/issues/32825#issuecomment -506882164 erwähnt, was heute nicht möglich ist. Ich denke, die "Vet"-Geschichte braucht einen viel konkreteren Plan mit Beispielen, wie sie Feedback geben wird, wenn sie als Verteidigung gegen die schädlichen try()-Beispiele verwendet wird, die ich hier gegeben habe, oder die Implementierung Kompilierzeitfehler liefern sollte für die Verwendung außerhalb Ihrer idealen Best Practices.

Bearbeiten: Ich habe in Gophers nach der Architektur von play.golang.org gefragt und jemand erwähnte, dass es über NaCl kompiliert wird, also wahrscheinlich nur eine Folge / ein Fehler davon. Aber ich könnte sehen, dass dies bei anderen Archs ein Problem darstellt. Ich denke, viele der Probleme, die sich aus der Einführung mehrerer Rückgaben pro Zeile ergeben könnten, wurden noch nicht vollständig untersucht, da sich die meisten Verwendungen auf eine vernünftige und saubere Verwendung einer einzelnen Zeile konzentrieren.

Oh nein, bitte fügen Sie diese "Magie" nicht in die Sprache ein.
Diese sehen und fühlen sich nicht wie der Rest der Sprache an.
Ich sehe bereits Code wie diesen, der überall auftaucht.

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

anstatt

a,b,err := f()
if err != nil {
...
}
...

oder

a,b,_:= f()

Das call if err.... -Muster war am Anfang etwas unnatürlich für mich, aber jetzt habe ich mich daran gewöhnt
Ich fühle mich einfacher, mit Fehlern umzugehen, da sie im Ausführungsfluss ankommen können, anstatt Wrapper/Handler zu schreiben, die eine Art Zustand verfolgen müssen, um nach dem Auslösen zu handeln.
Und wenn ich beschließe, Fehler zu ignorieren, um das Leben meiner Tastatur zu retten, bin ich mir bewusst, dass ich eines Tages in Panik geraten werde.

Ich habe sogar meine Gewohnheiten in VBScript geändert zu:

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

Ich mag diesen Vorschlag

Alle Bedenken, die ich hatte (z. B. sollte es idealerweise ein Schlüsselwort und kein eingebautes sein) werden durch das ausführliche Dokument angesprochen

Es ist nicht 100% perfekt, aber es ist eine Lösung, die gut genug ist, um a) ein tatsächliches Problem zu lösen und b) dies unter Berücksichtigung einer Menge Abwärtskompatibilität und anderer Probleme zu tun

Sicher, es macht etwas 'Magie', aber defer tut es auch. Der einzige Unterschied ist Schlüsselwort vs. eingebaut, und die Entscheidung, hier ein Schlüsselwort zu vermeiden, ist sinnvoll.

Ich habe das Gefühl, dass alle wichtigen Rückmeldungen zum try() -Vorschlag bereits geäußert wurden. Aber lassen Sie mich versuchen, zusammenzufassen:

1) try() verschiebt die vertikale Codekomplexität in die horizontale
2) Verschachtelte try()-Aufrufe sind genauso schwer zu lesen wie ternäre Operatoren
3) Führt einen unsichtbaren „Rückgabe“-Kontrollfluss ein, der visuell nicht unterscheidbar ist (im Vergleich zu eingerückten Blöcken, die mit dem Schlüsselwort return beginnen)
4) Verschlechtert die Fehlerumbruchpraxis (Funktionskontext statt einer bestimmten Aktion)
5) Teilt die #golang-Community und den Codestil (Anti-Gofmt)
6) Wird Entwickler oft dazu bringen, try() in if-err-nil und umgekehrt umzuschreiben (tryhard vs. Hinzufügen von Bereinigungslogik / zusätzliche Protokolle / besserer Fehlerkontext)

@VojtechVitek Ich denke, die Punkte, die Sie machen, sind subjektiv und können nur bewertet werden, wenn die Leute anfangen, sie ernsthaft zu verwenden.

Ich glaube jedoch, dass es einen technischen Punkt gibt, der nicht viel diskutiert wurde. Das Muster der Verwendung von defer für das Umschließen/Ausstatten von Fehlern hat Auswirkungen auf die Leistung, die über die einfachen Kosten von defer selbst hinausgehen, da Funktionen, die defer verwenden, nicht eingebettet werden können.

Dies bedeutet, dass die Verwendung try mit Fehlerumbruch zwei potenzielle Kosten verursacht, verglichen mit der Rückgabe eines umschlossenen Fehlers direkt nach einer err != nil -Prüfung:

  1. a defer für alle Pfade durch die Funktion, auch für erfolgreiche
  2. Verlust des Inlinings

Auch wenn einige beeindruckende Leistungsverbesserungen für defer anstehen, sind die Kosten immer noch ungleich Null.

try hat viel Potenzial, daher wäre es gut, wenn das Go-Team das Design überarbeiten könnte, um eine Art Umhüllung am Punkt des Fehlers zu ermöglichen, anstatt präventiv über defer .

vet" Story braucht einen viel konkreteren Plan

Tierarztgeschichte ist Märchen. Es funktioniert nur für bekannte Typen und ist für benutzerdefinierte Typen nutzlos.

Hallo allerseits,

Unser Ziel mit Vorschlägen wie diesem ist es, eine gemeinschaftsweite Diskussion über Auswirkungen, Kompromisse und das weitere Vorgehen zu führen und diese Diskussion dann zu nutzen, um über den weiteren Weg zu entscheiden.

Aufgrund der überwältigenden Reaktion der Community und der ausführlichen Diskussion hier markieren wir diesen Vorschlag vorzeitig als abgelehnt .

In Bezug auf technisches Feedback hat diese Diskussion hilfreich einige wichtige Überlegungen identifiziert, die wir übersehen haben, insbesondere die Auswirkungen auf das Hinzufügen von Debugging-Ausdrucken und die Analyse der Codeabdeckung.

Noch wichtiger ist, dass wir die vielen Leute deutlich gehört haben, die argumentiert haben, dass dieser Vorschlag nicht auf ein lohnendes Problem abzielt. Wir glauben immer noch, dass die Fehlerbehandlung in Go nicht perfekt ist und sinnvoll verbessert werden kann, aber es ist klar, dass wir als Community mehr darüber sprechen müssen, welche spezifischen Aspekte der Fehlerbehandlung Probleme sind, die wir ansprechen sollten.

Was die Diskussion des zu lösenden Problems anbelangt, so haben wir im vergangenen August versucht, unsere Vision des Problems in der „ Go 2-Fehlerbehandlungsproblemübersicht“ darzulegen , aber im Nachhinein haben wir diesem Teil nicht genug Aufmerksamkeit geschenkt und nicht genug dazu ermutigt Diskussion darüber, ob das konkrete Problem das richtige war. Der try -Vorschlag mag eine gute Lösung für das dort beschriebene Problem sein, aber für viele von Ihnen ist es einfach kein Problem, das es zu lösen gilt. In Zukunft müssen wir besser auf diese frühen Problemstellungen aufmerksam machen und sicherstellen, dass es eine breite Übereinstimmung über das zu lösende Problem gibt.

(Es ist auch möglich, dass die Problemstellung zur Fehlerbehandlung durch die Veröffentlichung eines generischen Designentwurfs am selben Tag völlig in den Hintergrund gedrängt wurde.)

Zum breiteren Thema, was man an der Go-Fehlerbehandlung verbessern sollte, würden wir uns sehr über Erfahrungsberichte darüber freuen, welche Aspekte der Fehlerbehandlung in Go für Sie in Ihren eigenen Codebasen und Arbeitsumgebungen am problematischsten sind und wie viel Einfluss eine gute Lösung hätte in Ihrer eigenen Entwicklung haben. Wenn Sie einen solchen Bericht schreiben, posten Sie bitte einen Link auf der Go2ErrorHandlingFeedback-Seite .

Vielen Dank an alle, die an dieser Diskussion teilgenommen haben, hier und anderswo. Wie Russ Cox bereits betont hat, sind gemeinschaftsweite Diskussionen wie diese Open Source im besten Sinne . Wir schätzen wirklich jede Hilfe bei der Prüfung dieses speziellen Vorschlags und ganz allgemein bei der Diskussion der besten Möglichkeiten zur Verbesserung des Zustands der Fehlerbehandlung in Go.

Robert Griesemer, für das Proposal Review Committee.

Vielen Dank, Go-Team, für die Arbeit, die in den Versuchsvorschlag geflossen ist. Und danke an die Kommentatoren, die damit zu kämpfen und Alternativen vorgeschlagen haben. Manchmal nehmen diese Dinge ein Eigenleben an. Vielen Dank an das Go-Team für das Zuhören und die angemessene Reaktion.

Yay!

Vielen Dank an alle, die das herausgekramt haben, damit wir das bestmögliche Ergebnis erzielen können!

Der Aufruf ist für eine Liste von Problemen und negativen Erfahrungen mit der Fehlerbehandlung von Go. Aber,
Ich und Teams sind sehr zufrieden mit xerrors.As, xerrors.Is und xerrors.Errorf in der Produktion. Diese neuen Ergänzungen ändern die Fehlerbehandlung auf wunderbare Weise für uns, nachdem wir die Änderungen vollständig angenommen haben. Im Moment sind wir auf keine Probleme oder Bedürfnisse gestoßen, die nicht angesprochen werden.

@griesemer wollte mich nur bei dir (und wahrscheinlich vielen anderen, die mit dir gearbeitet haben) für deine Geduld und Mühe bedanken.

gut!

@griesemer Vielen Dank an Sie und alle anderen im Go-Team, dass Sie unermüdlich auf all das Feedback hören und all unsere unterschiedlichen Meinungen ertragen.

Vielleicht ist jetzt also ein guter Zeitpunkt, um diesen Thread zu schließen und sich zukünftigen Dingen zuzuwenden?

@griesemer @rsc und @all , cool, danke an alle. Für mich ist es eine großartige Diskussion / Identifizierung / Klärung. Die Verbesserung einiger Teile wie „Fehler“ in go erfordert eine offenere Diskussion (in Vorschlägen und Kommentaren ...), um zuerst die Kernprobleme zu identifizieren / zu klären.

ps, die x/xerrors sind erstmal gut.

(Könnte sinnvoll sein, diesen Thread auch zu sperren ...)

Vielen Dank an das Team und die Community für das Engagement. Ich finde es toll, wie viele Leute sich für Go interessieren.

Ich hoffe wirklich, dass die Community zuerst die Mühe und das Können sieht, die überhaupt in den Versuchsvorschlag geflossen sind, und dann den Geist des darauf folgenden Engagements, das uns geholfen hat, diese Entscheidung zu treffen. Die Zukunft von Go ist sehr rosig, wenn wir so weitermachen können, besonders wenn wir alle eine positive Einstellung bewahren können.

func M() (Daten, Fehler){
a, err1 := A()
b, err2 := B()
gib b zurück, null
} => (if err1 != nil) {return a, err1}.
(if err2 != nil){ return b, err2}

Okay ... Ich mochte diesen Vorschlag, aber ich liebe die Art und Weise, wie die Community und das Go-Team reagiert und sich an einer konstruktiven Diskussion beteiligt haben, auch wenn es manchmal etwas rau war.

Ich habe jedoch 2 Fragen zu diesem Ergebnis:
1/ Ist „Error Handling“ noch ein Forschungsgebiet?
2/ Werden aufgeschobene Verbesserungen neu priorisiert?

Dies beweist einmal mehr, dass die Go-Community gehört wird und kontroverse Sprachänderungsvorschläge diskutieren kann. Wie die Änderungen, die es in die Sprache schaffen, sind die Änderungen, die es nicht tun, eine Verbesserung. Vielen Dank an das Go-Team und die Community für die harte Arbeit und die zivilisierte Diskussion rund um diesen Vorschlag!

Exzellent!

super, sehr hilfreich

Vielleicht bin ich zu sehr an Go gebunden, aber ich denke, hier wurde ein Punkt gezeigt, da
Russ beschrieb: Es gibt einen Punkt, an dem die Gemeinschaft nicht nur eins ist
Headless Chicken, es ist eine Kraft, mit der man rechnen muss und die man sein muss
zum eigenen Wohl genutzt.

Dank der Koordination durch das Go-Team können wir das
Alle sind stolz darauf, dass wir zu einem Ergebnis gekommen sind, mit dem wir leben können
werden zweifellos wiederkommen, wenn die Bedingungen reifer sind.

Hoffen wir, dass der hier gefühlte Schmerz uns in Zukunft gute Dienste leisten wird
(wäre es sonst nicht traurig?).

Lucio.

Ich bin mit der Entscheidung nicht einverstanden. Ich unterstütze jedoch absolut den Ansatz, den das Go-Team verfolgt hat. Eine gemeinschaftsweite Diskussion zu führen und Feedback von Entwicklern zu berücksichtigen, ist das, was Open Source sein sollte.

Ich bin gespannt, ob die Defer-Optimierungen auch noch kommen werden. Ich mag es sehr, Fehler damit und xerrors zusammen zu kommentieren, und es ist im Moment zu kostspielig.

@pierrec Ich denke, wir brauchen ein klareres Verständnis dafür, welche Änderungen in der Fehlerbehandlung nützlich wären. Einige der Fehlerwertänderungen werden in der kommenden Version 1.13 (https://tip.golang.org/doc/go1.13#errors) enthalten sein, und wir werden damit Erfahrungen sammeln. Im Laufe dieser Diskussion haben wir viele, viele Vorschläge zur syntaktischen Fehlerbehandlung gesehen, und es wäre hilfreich, wenn die Leute über Vorschläge abstimmen und Kommentare abgeben könnten, die besonders nützlich erscheinen. Ganz allgemein wären, wie @griesemer sagte, Erfahrungsberichte hilfreich.

Es wäre auch nützlich, besser zu verstehen, inwieweit die Fehlerbehandlungssyntax für Sprachanfänger problematisch ist, obwohl dies schwer zu bestimmen sein wird.

In https://golang.org/cl/183677 wird aktiv daran gearbeitet, die Leistung von defer zu verbessern, und wenn nicht auf ein größeres Hindernis gestoßen wird, würde ich erwarten, dass dies in die Version 1.14 aufgenommen wird.

@griesemer @ianlancetaylor @rsc Planen Sie immer noch, die Ausführlichkeit der Fehlerbehandlung anzugehen, indem ein weiterer Vorschlag einige oder alle der hier angesprochenen Probleme löst?

Also, spät zur Party, da dies bereits abgelehnt wurde, aber für zukünftige Diskussionen zu diesem Thema, was ist mit einer ternären bedingten Rückgabesyntax? (Ich habe in meinem Scan des Themas oder beim Betrachten der Ansicht, die Russ Cox auf Twitter gepostet hat, nichts Ähnliches gesehen.) Beispiel:

f, err := Foo()
return err != nil ? nil, err

Gibt nil, err zurück, wenn err nicht null ist, setzt die Ausführung fort, wenn err null ist. Das Erklärungsformular wäre

return <boolean expression> ? <return values>

und das wäre syntaktischer Zucker für:

if <boolean expression> {
    return <return values>
}

Der Hauptvorteil besteht darin, dass dies flexibler ist als ein check -Schlüsselwort oder eine try -integrierte Funktion, da es bei mehr als nur Fehlern ausgelöst werden kann (z. B. return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo") bei mehr als nur der Fehler, der nicht null ist (z. B. return err != nil && err != io.EOF ? nil, err ), usw., während er beim Lesen immer noch ziemlich intuitiv zu verstehen ist (insbesondere für diejenigen, die es gewohnt sind, ternäre Operatoren in anderen Sprachen zu lesen).

Es stellt auch sicher, dass die Fehlerbehandlung _immer noch am Ort des Aufrufs stattfindet_ und nicht automatisch aufgrund einer Zurückstellungsanweisung geschieht. Einer der größten Kritikpunkte, die ich an dem ursprünglichen Vorschlag hatte, war, dass er versucht, die eigentliche _Behandlung_ von Fehlern in gewisser Weise zu einem impliziten Prozess zu machen, der einfach automatisch passiert, wenn der Fehler nicht Null ist, ohne dass ein klarer Hinweis auf den Kontrollfluss besteht wird zurückgegeben, wenn der Funktionsaufruf einen Nicht-Null-Fehler zurückgibt. Der ganze _Punkt_ von Go, explizite Fehlerrückgaben anstelle eines ausnahmeähnlichen Systems zu verwenden, besteht darin, Entwickler zu ermutigen, ihre Fehler explizit und absichtlich zu überprüfen und zu behandeln, anstatt sie einfach im Stapel nach oben verbreiten zu lassen, um theoretisch an einem höheren Punkt behandelt zu werden hoch. Zumindest eine explizite, wenn auch bedingte return-Anweisung kommentiert klar, was vor sich geht.

@ngrilly Wie @griesemer sagte, ich denke, wir müssen besser verstehen, welche Aspekte der Fehlerbehandlung Go-Programmierer am problematischsten finden.

Persönlich denke ich nicht, dass es sich lohnt, einen Vorschlag zu machen, der ein wenig Ausführlichkeit entfernt. Schließlich funktioniert die Sprache heute gut genug. Jede Änderung ist mit Kosten verbunden. Wenn wir etwas ändern wollen, brauchen wir einen signifikanten Nutzen. Ich denke, dieser Vorschlag hat einen erheblichen Vorteil in Bezug auf die reduzierte Ausführlichkeit gebracht, aber es gibt eindeutig einen erheblichen Teil der Go-Programmierer, die der Meinung sind, dass die dadurch verursachten zusätzlichen Kosten zu hoch waren. Ob es hier einen Mittelweg gibt, weiß ich nicht. Und ich weiß nicht, ob sich das Problem überhaupt lohnt anzusprechen.

@kaedys Dieses geschlossene und äußerst ausführliche Thema ist definitiv nicht der richtige Ort, um bestimmte alternative Syntaxen für die Fehlerbehandlung zu diskutieren.

@ianlancetaylor

Ich denke, dieser Vorschlag hat einen erheblichen Vorteil in Bezug auf die reduzierte Ausführlichkeit gebracht, aber es gibt eindeutig einen erheblichen Teil der Go-Programmierer, die der Meinung sind, dass die dadurch verursachten zusätzlichen Kosten zu hoch waren.

Ich fürchte, es gibt eine Selbstselektionsverzerrung. Go ist bekannt für seine ausführliche Fehlerbehandlung und den Mangel an Generika. Das zieht natürlich Entwickler an, denen diese beiden Probleme egal sind. In der Zwischenzeit verwenden andere Entwickler ihre aktuellen Sprachen (Java, C++, C#, Python, Ruby usw.) und/oder wechseln aus diesem Grund zu moderneren Sprachen (Rust, TypeScript, Kotlin, Swift, Elixir usw.). . Ich kenne viele Entwickler, die Go hauptsächlich aus diesem Grund meiden.

Ich denke auch, dass eine Bestätigungsverzerrung im Spiel ist. Gophers wurden verwendet, um die ausführliche Fehlerbehandlung und den Mangel an Fehlerbehandlung zu verteidigen, wenn Leute Go kritisieren. Dies erschwert die objektive Bewertung eines Vorschlags wie try.

Steve Klabnik hat vor einigen Tagen einen interessanten Kommentar auf Reddit veröffentlicht. Er war gegen die Einführung ? in Rust, weil es "zwei Arten sei, dasselbe zu schreiben" und es "zu implizit" sei. Aber jetzt, nachdem er mehr als ein paar Codezeilen mit geschrieben hat, ist ? eines seiner Lieblingsfeatures.

@ngrilly Ich stimme deinen Kommentaren zu. Diese Vorurteile sind sehr schwer zu vermeiden. Was sehr hilfreich wäre, wäre ein klareres Verständnis dafür, wie viele Leute Go aufgrund der ausführlichen Fehlerbehandlung meiden. Ich bin mir sicher, dass die Zahl nicht Null ist, aber es ist schwierig zu messen.

Das heißt, es ist auch wahr, dass try eine neue Änderung im Kontrollfluss eingeführt hat, die schwer zu erkennen war, und dass try zwar dazu gedacht war, bei der Fehlerbehandlung zu helfen, aber nicht beim Kommentieren von Fehlern half .

Danke für das Zitat von Steve Klabnik. Auch wenn ich das Gefühl schätze und ihm zustimme, sollte man bedenken, dass Rust als Sprache etwas bereitwilliger scheint, sich auf syntaktische Details zu verlassen, als dies bei Go der Fall war.

Als Unterstützer dieses Vorschlags bin ich natürlich enttäuscht, dass er jetzt zurückgezogen wurde, obwohl ich denke, dass das Go-Team unter den gegebenen Umständen das Richtige getan hat.

Eine Sache, die jetzt ziemlich klar zu sein scheint, ist, dass die Mehrheit der Go-Benutzer die Ausführlichkeit der Fehlerbehandlung nicht als Problem betrachtet, und ich denke, damit müssen wir anderen leben, auch wenn es potenzielle neue Benutzer abschreckt .

Ich habe aufgehört zu zählen, wie viele alternative Vorschläge ich gelesen habe, und obwohl einige ziemlich gut sind, habe ich keinen gesehen, von dem ich dachte, dass er es wert wäre, angenommen zu werden, wenn try ins Gras beißen würde. Daher scheint mir die Chance, dass jetzt ein Mittelwegsvorschlag entsteht, gering.

Positiver ist zu vermerken, dass die aktuelle Diskussion Möglichkeiten aufgezeigt hat, wie alle potenziellen Fehler in einer Funktion auf die gleiche Weise und an der gleichen Stelle dekoriert werden können (mit defer oder sogar goto ). was ich zuvor nicht in Betracht gezogen hatte, und ich hoffe, das Go-Team wird zumindest erwägen, go fmt zu ändern, damit einzelne Anweisungen if in eine Zeile geschrieben werden können, was zumindest eine Fehlerbehandlung ermöglicht _look_ kompakter, auch wenn es eigentlich keine Boilerplate entfernt.

@pierrec

1/ Ist „Error Handling“ noch ein Forschungsgebiet?

Und das seit über 50 Jahren! Eine umfassende Theorie oder gar eine praktische Anleitung für eine konsequente und systematische Fehlerbehandlung scheint es nicht zu geben. Im Go-Land (wie in anderen Sprachen) herrscht sogar Verwirrung darüber, was ein Fehler ist. Zum Beispiel kann ein EOF eine außergewöhnliche Bedingung sein, wenn Sie versuchen, eine Datei zu lesen, aber warum ist es ein Fehler? Ob das tatsächlich ein Fehler ist oder nicht, hängt wirklich vom Kontext ab. Und es gibt andere solche Probleme.

Vielleicht ist eine Diskussion auf höherer Ebene erforderlich (allerdings nicht hier).

Vielen Dank an @griesemer @rsc und alle anderen, die mit Vorschlägen zu tun haben. Viele andere haben es oben gesagt, und es muss wiederholt werden, dass Ihre Bemühungen, das Problem zu durchdenken, den Vorschlag zu schreiben und ihn in gutem Glauben zu diskutieren, geschätzt werden. Danke.

@ianlancetaylor

Danke für das Zitat von Steve Klabnik. Auch wenn ich das Gefühl schätze und ihm zustimme, sollte man bedenken, dass Rust als Sprache etwas bereitwilliger scheint, sich auf syntaktische Details zu verlassen, als dies bei Go der Fall war.

Ich stimme im Allgemeinen zu, dass Rust sich mehr auf syntaktische Details verlässt als Go, aber ich glaube nicht, dass dies auf diese spezielle Diskussion über die Ausführlichkeit der Fehlerbehandlung zutrifft.

Fehler sind Werte in Rust wie in Go. Sie können sie wie in Go mit der Standardablaufsteuerung handhaben. In den ersten Versionen von Rust war es die einzige Möglichkeit, Fehler zu behandeln, wie in Go. Dann führten sie das try! -Makro ein, das dem eingebauten Funktionsvorschlag try überraschend ähnlich ist. Schließlich fügten sie den Operator ? hinzu, der eine syntaktische Variation und Verallgemeinerung des Makros try! ist, aber dies ist nicht notwendig, um die Nützlichkeit von try und die Tatsache zu demonstrieren dass die Rust-Community es nicht bereut, es hinzugefügt zu haben.

Ich bin mir der massiven Unterschiede zwischen Go und Rust bewusst, aber zum Thema Ausführlichkeit der Fehlerbehandlung denke ich, dass ihre Erfahrung auf Go übertragbar ist. Die RFCs und Diskussionen zu try! und ? sind wirklich lesenswert. Ich war wirklich überrascht, wie ähnlich die Themen und Argumente für und gegen die Sprachänderungen sind.

@griesemer , Sie haben die Entscheidung angekündigt, den try -Vorschlag in seiner aktuellen Form abzulehnen, aber Sie haben nicht gesagt, was das Go-Team als Nächstes plant.

Planen Sie immer noch, die Ausführlichkeit der Fehlerbehandlung mit einem anderen Vorschlag anzugehen, der die in dieser Diskussion angesprochenen Probleme lösen würde (Debugging von Drucken, Codeabdeckung, bessere Fehlerdekoration usw.)?

Ich stimme im Allgemeinen zu, dass Rust sich mehr auf syntaktische Details verlässt als Go, aber ich glaube nicht, dass dies auf diese spezielle Diskussion über die Ausführlichkeit der Fehlerbehandlung zutrifft.

Da andere immer noch ihren Senf hinzufügen, denke ich, dass es noch Raum für mich gibt, dasselbe zu tun.

Obwohl ich seit 1987 programmiere, arbeite ich erst seit etwa einem Jahr mit Go. Als ich vor etwa 18 Monaten nach einer neuen Sprache suchte, um bestimmte Anforderungen zu erfüllen, sah ich mir sowohl Go als auch Rust an. Ich entschied mich für Go, weil ich der Meinung war, dass der Go-Code viel einfacher zu erlernen und zu verwenden war, und dieser Go-Code viel besser lesbar war, weil Go Worte zu bevorzugen scheint, um Bedeutungen statt knapper Symbole zu vermitteln.

Ich für meinen Teil wäre also sehr unglücklich, wenn Go mehr Rust-ähnlich werden würde, einschließlich der Verwendung von Ausrufezeichen ( ! ) und Fragezeichen ( ? ), um eine Bedeutung zu implizieren.

In ähnlicher Weise denke ich, dass die Einführung von Makros die Natur von Go verändern und zu Tausenden von Go-Dialekten führen würde, wie es effektiv bei Ruby der Fall ist. Ich hoffe also, dass Makros auch nie Go hinzugefügt werden, obwohl ich vermute, dass die Wahrscheinlichkeit, dass dies passiert, meiner Meinung nach zum Glück gering ist.

jmtcw

@ianlancetaylor

Was sehr hilfreich wäre, wäre ein klareres Verständnis dafür, wie viele Leute Go aufgrund der ausführlichen Fehlerbehandlung meiden. Ich bin mir sicher, dass die Zahl nicht Null ist, aber es ist schwierig zu messen.

Es ist an sich keine „Maßnahme“, aber diese Hacker News-Diskussion enthält Dutzende von Kommentaren von Entwicklern, die mit der Go-Fehlerbehandlung aufgrund ihrer Ausführlichkeit unzufrieden sind (und einige Kommentare erläutern ihre Argumentation und geben Codebeispiele): https://news.ycombinator. com/item?id=20454966.

Zunächst einmal vielen Dank an alle für das unterstützende Feedback zur endgültigen Entscheidung, auch wenn diese Entscheidung für viele nicht zufriedenstellend war. Das war wirklich eine Teamleistung, und ich bin wirklich froh, dass wir es alle geschafft haben, die intensiven Diskussionen auf insgesamt zivile und respektvolle Weise zu überstehen.

@ngrilly Wenn ich nur für mich selbst spreche, denke ich immer noch, dass es schön wäre, irgendwann die Ausführlichkeit der Fehlerbehandlung anzugehen. Allerdings haben wir im letzten halben Jahr und insbesondere in den letzten 3 Monaten ziemlich viel Zeit und Energie darauf verwendet, und wir waren mit dem Vorschlag recht zufrieden, haben jedoch die möglichen Reaktionen darauf offensichtlich unterschätzt. Jetzt macht es sehr viel Sinn, einen Schritt zurückzutreten, das Feedback zu verdauen und zu destillieren und dann über die besten nächsten Schritte zu entscheiden.

Da wir nicht über unbegrenzte Ressourcen verfügen, sehe ich realistisch gesehen, dass das Nachdenken über die Sprachunterstützung für die Fehlerbehandlung zugunsten von mehr Fortschritt an anderen Fronten, insbesondere der Arbeit an Generika, zumindest für die, etwas in den Hintergrund rücken wird nächsten paar Monate. if err != nil mag ärgerlich sein, aber es ist kein Grund für dringende Maßnahmen.

Wenn Sie die Diskussion fortsetzen möchten, möchte ich allen höflich empfehlen, von hier wegzugehen und die Diskussion woanders fortzusetzen, in einer separaten Ausgabe (wenn es einen klaren Vorschlag gibt) oder in anderen Foren, die besser für eine offene Diskussion geeignet sind. Dieses Thema ist schließlich abgeschlossen. Danke.

Ich fürchte, es gibt eine Selbstselektionsverzerrung.

Ich möchte hier und jetzt einen neuen Begriff prägen: „Creator Bias“. Wenn jemand bereit ist, die Arbeit zu erledigen, sollte ihm im Zweifelsfall zugesprochen werden.

Es ist sehr einfach für die Erdnuss-Galerie, in unabhängigen Foren laut und breit zu schreien, wie sie eine vorgeschlagene Lösung für ein Problem nicht mögen. Es ist auch für jeden sehr einfach, einen unvollständigen Versuch mit 3 Absätzen für eine andere Lösung zu schreiben (ohne dass nebenbei wirkliche Arbeit präsentiert wird). Wenn man dem Status quo zustimmt, ok. Gutes Argument. Wenn Sie etwas anderes als Lösung ohne vollständigen Vorschlag präsentieren, erhalten Sie -10.000 Punkte.

Ich unterstütze oder bin nicht gegen Try, aber ich vertraue dem Urteil von Go Teams in dieser Angelegenheit, bisher hat ihr Urteil eine ausgezeichnete Sprache geliefert, also denke ich, was auch immer sie entscheiden, wird für mich funktionieren, Versuch oder nicht, denke ich Wir müssen als Außenstehende verstehen, dass die Betreuer einen breiteren Einblick in die Angelegenheit haben. Syntax können wir den ganzen Tag diskutieren. Ich möchte allen danken, die an go gearbeitet haben oder versuchen, es im Moment zu verbessern, für ihre Bemühungen. Wir sind dankbar und freuen uns auf neue (nicht rückwärts brechende) Verbesserungen in den Sprachbibliotheken und der Runtime, falls welche in Betracht gezogen werden nützlich von euch.

Es ist auch für jeden sehr einfach, einen unvollständigen Versuch mit 3 Absätzen für eine andere Lösung zu schreiben (ohne dass nebenbei wirkliche Arbeit präsentiert wird).

Das einzige, was ich (und eine Reihe anderer) try nützlich machen wollte, war ein optionales Argument, das es erlaubt, eine umschlossene Version des Fehlers anstelle des unveränderten Fehlers zurückzugeben. Ich glaube nicht, dass das viel Designarbeit erforderte.

Ach nein.

Ich verstehe. Wollen Sie etwas anderes machen als andere Sprachen.

Vielleicht sollte jemand dieses Problem sperren? Die Diskussion ist wahrscheinlich woanders besser geeignet.

Dieses Problem ist bereits so lang, dass das Sperren sinnlos erscheint.

Bitte beachten Sie, dass dieses Thema geschlossen ist und die Kommentare, die Sie hier machen, mit ziemlicher Sicherheit für immer ignoriert werden. Wenn das für dich in Ordnung ist, kommentiere weg.

Falls jemand das try-Wort hasst, das ihn an die Sprache Java, C * denken lässt, rate ich, nicht 'try' zu verwenden, sondern andere Wörter wie 'help' oder 'must' oder 'checkError' ... (ignorieren Sie mich)

Falls jemand das try-Wort hasst, das ihn an die Sprache Java, C * denken lässt, rate ich, nicht 'try' zu verwenden, sondern andere Wörter wie 'help' oder 'must' oder 'checkError' ... (ignorieren Sie mich)

Es wird immer überlappende Schlüsselwörter und Konzepte geben, die kleine oder große semantische Unterschiede in Sprachen haben, die ziemlich nahe beieinander liegen (wie Sprachen der C-Familie). Eine Sprachfunktion sollte innerhalb der Sprache selbst keine Verwirrung stiften, Unterschiede zwischen Sprachen werden immer vorkommen.

Schlecht. Dies ist ein Anti-Muster, respektloser Autor dieses Vorschlags

@alersenkevich Bitte sei höflich. Siehe https://golang.org/conduct.

Ich glaube, ich bin froh über die Entscheidung, damit nicht weiterzumachen. Für mich fühlte sich das wie ein schneller Hack an, um ein kleines Problem bezüglich if err != nil in mehreren Zeilen zu lösen. Wir wollen Go nicht mit kleinen Schlüsselwörtern aufblähen, um kleinere Dinge wie diese zu lösen, oder? Deshalb fühlt sich der Vorschlag mit hygienischen Makros https://github.com/golang/go/issues/32620 besser an. Es versucht, eine generischere Lösung zu sein, um mehr Flexibilität mit mehr Dingen zu eröffnen. Dort laufen Syntax- und Verwendungsdiskussionen, also denken Sie nicht nur, ob es sich um C/C++-Makros handelt. Der Punkt dort ist, einen besseren Weg zu diskutieren, um Makros zu machen. Damit könnten Sie Ihren eigenen Versuch umsetzen.

Ich würde mich über Feedback zu einem ähnlichen Vorschlag freuen, der ein Problem mit der aktuellen Fehlerbehandlung behandelt https://github.com/golang/go/issues/33161.

Ehrlich gesagt sollte dies wieder geöffnet werden, von allen Vorschlägen zur Behandlung von Fehlern ist dies der vernünftigste.

@OneOfOne respektvoll, ich bin anderer Meinung, dass dies wiedereröffnet werden sollte. Dieser Thread hat festgestellt, dass es echte Einschränkungen bei der Syntax gibt. Vielleicht haben Sie Recht, dass dies der "vernünftigste" Vorschlag ist: aber ich glaube, dass der Status quo noch vernünftiger ist.

Ich stimme zu, dass if err != nil viel zu oft in Go geschrieben wird, aber eine einzigartige Möglichkeit, von einer Funktion zurückzukehren, verbessert die Lesbarkeit enorm. Während ich mich im Allgemeinen hinter Vorschlägen stellen kann, die Boilerplate-Code reduzieren, sollten die Kosten meiner Meinung nach niemals die Lesbarkeit sein.

Ich weiß, dass viele Entwickler sich über die "langhändige" Fehlerprüfung beklagen, aber ehrlich gesagt steht die Kürze oft im Widerspruch zur Lesbarkeit. Go hat hier und anderswo viele etablierte Muster, die eine bestimmte Vorgehensweise fördern, und meiner Erfahrung nach ist das Ergebnis zuverlässiger Code, der gut altert. Dies ist entscheidend: Echter Code muss während seiner Lebensdauer viele Male gelesen und verstanden werden, wird aber immer nur einmal geschrieben. Kognitiver Overhead ist selbst für erfahrene Entwickler ein echter Kostenfaktor.

Anstatt:

f := try(os.Open(filename))

Ich würde erwarten:

f := try os.Open(filename)

Bitte beachten Sie, dass dieses Thema geschlossen ist und die Kommentare, die Sie hier machen, mit ziemlicher Sicherheit für immer ignoriert werden. Wenn das für dich in Ordnung ist, kommentiere weg.
—@ianlancetaylor

Es wäre schön, wenn wir try für einen Codeblock neben der aktuellen Art der Fehlerbehandlung verwenden könnten.
Etwas wie das:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

Der obige Code scheint sauberer zu sein als der ursprüngliche Kommentar. Ich wünschte, ich könnte das bezwecken.

Ich habe einen neuen Vorschlag #35179 gemacht

val := versuche f() (err){
Panik (äh)
}

Hoffentlich:

i, err := strconv.Atoi("1")
if err {
    println("ERROR")
} else {
    println(i)
}

oder

i, err := strconv.Atoi("1")
if err {
    io.EOF:
        println("EOF")
    io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@myroid Ich hätte nichts dagegen, wenn Ihr zweites Beispiel in Form einer switch-else -Anweisung etwas allgemeiner gestaltet wird:

„Geh
i, err := strconv.Atoi("1")
Schaltfehler != nil; irren {
Fall io.EOF:
println("EOF")
Fall io.ErrShortWrite:
println("ErrShortWrite")
} anders {
println(i)
}

@piotrkowalczuk Dein Code sieht viel besser aus als meiner. Ich denke, der Code könnte prägnanter sein.

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

Dies berücksichtigt nicht die Option, dass es ein Auge eines anderen Typs geben wird

Es muss sein
Fallfehler!=nil

Für Fehler hat der Entwickler nicht explizit festgehalten

Am Freitag, den 8. November 2019 um 12:06 Uhr schrieb Yang Fan, [email protected] :

@piotrkowalczuk https://github.com/piotrkowalczuk Ihr Code sieht viel aus
besser als meins. Ich denke, der Code könnte prägnanter sein.

i, err := strconv.Atoi("1")switch err {case io.EOF:
println("EOF")case io.ErrShortWrite:
println("ErrShortWrite")
} anders {
println(i)
}


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY#issuecomment-55150
oder abbestellen
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.

switch braucht kein else , es hat default .

Ich habe https://github.com/golang/go/issues/39890 geöffnet, das etwas Ähnliches wie guard von Swift vorschlägt, sollte einige der Bedenken mit diesem Vorschlag ausräumen:

  • Kontrollfluss
  • Lokalität der Fehlerbehandlung
  • Lesbarkeit

Es hat nicht viel Anklang gefunden, könnte aber für diejenigen von Interesse sein, die hier kommentiert haben.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen