Go: Vorschlag: Go 2: Fehlerbehandlung vereinfachen mit || Fehler-Suffix

Erstellt am 25. Juli 2017  ·  519Kommentare  ·  Quelle: golang/go

Es gab viele Vorschläge zur Vereinfachung der Fehlerbehandlung in Go, die alle auf der allgemeinen Beschwerde basieren, dass zu viel Go-Code die Zeilen enthält

if err != nil {
    return err
}

Ich bin mir nicht sicher, ob es hier ein Problem gibt, das gelöst werden muss, aber da es immer wieder auftaucht, werde ich diese Idee veröffentlichen.

Eines der Kernprobleme bei den meisten Vorschlägen zur Vereinfachung der Fehlerbehandlung besteht darin, dass sie nur zwei Arten der Fehlerbehandlung vereinfachen, aber es gibt tatsächlich drei:

  1. ignoriere den Fehler
  2. den Fehler unverändert zurückgeben
  3. den Fehler mit zusätzlichen Kontextinformationen zurückgeben

Es ist bereits leicht (vielleicht zu einfach), den Fehler zu ignorieren (siehe #20803). Viele existierende Vorschläge zur Fehlerbehandlung machen es einfacher, den Fehler unverändert zurückzugeben (zB #16225, #18721, #21146, #21155). Einige machen es einfacher, den Fehler mit zusätzlichen Informationen zurückzugeben.

Dieser Vorschlag basiert lose auf den Shell-Sprachen Perl und Bourne, fruchtbaren Quellen für Sprachideen. Wir führen eine neue Art von Anweisung ein, ähnlich einer Ausdrucksanweisung: ein Aufrufausdruck gefolgt von || . Die Grammatik lautet:

PrimaryExpr Arguments "||" Expression

In ähnlicher Weise führen wir eine neue Art von Zuweisungsanweisungen ein:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Obwohl die Grammatik im Nicht-Zuweisungsfall jeden Typ nach || akzeptiert, ist der einzig zulässige Typ der vordeklarierte Typ error . Der auf || folgende Ausdruck muss einen Typ haben, der error zuweisbar ist. Es darf kein boolescher Typ sein, nicht einmal ein benannter boolescher Typ, der error zuweisbar ist. (Die letztere Einschränkung ist erforderlich, um diesen Vorschlag mit der vorhandenen Sprache abwärtskompatibel zu machen.)

Diese neuen Anweisungsarten sind nur im Rumpf einer Funktion zulässig, die mindestens einen Ergebnisparameter hat, und der letzte Ergebnisparameter muss vom Typ error vordeklariert sein. Die aufgerufene Funktion muss ebenfalls mindestens einen Ergebnisparameter haben, und der Typ des letzten Ergebnisparameters muss der vordeklarierte Typ error .

Bei der Ausführung dieser Anweisungen wird der Aufrufausdruck wie gewohnt ausgewertet. Handelt es sich um eine Zuweisungsanweisung, werden die Aufrufergebnisse wie gewohnt den linken Operanden zugewiesen. Dann wird das letzte Aufrufergebnis, das wie oben beschrieben vom Typ error , mit nil verglichen. Wenn das Ergebnis des letzten Aufrufs nicht nil , wird implizit eine return-Anweisung ausgeführt. Wenn die aufrufende Funktion mehrere Ergebnisse hat, wird der Nullwert für alle Ergebnisse außer dem letzten zurückgegeben. Als letztes Ergebnis wird der auf || folgende Ausdruck zurückgegeben. Wie oben beschrieben, muss das letzte Ergebnis der aufrufenden Funktion den Typ error haben und der Ausdruck muss dem Typ error zuweisbar sein.

Im Nicht-Zuweisungsfall wird der Ausdruck in einem Gültigkeitsbereich ausgewertet, in dem eine neue Variable err eingeführt und auf den Wert des letzten Ergebnisses des Funktionsaufrufs gesetzt wird. Dadurch kann der Ausdruck leicht auf den vom Aufruf zurückgegebenen Fehler verweisen. Im Zuweisungsfall wird der Ausdruck im Rahmen der Ergebnisse des Aufrufs ausgewertet und kann somit direkt auf den Fehler verweisen.

Das ist der vollständige Vorschlag.

Zum Beispiel ist die Funktion os.Chdir derzeit

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Nach diesem Vorschlag könnte es geschrieben werden als

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

Ich schreibe diesen Vorschlag hauptsächlich, um Leute, die die Go-Fehlerbehandlung vereinfachen möchten, zu ermutigen, über Möglichkeiten nachzudenken, wie sie es einfach machen können, den Kontext um Fehler zu wickeln, und nicht nur den Fehler unverändert zurückzugeben.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Hilfreichster Kommentar

Eine einfache Idee mit Unterstützung für Fehlerdekoration, die jedoch eine drastischere Sprachänderung erfordert (offensichtlich nicht für go1.10), ist die Einführung eines neuen Schlüsselworts check .

Es hätte zwei Formen: check A und check A, B .

Sowohl A als auch B müssen error . Die zweite Form würde nur bei der Fehlerdekoration verwendet werden; Leute, die ihre Fehler nicht dekorieren müssen oder wollen, werden die einfachere Form verwenden.

1. Form (Check A)

check A wertet A . Wenn nil , passiert nichts. Wenn nicht nil , check wie ein return {<zero>}*, A .

Beispiele

  • Wenn eine Funktion nur einen Fehler zurückgibt, kann sie inline mit check , also
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

wird

check UpdateDB()
  • Für eine Funktion mit mehreren Rückgabewerten müssen Sie wie jetzt zuweisen.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

wird

a, b, err := Foo()
check err

// use a and b

2. Form (Check A, B)

check A, B wertet A . Wenn nil , passiert nichts. Wenn nicht nil , check wie ein return {<zero>}*, B .

Dies ist für Fehler-Dekorationsbedürfnisse. Wir prüfen immer noch auf A , aber B wird im impliziten return .

Beispiel

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

wird

a, err := Foo()
check err, &BarError{"Bar", err}

Anmerkungen

Es ist ein Kompilierungsfehler zu

  • Verwenden Sie die check Anweisung für Dinge, die nicht zu error ausgewertet werden
  • Verwenden Sie check in einer Funktion mit Rückgabewerten nicht in der Form { type }*, error

Die Zwei-Ausdrücke-Form check A, B ist kurzgeschlossen. B wird nicht ausgewertet, wenn A nil .

Hinweise zur Praktikabilität

Es gibt Unterstützung für das Verzieren von Fehlern, aber Sie zahlen nur dann für die klobigere check A, B Syntax, wenn Sie tatsächlich Fehler verzieren müssen.

Für die if err != nil { return nil, nil, err } Boilerplate (die sehr gebräuchlich ist) ist check err so kurz wie möglich, ohne an Klarheit zu verlieren (siehe Hinweis zur Syntax unten).

Hinweise zur Syntax

Ich würde argumentieren, dass diese Art von Syntax ( check .. , am Anfang der Zeile, ähnlich einem return ) eine gute Möglichkeit ist, Fehlerüberprüfungs-Boilerplates zu eliminieren, ohne die Unterbrechung des Kontrollflusses zu verbergen, die implizite Rückgaben einführen.

Ein Nachteil von Ideen wie den obigen <do-stuff> || <handle-err> und <do-stuff> catch <handle-err> oder den a, b = foo()? die in einem anderen Thread vorgeschlagen wurden, besteht darin, dass sie die Kontrollflussmodifikation auf eine Weise verbergen, die den Fluss erschwert Folgen; erstere mit || <handle-err> Maschinen, die am Ende einer ansonsten schlicht aussehenden Zeile angehängt sind, letztere mit einem kleinen Symbol, das überall erscheinen kann, auch in der Mitte und am Ende einer schlicht aussehenden Codezeile, eventuell mehrfach.

Eine check Anweisung wird im aktuellen Block immer auf der obersten Ebene sein und hat dieselbe Bedeutung wie andere Anweisungen, die den Kontrollfluss ändern (z. B. eine frühe return ).

Alle 519 Kommentare

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

Woher kommt e dort? Tippfehler?

Oder meintest du:

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(Das heißt, weist die implizite err != nil-Prüfung zuerst dem Ergebnisparameter einen Fehler zu, der benannt werden kann, um ihn vor der impliziten Rückgabe erneut zu ändern?)

Seufz, mein eigenes Beispiel vermasselt. Jetzt behoben: e sollte err . Der Vorschlag legt err in den Gültigkeitsbereich, um den Fehlerwert des Funktionsaufrufs aufzunehmen, wenn er sich nicht in einer Zuweisungsanweisung befindet.

Obwohl ich nicht sicher bin, ob ich mit der Idee oder der Syntax einverstanden bin, muss ich Ihnen danken, dass Sie darauf geachtet haben, Kontext zu Fehlern hinzuzufügen, bevor Sie sie zurückgeben.

Dies könnte für @davecheney von Interesse https://github.com/pkg/errors geschrieben hat.

Was passiert in diesem Code:

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(Ich entschuldige mich, wenn dies nicht einmal ohne den || &PathError{"chdir", dir, e} Teil möglich ist; ich versuche auszudrücken, dass sich dies wie eine verwirrende Überschreibung des bestehenden Verhaltens anfühlt und implizite Rückgaben ... hinterhältig sind?)

@object88 Es wäre gut if und for und switch verwendet wird, nicht zuzulassen. Das wäre wahrscheinlich das Beste, obwohl es die Grammatik etwas komplizieren würde.

Aber wenn wir das nicht tun, dann passiert Folgendes: Wenn thing.Nope() einen Fehler ungleich Null zurückgibt, dann kehrt die aufrufende Funktion mit &PathError{"chdir", dir, err} (wobei err die Variable, die durch den Aufruf auf thing.Nope() ). Wenn thing.Nope() einen nil Fehler zurückgibt, dann wissen wir mit Sicherheit, dass err == nil in der Bedingung der if Anweisung wahr ist, und so ist der Hauptteil der if-Anweisung wird ausgeführt. Die Variable ignoreError wird nie gelesen. Hier gibt es keine Mehrdeutigkeit oder Aufhebung des bestehenden Verhaltens; die hier eingeführte Behandlung von || wird nur akzeptiert, wenn der Ausdruck nach || kein boolescher Wert ist, was bedeutet, dass er derzeit nicht kompiliert würde.

Ich stimme zu, dass implizite Rückgaben hinterhältig sind.

Ja, mein Beispiel ist ziemlich schlecht. Aber die Operation innerhalb eines if , for oder switch nicht zuzulassen, würde eine Menge potenzieller Verwirrung beseitigen.

Da die Berücksichtigung in der Sprache im Allgemeinen schwer zu erreichen ist, habe ich mich entschieden, zu sehen, wie schwer diese Variante in der Sprache zu codieren ist. Nicht viel schwieriger als die anderen: https://play.golang.org/p/9B3Sr7kj39

Ich mag all diese Vorschläge, um einen Werttyp und eine Position in den Rückgabeargumenten besonders zu machen, wirklich nicht. Dieser ist in gewisser Weise sogar noch schlimmer, weil er err in diesem speziellen Kontext auch zu einem besonderen Namen macht.

Obwohl ich sicherlich zustimme, dass die Leute (einschließlich mir!) es leid sein sollten, Fehler ohne zusätzlichen Kontext zurückzugeben.

Wenn es andere Rückgabewerte gibt, wie

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

es kann definitiv ermüdend zu lesen. Mir hat @nigeltaos Vorschlag für return ..., err in https://github.com/golang/go/issues/19642#issuecomment -288559297 etwas gefallen

Wenn ich das richtig verstehe, müsste der Parser zum Erstellen des Syntaxbaums die Typen der Variablen kennen, zwischen denen unterschieden werden kann

boolean := BoolFunc() || BoolExpr

und

err := FuncReturningError() || Expr

Es sieht nicht gut aus.

weniger ist mehr...

Wenn die Rückgabe-Expressionsliste zwei oder mehr Elemente enthält, wie funktioniert sie?

Übrigens, ich möchte stattdessen panicIf.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor Im Beispiel Ihres Vorschlags wird err immer noch nicht explizit deklariert und als "magisch" (vordefinierte Sprache) eingefügt, oder?

Oder wird es sowas wie

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

?

Andererseits (da es bereits als "Sprachumstellung" gekennzeichnet ist...)
Führen Sie einen neuen Operator (!! oder ??) ein, der die Verknüpfung bei Fehler ausführt != nil (oder nullable?)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

Sorry wenn das zu weit weg ist :)

Ich stimme zu, dass sich die Fehlerbehandlung in Go wiederholen kann. Ich habe nichts gegen die Wiederholung, aber zu viele davon beeinträchtigen die Lesbarkeit. Es gibt einen Grund, warum "Cyclomatic Complexity" (ob Sie daran glauben oder nicht) Kontrollflüsse als Maß für die Komplexität verwendet. Die "if"-Anweisung fügt zusätzliches Rauschen hinzu.

Die vorgeschlagene Syntax "||" ist nicht sehr intuitiv zu lesen, zumal das Symbol allgemein als ODER-Operator bekannt ist. Wie gehen Sie außerdem mit Funktionen um, die mehrere Werte und Fehler zurückgeben?

Ich werfe hier nur ein paar Ideen ein. Wie wäre es, anstatt Fehler als Ausgabe zu verwenden, verwenden wir Fehler als Eingabe? Beispiel: https://play.golang.org/p/rtfoCIMGAb

Danke für alle Kommentare.

@opennota Guter Punkt. Es könnte immer noch funktionieren, aber ich stimme zu, dass dieser Aspekt umständlich ist.

@mattn Ich glaube nicht, dass ExpressionList zurückgegeben wird, daher bin ich mir nicht sicher, was Sie fragen. Wenn die aufrufende Funktion mehrere Ergebnisse hat, werden alle bis auf das letzte als Nullwert des Typs zurückgegeben.

@mattn panicif sich nicht mit einem der Schlüsselelemente dieses Vorschlags, der eine einfache Möglichkeit darstellt, einen Fehler mit zusätzlichem Kontext zurückzugeben. Und natürlich kann man heute leicht panicif schreiben.

@tandr Ja, err ist magisch definiert, was ziemlich schrecklich ist. Eine andere Möglichkeit wäre, dem error-expression zu erlauben, error zu verwenden, um auf den Fehler zu verweisen, was auf andere Weise schrecklich ist.

@tandr Wir könnten einen anderen Betreiber gebrauchen, aber ich sehe keinen großen Vorteil. Es scheint das Ergebnis nicht lesbarer zu machen.

@henryas Ich denke, der Vorschlag erklärt, wie er mit mehreren Ergebnissen

@henryas Danke für das Beispiel. Was ich an dieser Art von Ansatz nicht mag, ist, dass die Fehlerbehandlung zum wichtigsten Aspekt des Codes wird. Ich möchte, dass die Fehlerbehandlung vorhanden und sichtbar ist, aber ich möchte nicht, dass sie das erste in der Zeile ist. Das gilt heute mit dem Idiom if err != nil und der Einrückung des Fehlerbehandlungscodes, und es sollte auch so bleiben, wenn neue Funktionen zur Fehlerbehandlung hinzugefügt werden.

Danke noch einmal.

@ianlancetaylor Ich weiß nicht, ob Sie meinen Playground-Link überprüft haben, aber er hatte ein "panicIf", mit dem Sie zusätzlichen Kontext hinzufügen konnten.

Ich gebe hier eine etwas vereinfachte Version wieder:

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

Zufälligerweise habe ich gerade einen Blitzvortrag auf der GopherCon gehalten, in dem ich ein bisschen Syntax verwendet (aber nicht ernsthaft vorgeschlagen) habe, um bei der Visualisierung von Fehlerbehandlungscode zu helfen. Die Idee ist, diesen Code an die Seite zu legen, um ihn aus dem Hauptfluss zu entfernen, ohne auf irgendwelche Zaubertricks zurückzugreifen, um den Code zu kürzen. Das Ergebnis sieht aus wie

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

wobei =: das neue Bit der Syntax ist, ein Spiegel von := , der in die andere Richtung zuweist. Natürlich bräuchten wir auch etwas für = , was zugegebenermaßen problematisch ist. Die allgemeine Idee besteht jedoch darin, dem Leser das Verständnis des glücklichen Weges zu erleichtern, ohne Informationen zu verlieren.

Auf der anderen Seite hat die derzeitige Art der Fehlerbehandlung einige Vorteile, da sie als eklatante Erinnerung daran dient, dass Sie möglicherweise zu viele Dinge in einer einzigen Funktion tun und einige Refactorings möglicherweise überfällig sind.

Ich mag die von @billyh hier vorgeschlagene Syntax sehr

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

oder komplexeres Beispiel mit https://github.com/pkg/errors

func parse(input io.Reader) (*point, error) {
    var p point

    err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
    err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
    err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
    err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
    err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

    return &p, nil
}

Eine einfache Idee mit Unterstützung für Fehlerdekoration, die jedoch eine drastischere Sprachänderung erfordert (offensichtlich nicht für go1.10), ist die Einführung eines neuen Schlüsselworts check .

Es hätte zwei Formen: check A und check A, B .

Sowohl A als auch B müssen error . Die zweite Form würde nur bei der Fehlerdekoration verwendet werden; Leute, die ihre Fehler nicht dekorieren müssen oder wollen, werden die einfachere Form verwenden.

1. Form (Check A)

check A wertet A . Wenn nil , passiert nichts. Wenn nicht nil , check wie ein return {<zero>}*, A .

Beispiele

  • Wenn eine Funktion nur einen Fehler zurückgibt, kann sie inline mit check , also
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

wird

check UpdateDB()
  • Für eine Funktion mit mehreren Rückgabewerten müssen Sie wie jetzt zuweisen.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

wird

a, b, err := Foo()
check err

// use a and b

2. Form (Check A, B)

check A, B wertet A . Wenn nil , passiert nichts. Wenn nicht nil , check wie ein return {<zero>}*, B .

Dies ist für Fehler-Dekorationsbedürfnisse. Wir prüfen immer noch auf A , aber B wird im impliziten return .

Beispiel

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

wird

a, err := Foo()
check err, &BarError{"Bar", err}

Anmerkungen

Es ist ein Kompilierungsfehler zu

  • Verwenden Sie die check Anweisung für Dinge, die nicht zu error ausgewertet werden
  • Verwenden Sie check in einer Funktion mit Rückgabewerten nicht in der Form { type }*, error

Die Zwei-Ausdrücke-Form check A, B ist kurzgeschlossen. B wird nicht ausgewertet, wenn A nil .

Hinweise zur Praktikabilität

Es gibt Unterstützung für das Verzieren von Fehlern, aber Sie zahlen nur dann für die klobigere check A, B Syntax, wenn Sie tatsächlich Fehler verzieren müssen.

Für die if err != nil { return nil, nil, err } Boilerplate (die sehr gebräuchlich ist) ist check err so kurz wie möglich, ohne an Klarheit zu verlieren (siehe Hinweis zur Syntax unten).

Hinweise zur Syntax

Ich würde argumentieren, dass diese Art von Syntax ( check .. , am Anfang der Zeile, ähnlich einem return ) eine gute Möglichkeit ist, Fehlerüberprüfungs-Boilerplates zu eliminieren, ohne die Unterbrechung des Kontrollflusses zu verbergen, die implizite Rückgaben einführen.

Ein Nachteil von Ideen wie den obigen <do-stuff> || <handle-err> und <do-stuff> catch <handle-err> oder den a, b = foo()? die in einem anderen Thread vorgeschlagen wurden, besteht darin, dass sie die Kontrollflussmodifikation auf eine Weise verbergen, die den Fluss erschwert Folgen; erstere mit || <handle-err> Maschinen, die am Ende einer ansonsten schlicht aussehenden Zeile angehängt sind, letztere mit einem kleinen Symbol, das überall erscheinen kann, auch in der Mitte und am Ende einer schlicht aussehenden Codezeile, eventuell mehrfach.

Eine check Anweisung wird im aktuellen Block immer auf der obersten Ebene sein und hat dieselbe Bedeutung wie andere Anweisungen, die den Kontrollfluss ändern (z. B. eine frühe return ).

@ALTree , ich habe dein Beispiel nicht verstanden:

a, b, err := Foo()
check err

Erzielt die dreiwertige Rendite des Originals:

return "", "", err

Gibt es nur Nullwerte für alle deklarierten Rückgaben mit Ausnahme des endgültigen Fehlers zurück? Was ist mit Fällen, in denen Sie zusammen mit einem Fehler einen gültigen Wert zurückgeben möchten, z. B. die Anzahl der geschriebenen Bytes, wenn ein Write() fehlschlägt?

Welche Lösung wir auch immer wählen, sollte die Allgemeingültigkeit der Fehlerbehandlung minimal einschränken.

Was den Wert von check am Anfang der Zeile betrifft, so ist es meine persönliche Präferenz, den primären Kontrollfluss am Anfang jeder Zeile zu sehen und die Fehlerbehandlung so wenig wie möglich die Lesbarkeit dieses primären Kontrollflusses zu beeinträchtigen wie möglich. Auch wenn die Fehlerbehandlung durch ein reserviertes Wort wie check oder catch , dann wird so ziemlich jeder moderne Editor die reservierte Wortsyntax auf irgendeine Weise hervorheben und sogar bemerkbar machen wenn es auf der rechten seite ist.

@billyh dies wird oben in der Zeile erklärt, die besagt:

Wenn nicht null, verhält sich Check wie ein return {<zero>}*, A

check gibt den Nullwert aller Rückgabewerte mit Ausnahme des Fehlers (an letzter Position) zurück.

Was ist mit Fällen, in denen Sie zusammen mit einem Fehler einen gültigen Wert zurückgeben möchten?

Dann verwenden Sie das Idiom if err != nil { .

Es gibt viele Fälle, in denen Sie ein komplexeres Verfahren zur Fehlerbehebung benötigen. Zum Beispiel müssen Sie nach dem Abfangen eines Fehlers möglicherweise etwas zurücksetzen oder etwas in eine Protokolldatei schreiben. In all diesen Fällen haben Sie immer noch das übliche if err Idiom in Ihrer Toolbox, und Sie können es verwenden, um einen neuen Block zu beginnen, in dem jede Art von Fehlerbehandlungsoperation, egal wie artikuliert, möglich ist ausgeführt werden.

Welche Lösung wir auch immer wählen, sollte die Allgemeingültigkeit der Fehlerbehandlung minimal einschränken.

Siehe meine Antwort oben. Sie haben immer noch if und alles andere, was Ihnen die Sprache jetzt bietet.

so ziemlich jeder moderne Editor wird das reservierte Wort hervorheben

Könnte sein. Aber die Einführung undurchsichtiger Syntax, die Syntax-Highlighting erfordert, um lesbar zu sein, ist nicht ideal.

Dieser spezielle Fehler kann behoben werden, indem eine Doppelrückgabefunktion in die Sprache eingeführt wird.
in diesem Fall gibt die Funktion a() 123 zurück:

func a() int {
B()
zurück 456
}
Funktion b() {
Rückgabe Rückgabe int(123)
}

Diese Funktion kann verwendet werden, um die Fehlerbehandlung wie folgt zu vereinfachen:

func handle(var *foo, err error)(var *foo, err error) {
wenn err != nil {
Rückkehr Rückkehr Null, ähm
}
Return var, nil
}

func client_code() (*client_object, error) {
var obj, err =handle(something_that_can_fail())
// dies wird nur erreicht, wenn etwas nicht fehlgeschlagen ist
// andernfalls würde die client_code-Funktion den Fehler den Stack hinauf verbreiten
behaupten(err == null)
}

Dies ermöglicht es Benutzern, Fehlerbehandlungsfunktionen zu schreiben, die die Fehler den Stack hinauf verbreiten können
solche Fehlerbehandlungsfunktionen können vom Hauptcode getrennt sein

Tut mir leid, wenn ich mich falsch verstanden habe, aber ich möchte einen Punkt klarstellen. Die folgende Funktion erzeugt einen Fehler, eine vet Warnung oder wird sie akzeptiert?

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

@rodcorsi Unter diesem Vorschlag würde Ihr Beispiel ohne Tierarztwarnung akzeptiert. Es wäre gleichbedeutend mit

if err := syscall.Chdir(dir); err != nil {
    return err
}

Wie wäre es, die Verwendung von Kontext zur Fehlerbehandlung auszuweiten? Zum Beispiel gegeben die folgende Definition:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Jetzt in der fehleranfälligen Funktion ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
In der Zwischenfunktion ...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
Und in der oberen Ebene Funktion
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
Es gibt mehrere Vorteile, wenn dieser Ansatz verwendet wird. Erstens lenkt es den Leser nicht vom Hauptausführungspfad ab. Es gibt minimale "if"-Anweisungen, um eine Abweichung vom Hauptausführungspfad anzuzeigen.

Zweitens verbirgt es keine Fehler. Aus der Methodensignatur geht hervor, dass die Funktion möglicherweise Fehler enthält, wenn sie ErrorContext akzeptiert. Innerhalb der Funktion verwendet sie die normalen Verzweigungsanweisungen (zB "if"), die zeigen, wie Fehler mit normalem Go-Code behandelt werden.

Drittens werden Fehler automatisch an die interessierte Partei weitergegeben, die in diesem Fall der Kontextbesitzer ist. Sollte es zu einer zusätzlichen Fehlerbearbeitung kommen, wird diese deutlich angezeigt. Nehmen wir zum Beispiel einige Änderungen an der Zwischenfunktion vor, um alle vorhandenen Fehler einzuschließen:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
Im Grunde schreiben Sie einfach den Fehlerbehandlungscode nach Bedarf. Sie müssen sie nicht manuell aufblasen.

Schließlich haben Sie als Funktionsschreiber ein Mitspracherecht, ob der Fehler behandelt werden soll. Mit dem aktuellen Go-Ansatz ist dies einfach ...
````
//mit folgender Definition
func MyFunction(number int) error

// dann mach das
MyFunction(8) //ohne den Fehler zu überprüfen
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction(ctx ErrorContext) {
if ctx != nil && ctx.HasError() {
Rückkehr
}
//...
}
Or make it compulsory with this:
func MyFunction(ctx ErrorContext) {
if ctx.HasError () { // wird in Panik geraten, wenn ctx nil ist
Rückkehr
}
//...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction(ctx ErrorContext) {
ignoriert := context.New()
MyFunction(ignored) //diese wird ignoriert

 MyFunction(ctx) //this one is handled

}
````
Dieser Ansatz ändert nichts an der bestehenden Sprache.

@ALTree Alberto, was ist mit dem Mischen von check und dem, was @ianlancetaylor vorgeschlagen hat?

Also

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

wird

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

Außerdem können wir check um nur Fehlertypen zu behandeln. Wenn Sie also mehrere Rückgabewerte benötigen, müssen diese benannt und zugewiesen werden, damit es irgendwie "in-place" zuweist und sich wie ein einfacher "return" verhält.

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

Wenn return eines Tages im Ausdruck akzeptabel werden würde, dann wird check nicht benötigt oder wird zu einer Standardfunktion

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

die letzte Lösung fühlt sich jedoch wie Perl an 😄

Ich kann mich nicht erinnern, wer es ursprünglich vorgeschlagen hat, aber hier ist eine andere Syntaxidee (jeder Lieblingsfahrradschuppen :-). Ich sage nicht, dass es gut ist, aber wenn wir Ideen in den Topf werfen...

x, y := try foo()

wäre gleichbedeutend mit:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

und

x, y := try foo() catch &FooErr{E:$, S:"bad"}

wäre gleichbedeutend mit:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

Die try Form wurde sicherlich schon mehrmals vorgeschlagen, um oberflächliche Syntaxunterschiede zu modulieren. Die try ... catch Form ist weniger oft vorgeschlagen, aber es ist klar ähnlich wie @ALTree 's check A, B Konstrukt und @tandr' s Follow-up - Vorschlag. Ein Unterschied besteht darin, dass dies ein Ausdruck und keine Aussage ist, sodass Sie sagen können:

z(try foo() catch &FooErr{E:$, S:"bad"})

Sie könnten mehrere try/catches in einer einzigen Anweisung haben:

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

obwohl ich glaube nicht, dass wir das fördern wollen. Auch hier müssen Sie auf die Reihenfolge der Auswertung achten. Zum Beispiel, ob h() auf Nebenwirkungen ausgewertet wird, wenn e() einen Fehler ungleich Null zurückgibt.

Offensichtlich würden neue Schlüsselwörter wie try und catch die Kompatibilität von Go 1.x beeinträchtigen.

Ich schlage vor, dass wir das Ziel dieses Vorschlags unterdrücken sollten. Welches Problem wird durch diesen Vorschlag behoben? Reduzieren Sie die folgenden drei Zeilen in zwei oder eine ? Dies kann eine Änderung der Rücksendesprache/falls sein.

if err != nil {
    return err
}

Oder die Anzahl der Überprüfungen auf Fehler reduzieren? Dies kann eine Try/Catch-Lösung sein.

Ich möchte vorschlagen, dass jede vernünftige Shortcut-Syntax für die Fehlerbehandlung drei Eigenschaften hat:

  1. Es sollte nicht vor dem überprüften Code erscheinen, damit der fehlerfreie Pfad hervorsticht.
  2. Es sollte keine impliziten Variablen in den Gültigkeitsbereich einführen, damit die Leser nicht verwirrt werden, wenn es eine explizite Variable mit demselben Namen gibt.
  3. Es sollte nicht eine Wiederherstellungsaktion (z. B. return err ) einfacher machen als eine andere. Manchmal ist vielleicht eine ganz andere Aktion vorzuziehen (wie das Aufrufen von t.Fatal ). Wir möchten die Leute auch nicht davon abhalten, zusätzlichen Kontext hinzuzufügen.

Angesichts dieser Einschränkungen scheint eine fast minimale Syntax etwa so zu sein wie

STMT SEPARATOR_TOKEN VAR BLOCK

Beispielsweise,

syscall.Chdir(dir) :: err { return err }

was äquivalent zu ist

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir(dir) :: { return @err }
```

Ich frage mich, wie sich das verhält, wenn ein Wert ungleich Null und ein Fehler zurückgegeben werden. Beispielsweise gibt bufio.Peek möglicherweise einen Wert ungleich Null und ErrBufferFull beide gleichzeitig zurück.

@mattn Sie können weiterhin die alte Syntax verwenden.

@nigeltao Ja, ich verstehe. Ich vermute, dass dieses Verhalten möglicherweise einen Fehler im Code des Benutzers verursacht, da bufio.Peek auch Nicht-Null und Null zurückgibt. Der Code darf keine impliziten Werte und keinen Fehler enthalten. Daher sollten sowohl der Wert als auch der Fehler an den Aufrufer zurückgegeben werden (in diesem Fall).

ret, err := doSomething() :: err { return err }
return ret, err

@jba Was Sie beschreiben, sieht ein bisschen aus wie ein transponierter Funktionskompositionsoperator:

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

Aber die Tatsache, dass wir hauptsächlich zwingenden Code schreiben, erfordert, dass wir keine Funktion an der zweiten Position verwenden, da ein Teil des Punktes darin besteht, frühzeitig zurückkehren zu können.

Jetzt denke ich über drei Beobachtungen nach, die alle irgendwie miteinander verbunden sind:

  1. Die Fehlerbehandlung ist wie die Komposition von Funktionen, aber die Art und Weise, wie wir Dinge in Go tun, ist irgendwie das Gegenteil von Haskells Fehlermonade : Da wir hauptsächlich Imperative anstelle von sequenziellem Code schreiben, möchten wir den Fehler transformieren (um Kontext hinzuzufügen) anstatt der fehlerfreie Wert (den wir lieber nur an eine Variable binden würden).

  2. Go-Funktionen, die (x, y, error) bedeuten normalerweise eher so etwas wie eine Vereinigung (#19412) von (x, y) | error .

  3. In Sprachen, die Unions entpacken oder nach Mustern suchen, handelt es sich bei den Fällen um separate Bereiche, und viele der Probleme, die wir mit Fehlern in Go haben, sind auf unerwartete Shadowings von neu deklarierten Variablen zurückzuführen, die durch das Trennen dieser Bereiche verbessert werden könnten (#21114).

Was wir also vielleicht wirklich wollen, ist der Operator =: , aber mit einer Art von Union-Matching-Bedingung:

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

```gehen
n := io.WriteString(w, s) =? Fehler { Fehler zurückgeben }

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

Abgesehen vom Umfang ist das nicht viel anders, als nur gofmt zu ändern, um für Einzeiler zugänglicher zu sein:

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

```gehen
n, err := io.WriteString(w, s); if err != nil { return err }

```go
y, ok := m[x]; if !ok { return ErrNotFound }

Aber der Umfang ist eine große Sache! Die Scoping-Probleme bestehen darin, dass diese Art von Code die Grenze von "etwas hässlich" zu "subtilen Fehlern" überschreitet.

@ianlancetaylor
Obwohl ich ein Fan der Gesamtidee bin, bin ich kein großer Unterstützer der kryptischen perlartigen Syntax dafür. Vielleicht wäre eine wortreichere Syntax weniger verwirrend, wie zum Beispiel:

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

Außerdem habe ich nicht verstanden, ob das letzte Argument bei einer Zuweisung angezeigt wird, zB:

resp := http.Get("https://example.com") or dump

Vergessen wir nicht, dass Fehler Werte in go sind und keine speziellen Typen.
Es gibt nichts, was wir mit anderen Strukturen tun können, was wir nicht mit Fehlern tun können und umgekehrt. Das bedeutet, dass Sie, wenn Sie Strukturen im Allgemeinen verstehen, Fehler und deren Behandlung verstehen (auch wenn Sie denken, dass es ausführlich ist).

Diese Syntax würde erfordern, dass neue und alte Entwickler neue Informationen lernen, bevor sie anfangen können, Code zu verstehen, der sie verwendet.

Das allein macht diesen Vorschlag IMHO nicht wert.

Persönlich bevorzuge ich diese Syntax

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

über

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

Es ist eine Zeile mehr, trennt aber die beabsichtigte Aktion von der Fehlerbehandlung. Dieses Formular ist für mich das lesbarste.

@bcmills :

Abgesehen vom Scoping unterscheidet sich das nicht wesentlich davon, einfach nur das Gofmt zu ändern, um für Einzeiler zugänglicher zu sein

Nicht nur der Umfang; da ist auch der linke rand. Ich denke, das wirkt sich wirklich auf die Lesbarkeit aus. Ich denke

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

ist viel klarer als

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

insbesondere wenn es in mehreren aufeinanderfolgenden Zeilen auftritt, da Ihr Auge den linken Rand scannen kann, um die Fehlerbehandlung zu ignorieren.

Wenn wir die Idee

Die Funktion F2 wird ausgeführt, wenn der letzte Wert nicht nil ist .

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

Ein Sonderfall der Pipe-Weiterleitung mit Return-Anweisung

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

Echtes Beispiel von @urandom in einer anderen Ausgabe
Für mich viel besser lesbar mit Fokus im Primärfluss

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

Ich stimme zu, dass die Fehlerbehandlung nicht ergonomisch ist. Wenn Sie den folgenden Code lesen, müssen Sie ihn nämlich in if error not nil then vokalisieren - was in if there is an error then .

if err != nil {
    // handle error
}

Ich hätte gerne die Möglichkeit, den obigen Code auf diese Weise auszudrücken - was meiner Meinung nach besser lesbar ist.

if err {
    // handle error
}

Nur mein bescheidener Vorschlag :)

Es sieht aus wie Perl, es hat sogar die magische Variable
Als Referenz würden Sie in Perl tun

open (FILE, $file) or die("kann $file nicht öffnen: $!");

IMHO, es lohnt sich nicht, ein Punkt, den ich an go mag, ist die Fehlerbehandlung
ist explizit und 'in deinem Gesicht'

Wenn wir dabei bleiben, möchte ich keine magischen Variablen haben, das sollten wir sein
in der Lage, die Fehlervariable zu benennen

e := syscall.Chdir(dir) ?> e: &PathError{"chdir", dir, e}

Und wir könnten genauso gut ein anderes Symbol als || . verwenden speziell für diese Aufgabe,
Ich denke, Textsymbole wie 'oder' sind aufgrund von Rückwärts nicht möglich
Kompatibilität

n, _, err, _ = somecall(...) ?> err: &PathError{"somecall", n, err}

Am Dienstag, den 1. August 2017 um 14:47 Uhr schrieb Rodrigo [email protected] :

Mischen der Idee @bcmills https://github.com/bcmills können wir vorstellen
bedingter Rohrweiterleitungsoperator.

Die Funktion F2 wird ausgeführt, wenn der letzte Wert nicht null ist .

func F1() (foo, bar){}
erster := F1() ?> letzter: F2(erster, letzter)

Ein Sonderfall der Pipe-Weiterleitung mit Return-Anweisung

func Chdir(dir string) error {
syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
kehre zurück
}

Echtes Beispiel
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
gebracht von @urandom https://github.com/urandom in einer anderen Ausgabe
Für mich viel besser lesbar mit Fokus im Primärfluss

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, Fehler) {
// Beim Bootstrapping wollen wir nur apt-get update/upgrade
// und richten Sie die SSH-Schlüssel ein. Den Rest überlassen wir cloudinit/sshinit.
udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: nil zurückgeben, err
if icfg.Bootstrap != nil {
udata.ConfigureBasic() ?> err: nil zurückgeben, err
Udata zurückgeben, nichts
}
udata.Configure() ?> err: nil zurückgeben, err
Udata zurückgeben, nichts
}
func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
if cloudcfg == null {
cloudcfg = cloudinit.New(icfg.Series) ?> err: Null zurückgeben, Fehler.Trace(err)
}
configureCloudinit(icfg, cloudcfg) ?> err: nil zurückgeben, Fehler.Trace(err)
Betriebssystem := series.GetOSFromSeries(icfg.Series) ?> err: Null zurückgeben, Fehler.Trace(err)
udata := renderer.Render(cloudcfg, operatingSystem) ?> err: nil zurückgeben, Fehler.Trace(err)
logger.Tracef("Generierte Cloud-Init:\n%s", string(udata))
Udata zurückgeben, nichts
}


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/21161#issuecomment-319359614 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

Bin ich der einzige, der der Meinung ist, dass all diese vorgeschlagenen Änderungen komplizierter wären als die derzeitige Form.

Ich denke, Einfachheit und Kürze sind nicht gleich oder austauschbar. Ja, all diese Änderungen wären eine oder mehrere Zeilen kürzer, würden aber Operatoren oder Schlüsselwörter einführen, die ein Benutzer der Sprache lernen müsste.

@rodcorsi Ich weiß, es scheint geringfügig, aber ich denke, es ist wichtig, dass der zweite Teil ein Block ist : Die vorhandenen Anweisungen if und for beide Blöcke und select und switch beide eine durch geschweifte Klammern getrennte Syntax, daher erscheint es umständlich, die geschweiften Klammern für diese eine bestimmte Ablaufsteuerung wegzulassen.

Es ist auch viel einfacher sicherzustellen, dass der Parse-Baum eindeutig ist, wenn Sie sich nicht um beliebige Ausdrücke nach den neuen Symbolen kümmern müssen.

Die Syntax und Semantik, die ich für meine Skizze im Sinn hatte, sind:


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

Ein NonZeroGuardStmt führt Block wenn der letzte Wert eines Expression nicht gleich dem Nullwert seines Typs ist. Wenn ein identifier vorhanden ist, wird es an diesen Wert innerhalb von Block gebunden. Ein ZeroGuardStmt führt Block wenn der letzte Wert eines Expression gleich dem Nullwert seines Typs ist.

Für das := Formular werden die anderen (führenden) Werte von Expression an IdentifierList wie in einem ShortVarDecl gebunden. Die Bezeichner werden im enthaltenden Gültigkeitsbereich deklariert, was bedeutet, dass sie auch innerhalb von Block sichtbar sind.

Für das Formular assign_op muss jeder linke Operand adressierbar sein, ein Map-Index-Ausdruck oder (nur für = Zuweisungen) der leere Bezeichner. Operanden können in Klammern gesetzt werden. Die anderen (führenden) Werte der rechten Seite Expression werden wie in einem Assignment ausgewertet. Die Zuweisung erfolgt vor der Ausführung des Block und unabhängig davon, ob dann das Block ausgeführt wird.


Ich glaube, dass die hier vorgeschlagene Grammatik mit Go 1 kompatibel ist: ? ist kein gültiger Bezeichner und es gibt keine existierenden Go-Operatoren, die dieses Zeichen verwenden, und obwohl ! ein gültiger Operator ist, gibt es keinen bestehende Produktion, in der darauf ein { folgen kann.

@bcmills LGTM, mit begleitenden Änderungen an gofmt.

Ich hätte gedacht, Sie würden =? und =! jeweils zu einem eigenen Token machen, was die Grammatik trivial kompatibel machen würde.

Ich hätte gedacht, du würdest machen =? und =! jedes ein Token für sich, was die Grammatik trivial kompatibel machen würde.

Das können wir in der Grammatik tun, aber nicht im Lexer: die Folge "=!" kann im gültigen Go 1-Code erscheinen (https://play.golang.org/p/pMTtUWgBN9).

Die geschweifte Klammer macht den Parse in meinem Vorschlag eindeutig: =! kann derzeit nur in einer Deklaration oder Zuweisung zu einer booleschen Variablen erscheinen, und Deklarationen und Zuweisungen können derzeit nicht unmittelbar vor einer geschweiften Klammer stehen (https ://play.golang.org/p/ncJyg-GMuL), sofern nicht durch ein implizites Semikolon getrennt (https://play.golang.org/p/lhcqBhr7Te).

@romainmenke Nein. Du bist nicht der Einzige. Ich kann den Wert einer einzeiligen Fehlerbehandlung nicht erkennen. Sie können eine Zeile sparen, aber die Komplexität erhöhen. Das Problem besteht darin, dass in vielen dieser Vorschläge der Fehlerbehandlungsteil ausgeblendet wird. Die Idee ist nicht, sie weniger auffällig zu machen, weil die Fehlerbehandlung wichtig ist, sondern den Code leichter lesbar zu machen. Kürze ist nicht gleich gute Lesbarkeit. Wenn man Änderungen am bestehenden Fehlerbehandlungssystem vornehmen muss, finde ich das herkömmliche Try-Catch-Endlich viel reizvoller als viele Ideenzwecke hier.

Ich mag den Vorschlag von check , weil Sie ihn auch auf die Handhabung erweitern können

f, err := os.Open(myfile)
check err
defer check f.Close()

Andere Vorschläge scheinen sich auch nicht mit defer vermischen. check ist auch sehr gut lesbar und einfach für Google, wenn Sie es nicht kennen. Ich glaube nicht, dass es auf den Typ error . Alles, was ein Rückgabeparameter an der letzten Position ist, könnte ihn verwenden. Ein Iterator könnte also ein check für ein Next() bool .

Ich habe mal einen Scanner geschrieben, der aussieht wie

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

Das letzte Bit könnte stattdessen check s.read(&rt) .

@carlmjohnson

Andere Vorschläge scheinen sich auch nicht mit defer vermischen.

Wenn Sie davon ausgehen, dass wir defer , um die Rückkehr von der äußeren Funktion mit der neuen Syntax zu ermöglichen, können Sie diese Annahme genauso gut auf andere Vorschläge anwenden.

defer f.Close() =? err { return err }

Da der check Vorschlag von @ALTree eine separate Anweisung einführt, sehe ich nicht, wie Sie dies mit einem defer mischen könnten, das etwas anderes tut, als nur den Fehler zurückzugeben.

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

Kontrast:

defer f.Close() =? err { return fmt.Errorf(…, err) }

Die Begründung für viele dieser Vorschläge ist eine bessere "Ergonomie", aber ich sehe nicht wirklich, wie einer davon besser ist, als dass etwas weniger zu tippen ist. Wie erhöhen diese die Wartbarkeit des Codes? Die Zusammensetzbarkeit? Die Lesbarkeit? Die Leichtigkeit, den Kontrollfluss zu verstehen?

@jimmyfrasche

Wie erhöhen diese die Wartbarkeit des Codes? Die Zusammensetzbarkeit? Die Lesbarkeit? Die Leichtigkeit, den Kontrollfluss zu verstehen?

Wie ich bereits angemerkt habe, müsste der Hauptvorteil jedes dieser Vorschläge wahrscheinlich aus einem klareren Umfang der Zuweisungen und err Variablen bestehen: siehe #19727, #20148, #5634, #21114 und wahrscheinlich andere für verschiedene wie Menschen auf Scoping-Probleme in Bezug auf die Fehlerbehandlung stoßen.

@bcmills danke für die Motivation und Entschuldigung, dass ich sie in deinem früheren Beitrag verpasst habe.

Wäre es angesichts dieser Prämisse jedoch nicht besser, eine allgemeinere Einrichtung für eine "klarere Zuordnung von Zuweisungen" bereitzustellen, die von allen Variablen verwendet werden könnte? Ich habe sicherlich auch unbeabsichtigt meinen Anteil an Nicht-Fehlervariablen überschattet.

Ich erinnere mich, als das aktuelle Verhalten von := eingeführt wurde – ein Großteil dieses Threads auf Go Nuts† verlangte nach einer Möglichkeit, explizit annotieren zu können, welche Namen wiederverwendet werden sollen, anstelle des impliziten "Wiederverwendung nur, wenn diese Variable in existiert". genau der aktuelle Umfang", in dem sich meiner Erfahrung nach all die subtilen, schwer zu erkennenden Probleme manifestieren.

† Ich kann den Thread nicht finden, hat jemand einen Link?

Es gibt viele Dinge, die meiner Meinung nach an Go verbessert werden könnten, aber das Verhalten von := mir immer als der einzige schwerwiegende Fehler. Vielleicht ist das Überdenken des Verhaltens von := der Weg, um das Grundproblem zu lösen oder zumindest die Notwendigkeit anderer, extremerer Änderungen zu reduzieren?

@jimmyfrasche

Wäre es angesichts dieser Prämisse jedoch nicht besser, eine allgemeinere Einrichtung für eine "klarere Zuordnung von Zuweisungen" bereitzustellen, die von allen Variablen verwendet werden könnte?

Ja. Das ist eines der Dinge, die mir an dem =? oder :: Operator gefallen , den

Persönlich vermute ich, dass ich auf lange Sicht glücklicher wäre mit einer expliziteren Tagged-Union/varint/algebraic-Datentyp-Funktion (siehe auch #19412), aber das ist eine viel größere Änderung der Sprache: Es ist schwer vorstellbar, wie wir nachrüsten würden das auf bestehende APIs in einer gemischten Go 1 / Go 2 Umgebung.

Die Leichtigkeit, den Kontrollfluss zu verstehen?

In den Vorschlägen von mir und @bcmills kann Ihr Auge die linke Seite nach unten scannen und den fehlerfreien Kontrollfluss leicht

@bcmills Ich glaube, ich bin für mindestens die Hälfte der Wörter in #19412 verantwortlich, also musst du mich nicht auf

Wenn es darum geht, Dinge mit einem Fehler zurückzugeben, gibt es vier Fälle

  1. nur ein Fehler (müssen Sie nichts tun, geben Sie einfach einen Fehler zurück)
  2. Zeug UND ein Fehler (das würdest du genauso handhaben wie jetzt)
  3. eine Sache ODER ein Fehler (Sie könnten Summentypen verwenden! :tada: )
  4. zwei oder mehr Dinge ODER ein Fehler

Wenn Sie 4 erreichen, wird es schwierig. Ohne Tupeltypen einzuführen (unbeschriftete Produkttypen, die zu den beschrifteten Produkttypen der Struktur passen) müssten Sie das Problem auf Fall 3 reduzieren, indem Sie alles in einer Struktur bündeln, wenn Sie Summentypen verwenden möchten, um "dies oder ein Fehler" zu modellieren.

Die Einführung von Tupeltypen würde alle möglichen Probleme und Kompatibilitätsprobleme und seltsame Überschneidungen verursachen (ist func() (int, string, error) ein implizit definiertes Tupel oder sind mehrere Rückgabewerte ein separates Konzept? Wenn es ein implizit definiertes Tupel ist, bedeutet das dann func() (n int, msg string, err error) ist eine implizit definierte Struktur!? Wenn dies eine Struktur ist, wie greife ich dann auf die Felder zu, wenn ich mich nicht im selben Paket befinde!)

Ich denke immer noch, dass Summentypen viele Vorteile bieten, aber sie tun natürlich nichts, um die Probleme mit dem Umfang zu beheben. Wenn überhaupt, könnten sie es noch schlimmer machen, weil Sie die gesamte 'Ergebnis- oder Fehler'-Summe überschatten könnten, anstatt nur den Fehlerfall zu überschatten, wenn Sie etwas im Ergebnisfall hatten.

@jba Ich sehe nicht, wie das eine wünschenswerte Eigenschaft ist. Abgesehen von einem allgemeinen Mangel an Leichtigkeit mit dem Konzept, den Kontrollfluss sozusagen zweidimensional zu gestalten, kann ich mir jedoch auch nicht wirklich vorstellen, warum dies nicht der Fall ist. Können Sie mir den Nutzen erklären?

Ohne Tupeltypen einzuführen […] müssten Sie alles in einer Struktur [bündeln], wenn Sie Summentypen verwenden möchten, um "dies oder einen Fehler" zu modellieren.

Ich bin damit einverstanden: Ich denke, wir hätten auf diese Weise viel lesbarere Aufruf-Sites (keine versehentlich transponierten Positionsbindungen mehr!)

Das große Problem ist die Migration: Wie kommen wir vom "Werte- und Fehler"-Modell von Go 1 zu einem potenziellen "Werte- oder Fehler"-Modell in Go 2, insbesondere bei APIs wie io.Writer , die wirklich "Werte" zurückgeben und Fehler"?

Ich denke immer noch, dass Summentypen viele Vorteile bieten, aber sie tun natürlich nichts, um die Probleme mit dem Umfang zu beheben.

Das hängt davon ab, wie Sie sie auspacken, was uns vermutlich dorthin zurückbringt, wo wir heute sind. Wenn Sie Unions bevorzugen, können Sie sich vielleicht eine Version von =? als "asymmetrische Musterabgleich"-API vorstellen:

i := match strconv.Atoi(str) | err error { return err }

Wobei match die traditionelle Mustervergleichsoperation im ML-Stil wäre, aber im Fall einer nicht erschöpfenden Übereinstimmung den Wert zurückgeben würde (als interface{} wenn die Vereinigung mehr als eine nicht übereinstimmende Alternative hat). anstatt mit einem nicht erschöpfenden Match-Misserfolg in Panik zu geraten.

Ich habe gerade ein Paket unter https://github.com/mpvl/errd eingecheckt, das die hier besprochenen Probleme programmgesteuert behebt (keine Sprachänderungen). Der wichtigste Aspekt dieses Pakets ist, dass es nicht nur die Fehlerbehandlung verkürzt, sondern auch die korrekte Ausführung erleichtert. Ich gebe Beispiele in den Dokumenten, wie die traditionelle idiomatische Fehlerbehandlung schwieriger ist, als es scheint, insbesondere in der Interaktion mit defer.

Ich halte dies jedoch für ein "Brenner" -Paket; Ziel ist es, gute Erfahrungen und Erkenntnisse darüber zu sammeln, wie die Sprache am besten erweitert werden kann. Es interagiert übrigens recht gut mit Generika, wenn dies etwas werden würde.

Wir arbeiten noch an einigen weiteren Beispielen, aber dieses Paket ist bereit zum Experimentieren.

@bcmills a million :+1: für #12854

Wie Sie bemerkt haben, gibt es "X und Fehler zurückgeben" und "X oder Fehler zurückgeben", so dass Sie das nicht wirklich umgehen könnten, ohne ein Makro, das bei Bedarf den alten Weg in den neuen übersetzt (und natürlich gäbe es Fehler) oder zumindest Laufzeitpanik, wenn es unweigerlich für eine "X-und-Fehler"-Funktion verwendet wurde).

Ich mag die Idee, spezielle Makros in die Sprache einzuführen, wirklich nicht, besonders wenn es nur zur Fehlerbehandlung ist, was bei vielen dieser Vorschläge mein Hauptproblem ist.

Go ist nicht viel Zucker oder Magie und das ist ein Plus.

Es gibt zu viel Trägheit und zu wenig Informationen, die in der gegenwärtigen Praxis kodiert sind, um einen Massensprung zu einem funktionaleren Fehlerbehandlungsparadigma zu bewältigen.

Wenn Go 2 Summentypen bekommt – was mich ehrlich gesagt (im positiven Sinne!) mehr Fragmentierung und Verwirrung beim Umgang mit Fehlern, daher sehe ich dies nicht als positiv an. (Ich würde es jedoch sofort für Dinge wie chan union { Msg1 T; Msg2 S; Err error } anstelle von drei Kanälen verwenden).

Wenn dies vor Go1 wäre und das Go-Team sagen könnte "Wir werden die nächsten sechs Monate nur damit verbringen, alles zu verschieben und wenn es kaputt geht, mach weiter", wäre das eine Sache, aber im Moment stecken wir im Grunde fest auch wenn wir Summentypen bekommen.

Wie Sie bemerkt haben, gibt es "X und Fehler zurückgeben" und "X oder Fehler zurückgeben", also könnten Sie das nicht wirklich umgehen, ohne ein Makro, das bei Bedarf den alten Weg in den neuen übersetzt.

Wie ich oben zu sagen versuchte, glaube ich nicht, dass es für den neuen Weg, was auch immer es ist, notwendig ist, "Rückgabe X und Fehler" zu behandeln. Wenn die überwiegende Mehrheit der Fälle "Rückgabe X oder Fehler" sind und der neue Weg nur das verbessert, dann ist das großartig, und Sie können immer noch den alten, Go 1-kompatiblen Weg für den selteneren "Rückgabe X und Fehler" verwenden.

@nigeltao Stimmt, aber wir müssten sie während des Übergangs noch beibehalten .

@jimmyfrasche Ich glaube nicht, dass ich ein Argument dafür konstruieren kann. Sie können sich meinen Vortrag ansehen oder das Beispiel in der README-Datei des Repo sehen. Aber wenn die visuellen Beweise für Sie nicht überzeugend sind, kann ich nichts sagen.

@jba hat sich den Vortrag angesehen und die README gelesen. Ich verstehe jetzt, woher Sie mit der Sache mit Klammern / Fußnoten / Endnoten / Randnotizen kommen (und ich bin ein Fan von Randnotizen (und Klammern)).

Wenn das Ziel darin besteht, mangels eines besseren Begriffs den unglücklichen Pfad zur Seite zu stellen, dann würde ein $EDITOR-Plugin ohne Sprachänderung funktionieren und es würde mit allem existierenden Code funktionieren, unabhängig von den Vorlieben des Code-Autors.

Eine Sprachänderung macht die Syntax etwas kompakter. @bcmills erwähnt, dass dies den Scoping verbessert, aber ich sehe nicht wirklich, wie dies möglich ist, es sei denn, es gibt andere Scoping-Regeln als := aber das scheint nur mehr Verwirrung zu stiften.

@bcmills Ich verstehe deinen Kommentar nicht. Sie können sie offensichtlich unterscheiden. Der alte Weg sieht so aus:

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

So sieht der neue Weg aus

check foo()

oder

foo() || &FooError{err}

oder was auch immer die Fahrradschuppenfarbe ist. Ich vermute, die meisten der Standardbibliotheken können übergehen, aber nicht alle müssen.

Um die Anforderungen von @ianlancetaylor zu ergänzen: Die Vereinfachung von Fehlermeldungen sollte nicht nur die Dinge kürzer machen, sondern auch die richtige Fehlerbehandlung erleichtern. Es ist schwierig, Panik und nachgelagerte Fehler bis zum Zurückstellen von Funktionen zu übergeben.

Betrachten Sie beispielsweise das Schreiben in eine Google Cloud Storage-Datei, bei der das Schreiben der Datei bei einem Fehler abgebrochen werden soll:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

Die Feinheiten dieses Codes umfassen:

  • Der Fehler von Copy wird heimlich über das benannte Return-Argument an die Defer-Funktion übergeben.
  • Um ganz sicher zu gehen, fangen wir Panik von r ab und stellen sicher, dass wir das Schreiben abbrechen, bevor die Panik wieder aufgenommen wird.
  • Das Ignorieren des Fehlers beim ersten Schließen ist beabsichtigt, sieht aber irgendwie wie ein Artefakt für faule Programmierer aus.

Mit dem Paket errd sieht dieser Code wie folgt aus:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard ist ein Fehlerhandler. Fehlerhandler können auch verwendet werden, um Fehler zu umschließen, zu protokollieren.

e.Must entspricht foo() || wrapError

e.Defer ist extra und befasst sich mit der Übergabe von Fehlern an Verzögerungen.

Bei Verwendung von Generika könnte dieser Codeabschnitt etwa so aussehen:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

Wenn wir die für Defer zu verwendenden Methoden standardisieren, könnte dies sogar so aussehen:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

Wobei DeferClose entweder Close oder CloseWithError auswählt. Nicht sagen, dass das besser ist, sondern nur die Möglichkeiten aufzeigen.

Wie auch immer, ich habe letzte Woche bei einem Treffen in Amsterdam eine Präsentation zu diesem Thema gehalten und es schien, dass die Möglichkeit, die Fehlerbehandlung zu vereinfachen, nützlicher ist, als sie zu verkürzen.

Eine Lösung, die Fehler verbessert, sollte sich mindestens genauso darauf konzentrieren, die Dinge einfacher zu machen, als die Dinge zu verkürzen.

@ALTree errd handhabt die "ausgeklügelte Fehlerwiederherstellung" von Haus aus.

@jimmyfrasche : errd macht ungefähr das, was Ihr Spielplatz-Beispiel tut, aber auch Fehler und Panik zu

@jimmyfrasche : Ich stimme zu, dass die meisten Vorschläge nicht viel zu dem

@romainmenke : stimme zu, dass zu viel Wert auf Kürze gelegt wird. Um es einfacher zu machen, Dinge richtig zu machen, sollte ein größerer Fokus gelegt werden.

@jba : Der errd-Ansatz macht es ziemlich einfach, Fehler- und Nicht-Fehler-Fluss zu scannen, indem man nur auf die linke Seite schaut (alles, was mit e beginnt, ist Fehler- oder Verzögerungsbehandlung). Es macht es auch sehr einfach zu scannen, welche der Rückgabewerte auf Fehler oder Verzögerung behandelt werden und welche nicht.

@bcmills : Obwohl errd die Scoping-Probleme an sich nicht behebt, beseitigt es die Notwendigkeit, nachgelagerte Fehler an früher deklarierte Fehlervariablen und alle weiterzugeben, wodurch das Problem für die Fehlerbehandlung erheblich gemildert wird, AFAICT.

errd scheint sich ganz auf Panik und Erholung zu verlassen. das hört sich so an, als ob es mit erheblichen Leistungseinbußen verbunden wäre. Aus diesem Grund bin ich mir nicht sicher, ob es eine Gesamtlösung ist.

@urandom : Unter der Haube wird es als Aufschub implementiert.
Wenn der Originalcode:

  • verwendet keine Verzögerung: Die Strafe für die Verwendung von errd ist groß, etwa 100 ns*.
  • verwendet idiomatic defer: die Laufzeit oder der Fehler ist in der gleichen Größenordnung, wenn auch etwas langsamer
  • verwendet die richtige Fehlerbehandlung für Defer: die Laufzeit ist ungefähr gleich; errd kann schneller sein, wenn die Anzahl der Verzögerungen > 1 . ist

Sonstige Gemeinkosten:

  • Das Übergeben von Closures (w.Close) an Defer führt derzeit auch zu einem Overhead von etwa 25 ns* im Vergleich zur Verwendung der DeferClose- oder DeferFunc-API (siehe Version v0.1.0). Nach der Diskussion mit @rsc habe ich es entfernt, um die API einfach zu halten und mich um die spätere Optimierung zu kümmern.
  • Das Umschließen von Inline-Fehlerstrings als Handler ( e.Must(err, msg("oh noes!") ) kostet mit Go 1.8 etwa 30 ns. Mit tip (1.9) allerdings noch eine Belegung, habe ich die Kosten auf 2ns getaktet. Für vorab deklarierte Fehlermeldungen sind die Kosten natürlich noch vernachlässigbar.

(*) Alle Nummern laufen auf meinem 2016 MacBook Pro.

Alles in allem erscheinen die Kosten akzeptabel, wenn Ihr ursprünglicher Code aufschieben verwendet. Wenn nicht, arbeitet Austin daran, die Kosten für den Aufschub deutlich zu senken, sodass die Kosten im Laufe der Zeit sogar sinken können.

Wie auch immer, der Zweck dieses Pakets besteht darin, Erfahrungen darüber zu sammeln, wie sich die Verwendung alternativer Fehlerbehandlungen jetzt anfühlen und nützlich sein würde, damit wir die beste Spracherweiterung in Go 2 erstellen können. Ein Beispiel dafür ist die aktuelle Diskussion, die sich zu sehr auf die Reduzierung von a . konzentriert wenige Zeilen für einfache Fälle, wobei es viel mehr zu gewinnen gibt und andere Punkte wohl wichtiger sind.

@jimmyfrasche :

dann würde ein $EDITOR-Plugin ohne Sprachänderung funktionieren

Ja, genau das argumentiere ich im Gespräch. Hier bin ich argumentieren , dass , wenn wir eine Sprache ändern zu machen, sollte es mit dem „Nebenbei bemerkt“ Konzept in Einklang stehen.

@nigeltao

Sie können sie offensichtlich unterscheiden. Der alte Weg sieht so aus:

Ich spreche vom Deklarationspunkt, nicht vom Verwendungszweck.

Einige der hier diskutierten Vorschläge unterscheiden nicht zwischen den beiden am Ort der Aufforderung, andere jedoch. Wenn wir eine der Optionen wählen, die "Wert oder Fehler" annimmt – wie || , try … catch oder match – dann sollte dies ein Fehler bei der Kompilierung sein diese Syntax mit einer "Wert- und Fehler"-Funktion, und es sollte dem Implementierer der Funktion überlassen sein, zu definieren, was es ist.

Zum Zeitpunkt der Deklaration gibt es derzeit keine Möglichkeit, zwischen "Wert und Fehler" und "Wert oder Fehler" zu unterscheiden:

func Atoi(string) (int, error)

und

func WriteString(Writer, String) (int, error)

haben die gleichen Rückgabetypen, aber unterschiedliche Fehlersemantiken.

@mpvl Ich

Wenn wir allgemeine Helfer wie Top-Level-Funks für die Bearbeitung des Ergebnisses von WithDefault() ignorieren und der Einfachheit halber annehmen, dass wir immer den Kontext verwendet haben, und alle Entscheidungen ignorieren, die für die Leistung getroffen werden, würde sich die absolut minimale Barebone-API auf die unten Operationen?

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

Wenn ich mir den Code ansehe, sehe ich einige gute Gründe, warum er nicht wie oben definiert ist, aber ich versuche, die Kernsemantik zu verstehen, um das Konzept besser zu verstehen. Ich bin mir zum Beispiel nicht sicher, ob IsSentinel im Kern ist oder nicht.

@jimmyfrasche

@bcmills erwähnt, dass dies das Scoping verbessert, aber ich sehe nicht wirklich, wie es sein könnte

Die Hauptverbesserung besteht darin, dass die Variable err außerhalb des Gültigkeitsbereichs bleibt. Das würde Fehler wie die von https://github.com/golang/go/issues/19727 verlinkten vermeiden

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

Der Fehler tritt in der letzten if-Anweisung auf: Der Fehler von Decode wird gelöscht, aber es ist nicht offensichtlich, weil ein err aus einer früheren Prüfung noch im Gültigkeitsbereich war. Im Gegensatz dazu würde dies mit den Operatoren :: oder =? geschrieben:

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

Hier gibt es zwei Verbesserungen des Bereichs, die helfen:

  1. Das erste err (vom früheren Get Aufruf) ist nur für den return Block gültig, kann also nicht versehentlich in nachfolgenden Prüfungen verwendet werden.
  2. Da das err von Decode in derselben Anweisung deklariert wird, in der es auf Null geprüft wird, kann es keine Abweichung zwischen der Deklaration und der Prüfung geben.

(1) allein wäre ausreichend gewesen, um den Fehler zur Kompilierzeit aufzudecken, aber (2) macht es leicht, ihn zu vermeiden, wenn die Guard-Anweisung auf offensichtliche Weise verwendet wird.

@bcmills danke für die Klarstellung

Also, in res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } das =? Makro erweitert zu

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

Wenn das richtig ist, meinte ich das, als ich sagte, es müsste eine andere Semantik als := .

Es scheint, als würde es seine eigenen Verwirrungen verursachen, wie zum Beispiel:

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

Es sei denn, ich habe etwas falsch verstanden.

Ja, das ist die richtige Erweiterung. Sie haben Recht, dass es sich von := , und das ist beabsichtigt.

Es scheint, als würde es seine eigenen Verwirrungen verursachen

Das stimmt. Ob das in der Praxis verwirrend wäre, ist mir nicht klar. Wenn dies der Fall ist, könnten wir ":"-Varianten der Guard-Anweisung zur Deklaration bereitstellen (und die "="-Varianten nur zuweisen lassen).

(Und jetzt denke ich, dass die Operatoren ? und ! statt =? und =! .)

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

aber

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

@mpvl Meine Hauptsorge bezüglich errd betrifft die Handler-Schnittstelle: Sie scheint Pipelines von Callbacks im funktionalen Stil zu fördern, aber meine Erfahrung mit Code im Callback- / Fortsetzungsstil (sowohl in zwingenden Sprachen wie Go und C++ als auch in funktionalen) Sprachen wie ML und Haskell) ist, dass es oft viel schwieriger zu folgen ist als der äquivalente sequentielle / zwingende Stil, der zufällig auch mit dem Rest der Go-Idiome übereinstimmt.

Stellen Sie sich Ketten im Handler Stil als Teil der API vor oder ist Ihr Handler ein Ersatz für eine andere Syntax, die Sie in Betracht ziehen (z. B. etwas, das auf Block S?)

@bcmills Ich bin immer noch nicht mit den magischen Funktionen an Bord, die Dutzende von Konzepten in einer einzigen Zeile in die Sprache einführen und nur mit einer Sache arbeiten, aber ich verstehe endlich, warum sie mehr als nur eine etwas kürzere Art sind, x, err := f(); if err != nil { return err } zu schreiben

@bcmills Ich habe das motivierende Beispiel von =? Vorschlag neu geschrieben, der nicht immer eine neue err-Variable deklariert:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

Der Großteil der Fehlerbehandlung ist unverändert. Ich konnte =? an zwei Stellen verwenden. Erstens hat es keinen wirklichen Nutzen gebracht, den ich sehen konnte. Im zweiten hat es den Code länger gemacht und die Tatsache verschleiert, dass io.Copy zwei Dinge zurückgibt, also wäre es wahrscheinlich besser gewesen, es dort einfach nicht zu verwenden.

@jimmyfrasche Dieser Code ist die Ausnahme, nicht die Regel. Wir sollten keine Funktionen entwerfen, die das Schreiben erleichtern.

Außerdem frage ich mich, ob das recover überhaupt da sein sollte. Wenn w.Write oder r.Read (oder io.Copy !) in Panik gerät, ist es wahrscheinlich am besten zu beenden.

Ohne recover gibt es keine wirkliche Notwendigkeit für defer , und das Ende der Funktion könnte zu werden

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

@jimmyfrasche

// r is interface{} not error so we can't use it here

Beachten Sie, dass es sich bei meiner spezifischen Formulierung (in https://github.com/golang/go/issues/21161#issuecomment-319434101) um Nullwerte handelt, nicht um Fehler.

// what about n? does this need to be prefixed by a '_ ='?

Das ist nicht der Fall, obwohl ich das deutlicher hätte sagen können.

Ich mag @mpvl 's Verwendung von recover in diesem Beispiel nicht besonders: Es fördert die Verwendung von Panik über idiomatischen Kontrollfluss, während wir, wenn überhaupt, meiner Meinung nach überflüssige recover Aufrufe eliminieren sollten ( wie die in fmt ) aus der Standardbibliothek in Go 2.

Mit diesem Ansatz würde ich diesen Code schreiben als:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

Auf der anderen Seite haben Sie Recht, dass es mit der unidiomatischen Wiederherstellung wenig Möglichkeiten gibt, Funktionen anzuwenden, die die Behandlung idiomatischer Fehler unterstützen sollen. Die Trennung der Wiederherstellung von der Operation Close führt jedoch zu einem etwas saubereren IMO-Code.

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

@jba Der festlegt (vorausgesetzt, dies ist in einem potenziellen Fehlerzustand immer noch möglich). Wenn das nicht üblich ist, sollte es wahrscheinlich sein. Ich stimme zu, dass Lesen/Schreiben/Kopieren nicht in Panik geraten sollte, aber wenn es anderen Code gäbe, der aus irgendeinem Grund in Panik geraten könnte, wären wir wieder da, wo wir angefangen haben.

@bcmills , die letzte Überarbeitung sieht besser aus (auch wenn Sie das =? wirklich herausgenommen haben)

@jba :

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

das deckt immer noch nicht den Fall einer Panik im Leser ab. Zugegeben, dies ist ein seltener, aber durchaus wichtiger Fall: Close hier im Panikfall zu rufen, ist sehr schlecht.

Dieser Code ist die Ausnahme, nicht die Regel. Wir sollten keine Funktionen entwerfen, die das Schreiben erleichtern.

@jba : Ich stimme in diesem Fall voll und ganz nicht zu. Es ist wichtig, die Fehlerbehandlung richtig zu machen. Wenn der einfache Fall einfacher wird, werden die Leute noch weniger dazu angehalten, über die richtige Fehlerbehandlung nachzudenken. Ich würde gerne einen Ansatz sehen, der, wie errd , die konservative Fehlerbehandlung trivial macht, während er einige Anstrengungen erfordert, um Regeln zu lockern, nicht etwas, das sich auch nur geringfügig in die andere Richtung bewegt.

@jimmyfrasche : zu deiner Vereinfachung: du hast ungefähr recht.

  • IsSentinel ist nicht unbedingt erforderlich, nur praktisch und üblich. Ich habe es fallen lassen, zumindest vorerst.
  • Der Err in State unterscheidet sich von err, daher löscht Ihre API dies. Für das Verständnis ist es jedoch nicht entscheidend.
  • Handler können Funktionen sein, sind aber hauptsächlich aus Performancegründen Schnittstellen. Ich weiß nur, dass viele Leute das Paket nicht verwenden werden, wenn es nicht optimiert ist. (siehe einige der ersten Kommentare zu errd in dieser Ausgabe)
  • Der Kontext ist unglücklich. AppEngine braucht es, aber sonst nicht viel, denke ich. Es würde mir gut tun, die Unterstützung dafür zu entfernen, bis die Leute sich sträuben.

@mpvl Ich habe nur versucht, es auf ein paar Dinge zu erklären, wie es funktioniert, wie man es benutzt und sich vorzustellen, wie es in den von mir geschriebenen Code passen würde.

@jimmyfrasche : verstanden, obwohl es gut ist, wenn eine API dies nicht erfordert. :)

@bcmills : Handler dienen zum Beispiel in der Reihenfolge ihrer Wichtigkeit einigen Zwecken:

  • einen Fehler einpacken
  • define um Fehler zu ignorieren (um dies explizit zu machen. Siehe Beispiel)
  • Fehler protokollieren
  • Fehlermetriken

Diese müssen wiederum in der Reihenfolge ihrer Wichtigkeit eingeordnet werden nach:

  • Block
  • Linie
  • Paket

Die Standardfehler sind nur dazu da, um sicherzustellen, dass ein Fehler irgendwo behandelt wird,
aber ich könnte nur mit block-level leben. Ich hatte ursprünglich eine API mit Optionen anstelle von Handlern. Dies führte jedoch zu einer größeren und ungeschickteren API, die nicht nur langsamer war.

Ich sehe das Rückrufproblem hier nicht so schlimm. Benutzer definieren einen Runner, indem sie ihm einen Handler übergeben, der aufgerufen wird, wenn ein Fehler auftritt. Der spezifische Läufer wird explizit in dem Block angegeben, in dem die Fehler behandelt werden. In vielen Fällen ist ein Handler nur ein umschlossenes String-Literal, das inline übergeben wird. Ich werde etwas herumspielen, um zu sehen, was nützlich ist und was nicht.

Übrigens, wenn wir die Protokollierung von Fehlern in Handlern nicht fördern sollten, dann kann die Kontextunterstützung wahrscheinlich fallengelassen werden.

@jba :

Außerdem frage ich mich, ob die Wiederherstellung überhaupt da sein sollte. Wenn w.Write oder r.Read (oder io.Copy!) in Panik geraten, ist es wahrscheinlich am besten zu beenden.

writeToGS terminiert immer noch, wenn eine Panik auftritt, wie es sollte (!!!), es stellt lediglich sicher, dass es CloseWithError mit einem Fehler ungleich Null aufruft. Wenn die Panik nicht behandelt wird, wird die Verzögerung immer noch aufgerufen, jedoch mit err == nil, was dazu führt, dass eine potenziell beschädigte Datei auf Cloud Storage materialisiert wird. Hier ist es richtig, CloseWithError mit einem vorübergehenden Fehler aufzurufen und dann die Panik fortzusetzen.

Ich habe eine Reihe von Beispielen wie dieses im Go-Code gefunden. Auch der Umgang mit io.Pipes führt oft zu etwas zu subtilem Code. Der Umgang mit Fehlern ist oft nicht so einfach, wie Sie es jetzt selbst gesehen haben.

@bcmills

Ich mag die Verwendung von recovery in diesem Beispiel durch

Ich versuche nicht, den Gebrauch von Panik zu fördern. Beachten Sie, dass die Panik direkt nach CloseWithError erneut ausgelöst wird und somit den Kontrollfluss ansonsten nicht ändert. Panik bleibt Panik.
Es ist jedoch falsch, hier "recover" nicht zu verwenden, da eine Panik dazu führt, dass der Defer mit einem Null-Fehler aufgerufen wird, was signalisiert, dass das bisher Geschriebene festgeschrieben werden kann.

Das einzige einigermaßen gültige Argument, hier "recover" nicht zu verwenden, ist, dass es sehr unwahrscheinlich ist, dass eine Panik auftritt, selbst bei einem beliebigen Reader (der Reader ist in diesem Beispiel aus einem bestimmten Grund von einem unbekannten Typ :) ).
Für Produktionscode ist dies jedoch eine inakzeptable Haltung. Vor allem beim Programmieren in einem ausreichend großen Umfang wird dies irgendwann passieren (Panik kann auch durch andere Dinge als durch Fehler im Code verursacht werden).

Übrigens, beachten Sie, dass das Paket errd den Benutzer überflüssig macht. Jeder andere Mechanismus, der im Falle einer Panik einen Fehler signalisiert, ist jedoch in Ordnung. Es würde auch funktionieren, bei Panik keine Verschiebungen anzurufen, aber das bringt seine eigenen Probleme mit sich.

Zum Zeitpunkt der Deklaration gibt es derzeit keine Möglichkeit, zwischen "Wert und Fehler" und "Wert oder Fehler" zu unterscheiden:

@bcmills Oh, ich

func Atoi(string) ?int

anstatt

func Atoi(string) (int, error)

aber WriteString würde unverändert bleiben:

func WriteString(Writer, String) (int, error)

Mir gefällt der =? / =! / :=? / :=! Vorschlag von @bcmills / @jba besser als ähnliche Vorschläge. Es hat einige schöne Eigenschaften:

  • Composable (Sie können =? innerhalb eines =? Blocks verwenden)
  • allgemein (beachtet nur den Nullwert, nicht spezifisch für den Fehlertyp)
  • verbesserter Scoping
  • könnte mit defer arbeiten (in einer Variante oben)

Es hat auch einige Eigenschaften, die ich nicht so schön finde.

Zusammensetzung Nester. Bei wiederholter Verwendung wird immer weiter nach rechts eingerückt. Das ist an sich nicht unbedingt eine schlechte Sache, aber ich könnte mir vorstellen, dass in Situationen mit sehr komplizierter Fehlerbehandlung, die den Umgang mit Fehlern erfordert, die Fehler verursachen, Fehler verursachen, der Code, der damit umgeht, schnell viel weniger klar wird als der aktuelle Status Quo. In einer solchen Situation könnte man =? für den äußersten Fehler und if err != nil für die inneren Fehler verwenden, aber hat dies dann wirklich die Fehlerbehandlung im Allgemeinen oder nur im allgemeinen Fall verbessert? Vielleicht ist es nur notwendig, den allgemeinen Fall zu verbessern, aber ich persönlich finde es nicht zwingend.

Es führt Falschheit in die Sprache ein, um ihre Allgemeinheit zu gewinnen. Die Definition von Falschheit als "ist (nicht) der Nullwert" ist absolut vernünftig, aber if err != nil { ist meiner Meinung nach besser als if err { da es explizit ist. Ich würde erwarten, Verrenkungen in freier Wildbahn zu sehen, die versuchen, =? /etc. über einen natürlicheren Kontrollfluss, um zu versuchen, Zugang zu seiner Falschheit zu bekommen. Das wäre sicherlich unidiomatisch und verpönt, aber es würde passieren. Der potenzielle Missbrauch einer Funktion an sich ist zwar kein Argument gegen eine Funktion, sollte jedoch in Betracht gezogen werden.

Der verbesserte Bereich (für die Varianten, die ihre Parameter deklarieren) ist in einigen Fällen nett, aber wenn der Bereich korrigiert werden muss, korrigieren Sie den Bereich im Allgemeinen.

Die Semantik des "einzigen Ergebnisses ganz rechts" ist sinnvoll, erscheint mir aber etwas seltsam. Das ist eher ein Gefühl als ein Argument.

Dieser Vorschlag fügt der Sprache eine Kürze, aber keine zusätzliche Macht hinzu. Es könnte vollständig als ein Präprozessor implementiert werden, der die Makroerweiterung durchführt. Das wäre natürlich unerwünscht: Es würde die Entwicklung von Builds und Fragmenten verkomplizieren und ein solcher Präprozessor wäre äußerst kompliziert, da er typbewusst und hygienisch sein muss. Ich versuche nicht abzutun, indem ich sage "mach einfach einen Präprozessor". Ich erwähne dies nur, um darauf hinzuweisen, dass dieser Vorschlag ganz und gar Zucker ist. Es erlaubt Ihnen nichts zu tun, was Sie in Go jetzt nicht tun könnten; Sie können es nur kompakter schreiben. Ich bin nicht dogmatisch gegen Zucker. Eine sorgfältig gewählte sprachliche Abstraktion hat Macht, aber die Tatsache, dass sie Zucker ist, bedeutet, dass sie betrachtet werden sollte – bis sich sozusagen die Unschuld bewiesen hat.

Die Links der Operatoren sind eine Anweisung, aber eine sehr begrenzte Teilmenge von Anweisungen. Welche Elemente in diese Teilmenge aufgenommen werden sollen, ist ziemlich selbstverständlich, aber zumindest müsste die Grammatik in der Sprachspezifikation umgestaltet werden, um die Änderung zu berücksichtigen.

Möchte sowas

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

zugelassen werden?

Wenn

defer f.Close() :=? err {
    return err
}

erlaubt ist das muss (irgendwie) äquivalent zu . sein

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

was zutiefst problematisch erscheint und wahrscheinlich sehr verwirrende Situationen verursacht, selbst wenn man ignoriert, dass es auf eine sehr un-Go-ähnliche Weise die Auswirkungen der impliziten Zuweisung eines Abschlusses auf die Leistung verbirgt. Die Alternative besteht darin, return vom impliziten Abschluss zurückzugeben und dann eine Fehlermeldung zu erhalten, dass Sie keinen Wert vom Typ error von einem func() was ein bisschen ist stumpf.

Abgesehen von einer leicht verbesserten Korrektur des Umfangs behebt dies jedoch keines der Probleme, mit denen ich bei Fehlern in Go konfrontiert bin. Höchstens die Eingabe von if err != nil { return err } ist ein Ärgernis, modulo die leichten Bedenken hinsichtlich der Lesbarkeit, die ich in #21182 geäußert habe. Die zwei größten Probleme sind

  1. darüber nachdenken, wie man mit dem Fehler umgeht – und es gibt nichts, was eine Sprache dagegen tun kann
  2. einen Fehler selbst zu untersuchen, um zu bestimmen, was in einigen Situationen zu tun ist – einige zusätzliche Konventionen mit Unterstützung des errors Pakets würden hier viel bewirken, obwohl sie nicht alle Probleme lösen können.

Mir ist klar, dass dies nicht die einzigen Probleme sind und dass viele andere Aspekte direkter betreffen, aber mit ihnen verbringe ich die meiste Zeit und finde sie ärgerlicher und lästiger als alles andere.

Eine bessere statische Analyse, um zu erkennen, wenn ich etwas vermasselt habe, wäre natürlich immer wünschenswert (und im Allgemeinen nicht nur dieses Szenario). Interessant wären auch Sprachänderungen und Konventionen, die die Analyse der Quelle erleichtern, damit diese nützlicher sind.

Ich habe gerade viel (VIEL! Entschuldigung!) darüber geschrieben, aber ich lehne den Vorschlag nicht ab. Ich denke, es hat seinen Wert, aber ich bin nicht davon überzeugt, dass es die Messlatte überschreitet oder sein Gewicht trägt.

@jimmyfrasche

Ich erinnere mich, als das aktuelle Verhalten von := eingeführt wurde – ein Großteil dieses Threads ging durcheinander† verlangte nach einer Möglichkeit, explizit annotieren zu können, welche Namen wiederverwendet werden sollen, anstelle des impliziten "Wiederverwendung nur, wenn diese Variable genau im aktuellen Geltungsbereich existiert". “, wo sich meiner Erfahrung nach all die subtilen, schwer zu erkennenden Probleme manifestieren.

† Ich kann den Thread nicht finden, hat jemand einen Link?

Ich denke, Sie erinnern sich an einen anderen Thread, es sei denn, Sie waren an Go beteiligt, als es veröffentlicht wurde. Die Spezifikation vom 09.11.2009, kurz vor der Veröffentlichung, hat:

Im Gegensatz zu regulären Variablendeklarationen kann eine kurze Variablendeklaration Variablen erneut deklarieren, sofern sie ursprünglich im selben Block mit demselben Typ deklariert wurden und mindestens eine der nicht leeren Variablen neu ist.

Ich erinnere mich, dass ich dies beim ersten Lesen der Spezifikation gesehen habe und dachte, dass dies eine großartige Regel war, da ich zuvor eine Sprache mit := verwendet hatte, jedoch ohne diese Wiederverwendungsregel, und es war mühsam, sich neue Namen für dasselbe zu überlegen.

@mpvl
Ich denke, dass die Kniffigkeit Ihres ursprünglichen Beispiels eher auf die
die API, die Sie dort verwenden, als von der Fehlerbehandlung von Go selbst.

Es ist jedoch ein interessantes Beispiel, insbesondere wegen der Tatsache, dass
Sie möchten die Datei nicht normal schließen, wenn Sie in Panik geraten
normales Idiom "defer w.Close()" funktioniert nicht.

Wenn Sie es nicht vermeiden müssen, Close anzurufen, wenn ein
Panik, dann könntest du tun:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

unter der Annahme, dass die Semantik so geändert wurde, dass der Aufruf von Close
nach dem Aufruf von CloseWithError ist ein No-Op.

Ich finde das sieht nicht mehr so ​​schlimm aus.

Selbst mit der Anforderung, dass die Datei nicht fehlerfrei geschrieben wird, wenn eine Panik auftritt, sollte es nicht allzu schwer sein, dies zu berücksichtigen; B. durch Hinzufügen einer Finalize-Funktion, die explizit vor Close aufgerufen werden muss.

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

Das kann die Panik-Fehlermeldung zwar nicht anhängen, aber eine anständige Protokollierung könnte dies klarer machen.
(Die Close-Methode könnte sogar einen Recovery-Aufruf enthalten, obwohl ich mir nicht sicher bin, ob das so ist
eigentlich eine ganz schlechte idee...)

Ich denke jedoch, dass der Aspekt der Panikwiederherstellung in diesem Beispiel in diesem Zusammenhang ein Ablenkungsmanöver ist, da über 99 % der Fälle von Fehlerbehandlung keine Panikwiederherstellung durchführen.

@rogpeppe :

Das kann die Panik-Fehlermeldung zwar nicht anhängen, aber eine anständige Protokollierung könnte dies klarer machen.

Ich glaube nicht, dass das ein Problem ist.

Ihre vorgeschlagene API-Änderung mildert das Problem, löst es jedoch noch nicht vollständig. Die erforderliche Semantik erfordert, dass sich auch anderer Code richtig verhält. Betrachten Sie das folgende Beispiel:

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

An sich zeigt dieser Code, dass die Fehlerbehandlung knifflig oder zumindest chaotisch sein kann (und ich wäre gespannt, wie dies mit den anderen Vorschlägen verbessert werden könnte): Er leitet nachgeschaltete Fehler heimlich durch eine Variable weiter und hat auch ein bisschen clunky if-Anweisung, um sicherzustellen, dass der richtige Fehler übergeben wird. Beides lenkt zu sehr von der „Geschäftslogik“ ab. Die Fehlerbehandlung dominiert den Code. Und dieses Beispiel behandelt noch nicht einmal Panik.

Der Vollständigkeit halber würde dies in errd _würde_ mit Paniken richtig umgehen und würde so aussehen:

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

Wenn der obige Reader (ohne errd ) als Reader an writeToGS übergeben wird und der io.Reader von newReader panics zurückgegeben wird, würde dies immer noch zu einer fehlerhaften Semantik mit Ihrem vorgeschlagenen API-Fix führen (könnte zu einem erfolgreichen Schließen führen). die GS-Datei, nachdem das Rohr auf Panik mit einem Nullfehler geschlossen wurde.)

Dies beweist auch den Punkt. Es ist nicht trivial, über die richtige Fehlerbehandlung in Go nachzudenken. Als ich mir ansah, wie Code aussehen würde, um ihn mit errd , fand ich eine Menge fehlerhaften Codes. Wie schwierig und subtil es ist, eine richtige idiomatische Go-Fehlerbehandlung zu schreiben, habe ich jedoch erst beim Schreiben der Komponententests für das Paket errd gelernt. :)

Eine Alternative zu Ihrer vorgeschlagenen API-Änderung wäre, bei Panik überhaupt keine Verschiebungen vorzunehmen. Dies hat seine eigenen Probleme und würde das Problem nicht vollständig lösen und ist wahrscheinlich rückgängig zu machen, hätte aber einige nette Eigenschaften.

So oder so, am besten wäre eine Sprachänderung, die die Feinheiten der Fehlerbehandlung mildert, anstatt sich auf die Kürze zu konzentrieren.

@mpvl
Ich finde oft bei Fehlerbehandlungscode in Go, dass das Erstellen einer anderen Funktion die Dinge bereinigen kann. Ich würde deinen Code oben etwa so schreiben:

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

Ich gehe davon aus, dass r.Close keinen nützlichen Fehler zurückgibt - wenn Sie einen Reader vollständig durchgelesen haben und gerade nur auf io.EOF gestoßen sind, spielt es mit ziemlicher Sicherheit keine Rolle, ob es beim Schließen einen Fehler zurückgibt.

Ich bin nicht an der errd-API interessiert - sie ist zu empfindlich gegenüber dem Starten von Goroutinen. Zum Beispiel: https://play.golang.org/p/iT441gO5us Ob doSomething eine Goroutine zum Ausführen der . startet oder nicht
Die Funktion in sollte die Korrektheit des Programms nicht beeinträchtigen, aber bei der Verwendung von errd tut sie es. Sie erwarten, dass Paniken Abstraktionsgrenzen sicher überschreiten, und dies ist in Go nicht der Fall.

@mpvl

defer w.CloseWithError(err)

Übrigens, diese Zeile ruft CloseWithError immer mit einem Null-Fehlerwert auf. Ich glaube du wolltest
schreiben:

defer func() { 
   w.CloseWithError(err)
}()

@mpvl

Beachten Sie, dass der Fehler, der von der Methode Close bei einem io.Reader wird, fast nie nützlich ist (siehe Liste in https://github.com/golang/go/issues/20803#issuecomment-312318808 ).

Das legt nahe, dass wir Ihr Beispiel heute so schreiben sollten:

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

...was mir völlig in Ordnung erscheint, abgesehen davon, dass es etwas ausführlich ist.

Es stimmt, dass es im Falle einer Panik einen Null-Fehler an w.CloseWithError übergibt, aber das ganze Programm bricht an diesem Punkt trotzdem ab. Wenn es wichtig ist, niemals mit einem Null-Fehler zu schließen, ist dies eine einfache Umbenennung plus eine zusätzliche Zeile:

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

@rogpeppe : in der Tat, danke. :)

Ja, das Problem mit der Goroutine ist mir bekannt. Es ist unangenehm, aber wahrscheinlich etwas, das mit einem Tierarztcheck nicht schwer zu erwischen ist. Wie auch immer, ich sehe errd nicht als endgültige Lösung, sondern eher als eine Möglichkeit, Erfahrungen zu sammeln, wie man die Fehlerbehandlung am besten angeht. Im Idealfall würde es eine Sprachänderung geben, die die gleichen Probleme löst, jedoch mit den entsprechenden Einschränkungen.

Sie erwarten, dass Paniken Abstraktionsgrenzen sicher überschreiten, und dies ist in Go nicht der Fall.

Das erwarte ich nicht. In diesem Fall erwarte ich, dass APIs keinen Erfolg melden, wenn dies nicht der Fall war. Ihr letztes Codestück verarbeitet es korrekt, da es keine Verzögerung für den Writer verwendet. Aber das ist sehr subtil. Viele Benutzer würden in diesem Fall defer verwenden, da dies als idiomatisch gilt.

Vielleicht könnte eine Reihe von Tierarztkontrollen problematische Verwendungen von Aufschiebungen aufdecken. Dennoch wird sowohl im ursprünglichen "idiomatischen" Code als auch in Ihrem letzten überarbeiteten Stück viel herumgebastelt, um Feinheiten der Fehlerbehandlung für etwas zu umgehen, das ansonsten ein ziemlich einfacher Code ist. Der Workaround-Code dient nicht dazu, herauszufinden, wie mit bestimmten Fehlerfällen umzugehen ist, sondern ist reine Verschwendung von Gehirnzyklen, die für den produktiven Einsatz verwendet werden könnten.

Was ich insbesondere von errd lernen

@jimmyfrasche

Es führt Falschheit in die Sprache ein, um ihre Allgemeinheit zu gewinnen.

Das ist ein sehr guter Punkt. Die üblichen Probleme mit Falschheit entstehen dadurch, dass man vergisst, eine boolesche Funktion aufzurufen oder einen Zeiger auf nil zu dereferenzieren.

Letzteres könnten wir angehen, indem wir den Operator so definieren, dass er nur mit nillbaren Typen arbeitet (und dadurch wahrscheinlich =! , da es meistens nutzlos wäre).

Wir könnten ersteres ansprechen, indem wir es weiter einschränken, um nicht mit Funktionstypen oder nur mit Zeiger- oder Schnittstellentypen zu arbeiten: dann wäre klar, dass die Variable kein boolescher Wert ist, und Versuche, sie für boolesche Vergleiche zu verwenden, wären mehr offensichtlich falsch.

Wäre etwas wie [ MustF ] erlaubt?

Ja.

Wenn [ defer f.Close() :=? err { ] erlaubt ist, muss das (irgendwie) äquivalent zu . sein
[ defer func() { … }() ].

Nicht unbedingt, nein. Es könnte seine eigene Semantik haben (eher call/cc als eine anonyme Funktion). Ich habe keine Spezifikationsänderung für die Verwendung von =? in defer (es würde zumindest eine Änderung der Grammatik erfordern), daher bin ich mir nicht sicher, wie kompliziert eine solche Definition wäre .

Die beiden größten Probleme sind […] 2. einen Fehler selbst zu untersuchen, um zu bestimmen, was in bestimmten Situationen zu tun ist

Ich stimme zu, dass dies in der Praxis ein größeres Problem darstellt, aber es scheint mehr oder weniger orthogonal zu diesem Problem zu sein (wobei es eher um die Reduzierung von Boilerplate und dem damit verbundenen Fehlerpotenzial geht).

( @rogpeppe , @davecheney , @dsnet , @crawshaw , ich und einige andere, die ich sicherlich vergessen habe, hatten auf der GopherCon eine nette Diskussion über APIs zur Fehlerprüfung, und ich hoffe, dass wir auch in dieser Hinsicht einige gute Vorschläge sehen werden , aber ich denke wirklich, das ist eine Sache für ein anderes Thema.)

@bcmills : Dieser Code hat zwei Probleme 1) wie @rogpeppe erwähnt: err, der an CloseWithError übergeben wird, ist immer null, und 2) er behandelt immer noch keine Panik, was bedeutet, dass die API den Erfolg explizit meldet, wenn eine Panik auftritt (die Das zurückgegebene r kann ein io.EOF ausgeben, auch wenn nicht alle Bytes geschrieben wurden), selbst wenn 1 fest ist.

Ansonsten stimme ich zu, dass der von Close zurückgegebene Fehler oft ignoriert werden kann. Allerdings nicht immer (siehe erstes Beispiel).

Ich finde es etwas überraschend, dass bei meinen eher einfachen Beispielen (einschließlich eines von mir) etwa 4 oder 5 fehlerhafte Vorschläge gemacht wurden, und ich habe immer noch das Gefühl, dass ich argumentieren muss, dass die Fehlerbehandlung in Go nicht trivial ist. :)

@bcmills :

Es stimmt, dass es im Panikfall einen Null-Fehler an w.CloseWithError übergibt, aber das ganze Programm bricht an diesem Punkt trotzdem ab.

Macht es? Defers dieser Goroutine werden immer noch aufgerufen. Soweit ich weiß, werden sie bis zur Fertigstellung laufen. In diesem Fall signalisiert der Close ein io.EOF.

Siehe zum Beispiel https://play.golang.org/p/5CFbsAe8zF. Nachdem die Goroutine in Panik gerät, übergibt sie immer noch glücklich "foo" an die andere Goroutine, die es dann immer noch schafft, es nach Stdout zu schreiben.

In ähnlicher Weise kann anderer Code ein falsches io.EOF von einer in Panik geratenen Goroutine (wie in Ihrem Beispiel) erhalten, den Erfolg abschließen und glücklich eine Datei an GS übergeben, bevor die in Panik geratene Goroutine ihre Panik wieder aufnimmt.

Ihr nächstes Argument könnte sein: Nun, schreibe keinen fehlerhaften Code, aber:

  • dann machen Sie es einfacher, diese Fehler zu verhindern, und
  • Panik kann durch externe Faktoren wie OOMs verursacht werden.

Wenn es wichtig ist, niemals mit einem Null-Fehler zu schließen, ist dies eine einfache Umbenennung plus eine zusätzliche Zeile:

Es sollte immer noch mit nil schließen, um io.EOF zu signalisieren, wenn es fertig ist, damit das nicht funktioniert.

Wenn es wichtig ist, niemals mit einem Null-Fehler zu schließen, ist dies eine einfache Umbenennung plus eine zusätzliche Zeile:

Es sollte immer noch mit nil schließen, um io.EOF zu signalisieren, wenn es fertig ist, damit das nicht funktioniert.

Warum nicht? Das return err am Ende setzt rerr auf nil .

@bcmills : ah ich verstehe jetzt was du meinst. Ja, das sollte funktionieren. Ich mache mir jedoch keine Sorgen um die Anzahl der Zeilen, sondern eher um die Feinheit des Codes.

Ich finde, dass dies in der gleichen Kategorie von Problemen wie das variable Shadowing liegt, nur dass es weniger wahrscheinlich ist (was es möglicherweise noch schlimmer macht). Die meisten variablen Shadowing-Bugs, auf die Sie bei guten Unit-Tests stoßen werden. Willkürliche Paniken sind schwerer zu testen.

Wenn Sie in großem Maßstab arbeiten, ist es ziemlich sicher, dass sich Fehler wie diese manifestieren. Ich mag paranoid sein, aber ich habe weit weniger wahrscheinliche Szenarien gesehen, die zu Datenverlust und -beschädigung führen. Normalerweise ist dies in Ordnung, aber nicht für die Transaktionsverarbeitung (wie das Schreiben von gs-Dateien).

Ich hoffe, es macht Ihnen nichts aus, dass ich Ihren Vorschlag mit einer alternativen Syntax kapere – wie denken die Leute über so etwas:

return err if f, err := os.Open("..."); err != nil

@SirCmpwn Das begräbt die Lede. Das Einlesen einer Funktion sollte am einfachsten der normale Kontrollfluss sein, nicht die Fehlerbehandlung.

Das ist fair, aber Ihr Vorschlag ist mir auch unangenehm - er führt eine undurchsichtige Syntax (||) ein, die sich anders verhält, als die Benutzer es erwarten || sich benehmen. Ich bin mir nicht sicher, was die richtige Lösung ist, werde noch etwas darüber nachdenken.

@SirCmpwn Ja, wie ich im ursprünglichen Beitrag sagte: "Ich schreibe diesen Vorschlag hauptsächlich, um Leute, die die Go-Fehlerbehandlung vereinfachen möchten, zu ermutigen, darüber nachzudenken, wie man Fehler einfach in den Kontext einbetten kann und nicht nur den Fehler unverändert zurückgibt ." Ich habe meinen Vorschlag so gut es ging geschrieben, aber ich erwarte nicht, dass er angenommen wird.

Verstanden.

Dies ist etwas radikaler, aber vielleicht würde ein makrogesteuerter Ansatz besser funktionieren.

f = try!(os.Open("..."))

try! würde den letzten Wert im Tupel essen und ihn zurückgeben, falls nicht nil, andernfalls den Rest des Tupels zurückgeben.

Ich möchte vorschlagen, dass unsere Problembeschreibung lautet:

Die Fehlerbehandlung in Go ist ausführlich und repetitiv. Das idiomatische Format der Fehlerbehandlung von Go macht es schwieriger, den fehlerfreien Kontrollfluss zu sehen, und die Ausführlichkeit ist insbesondere für Neulinge unattraktiv. Bis heute erfordern für dieses Problem vorgeschlagene Lösungen typischerweise handwerkliche einmalige Fehlerbehandlungsfunktionen, reduzieren die Lokalität der Fehlerbehandlung und erhöhen die Komplexität. Da es eines der Ziele von Go ist, den Autor zu zwingen, über Fehlerbehandlung und -wiederherstellung nachzudenken, sollte jede Verbesserung der Fehlerbehandlung auch auf diesem Ziel aufbauen.

Um diese Problemstellung anzugehen, schlage ich diese Ziele für Verbesserungen der Fehlerbehandlung in Go 2.x vor:

  1. Reduziert sich wiederholende Fehlerbehandlungs-Boilerplates und maximiert den Fokus auf die primäre Absicht des Codepfads.
  2. Fördert eine ordnungsgemäße Fehlerbehandlung, einschließlich des Umschließens von Fehlern bei der Weiterleitung.
  3. Hält sich an die Go-Designprinzipien der Klarheit und Einfachheit.
  4. Ist in einem möglichst breiten Spektrum von Fehlerbehandlungssituationen anwendbar.

Bewertung dieses Vorschlags:

f.Close() =? err { return fmt.Errorf(…, err) }

nach diesen Zielen würde ich schlussfolgern, dass es bei Ziel Nr. 1 gut gelingt. Ich bin mir nicht sicher, wie es bei #2 hilft, aber es macht das Hinzufügen von Kontext auch nicht weniger wahrscheinlich (mein eigener Vorschlag teilte diese Schwäche bei #2). Bei #3 und #4 gelingt es allerdings nicht wirklich:
1) Wie andere gesagt haben, ist die Fehlerwertprüfung und -zuweisung undurchsichtig und ungewöhnlich; und
2) Die Syntax von =? ist ebenfalls ungewöhnlich. Es ist besonders verwirrend, wenn es mit der ähnlichen, aber unterschiedlichen =! Syntax kombiniert wird. Es wird eine Weile dauern, bis sich die Leute an ihre Bedeutungen gewöhnen; und
3) Das Zurückgeben eines gültigen Werts zusammen mit dem Fehler ist häufig genug, dass jede neue Lösung auch diesen Fall behandeln sollte.

Es kann eine gute Idee sein, die Fehlerbehandlung zu einem Block zu machen, obwohl dies, wie andere vorgeschlagen haben, mit Änderungen an gofmt kombiniert

Wenn Sie mich in der Zusammenfassung gefragt hätten, hätte ich vielleicht zugestimmt, dass eine allgemeinere Lösung einer spezifischen Lösung für die Fehlerbehandlung vorzuziehen wäre, solange sie die oben genannten Verbesserungsziele für die Fehlerbehandlung erfüllt. Nachdem ich diese Diskussion gelesen und genauer darüber nachgedacht habe, bin ich jedoch geneigt zu glauben, dass eine spezifische Lösung für die Fehlerbehandlung zu größerer Klarheit und Einfachheit führen wird. Während Fehler in Go nur Werte sind, macht die Fehlerbehandlung einen so bedeutenden Teil jeder Programmierung aus, dass eine bestimmte Syntax zur Darstellung des Fehlerbehandlungscodes klar und prägnant erscheint. Ich fürchte, wir machen ein bereits schwieriges Problem (eine saubere Lösung für die Fehlerbehandlung) noch schwieriger und komplizierter, wenn wir es mit anderen Zielen wie Scoping und Composability verbinden.

Unabhängig davon, wie @rsc in seinem Artikel Toward Go 2 betont , werden Syntaxvorschlag ohne Erfahrungsberichte vorankommen, die zeigen, dass das Problem erheblich ist. Vielleicht sollten wir, anstatt verschiedene Syntaxvorschläge zu diskutieren, anfangen, nach unterstützenden Daten zu suchen?

Unabhängig davon, wie @rsc in seinem Artikel Toward Go 2 wahrscheinlich weder die Problembeschreibung, die Ziele noch ein

Ich denke, das ist selbstverständlich, wenn wir davon ausgehen, dass Ergonomie wichtig ist. Öffnen Sie eine beliebige Go-Codebasis und suchen Sie nach Stellen, an denen es Möglichkeiten gibt, Dinge zu TROCKNEN und/oder die Ergonomie zu verbessern, die die Sprache ansprechen kann - im Moment ist die Fehlerbehandlung ein klarer Ausreißer. Ich denke, der Ansatz von Toward Go 2 könnte fälschlicherweise dafür plädieren, Probleme mit Workarounds zu ignorieren - in diesem Fall müssen die Leute nur grinsen und es ertragen.

if $val, err := $operation($args); err != nil {
  return err
}

Wenn es mehr Boilerplate als Code gibt, ist das Problem imho offensichtlich.

@billyh

Ich finde, dass das Format: f.Close() =? err { return fmt.Errorf(…, err) } ausführlich und verwirrend ist. Ich persönlich finde nicht, dass der Fehlerteil in einem Block sein sollte. Das würde unweigerlich dazu führen, dass es in 3 Zeilen statt in 1 verteilt wird. Darüber hinaus kann man in der Off-Änderung, die Sie mehr tun müssen, als nur einen Fehler zu ändern, bevor Sie ihn zurückgeben, einfach das aktuelle if err != nil { ... } -Syntax.

Der Operator =? ist auch etwas verwirrend. Es ist nicht sofort ersichtlich, was dort passiert.

Mit so etwas:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
oder die Kurzform:
file := os.Open("/some/file") or raise
und das aufgeschobene:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
ist etwas wortreicher und die Wortwahl könnte die anfängliche Verwirrung verringern (dh die Leute assoziieren raise sofort mit einem ähnlichen Schlüsselwort aus anderen Sprachen wie Python oder folgern einfach, dass die Erhöhung den Fehler auslöst/last-non- Standardwert den Stack zum Aufrufer aufwärts).

Es ist auch eine gute zwingende Lösung, die nicht versucht, jede mögliche obskure Fehlerbehandlung unter der Sonne zu lösen. Der bei weitem größte Teil der Fehlerbehandlung in freier Wildbahn ist von der oben genannten Art. Für Letzteres hilft auch die aktuelle Syntax.

Bearbeiten:
Wenn wir die "Magie" etwas reduzieren wollen, könnten die vorherigen Beispiele auch so aussehen:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
Ich persönlich finde die vorherigen Beispiele besser, da sie die gesamte Fehlerbehandlung nach rechts verschieben, anstatt sie wie hier aufzuteilen. Dies könnte jedoch klarer sein.

Ich möchte vorschlagen, dass unsere Problemstellung lautet, ...

Ich stimme der Problembeschreibung nicht zu. Ich möchte eine Alternative vorschlagen:


Fehlerbehandlung existiert aus sprachlicher Sicht nicht. Das einzige, was Go bietet, ist ein vordeklarierter Fehlertyp, und selbst das dient nur der Bequemlichkeit, da es nichts wirklich Neues ermöglicht. Fehler sind nur Werte . Die Fehlerbehandlung ist nur normaler Benutzercode. Es ist nichts Besonderes aus dem Sprach-POV und es sollte nichts Besonderes daran sein. Das einzige Problem bei der Fehlerbehandlung besteht darin, dass manche Leute glauben, dass diese wertvolle und schöne Einfachheit um jeden Preis beseitigt werden muss.

In Anlehnung an das, was Cznic sagt, wäre es schön, eine Lösung zu haben, die nicht nur für die Fehlerbehandlung nützlich ist.

Eine Möglichkeit, die Fehlerbehandlung allgemeiner zu gestalten, besteht darin, sie in Begriffen von Union-Typen/Summen-Typen und Entpacken zu betrachten. Swift und Rust haben beide Lösungen mit ? ! Syntax, obwohl ich denke, dass die von Rust etwas instabil war.

Wenn wir Summentypen nicht zu einem Konzept auf hoher Ebene machen möchten, könnten wir es nur zu einem Teil der Mehrfachrückgabe machen, so wie Tupel nicht wirklich Teil von Go sind, aber Sie können immer noch Mehrfachrückgaben durchführen.

Ein von Swift inspirierter Stich in die Syntax:

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

Sie können dies auch für andere Dinge verwenden, wie zum Beispiel:

guard val := myMap[key] else { val = "default" }

Die von @bcmills und @jba vorgeschlagene Lösung =? ist nicht nur für Fehler gedacht , das Konzept ist für Nicht-Null. Dieses Beispiel wird normal funktionieren.

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

Die Hauptidee dieses Vorschlags sind die Randnotizen, um den Hauptzweck des Codes zu trennen und Nebenfälle beiseite zu lassen, um das Lesen zu erleichtern.
Für mich ist das Lesen eines Go-Codes in einigen Fällen nicht kontinuierlich, oft muss man die Idee mit if err!= nil {return err} stoppen, daher scheint mir die Idee der Randnotizen interessant zu sein, wie in einem Buch, das wir lesen die Hauptidee fortwährend und lesen Sie dann die Randnotizen. ( @jba reden )
In sehr seltenen Situationen ist der Fehler der Hauptzweck einer Funktion, vielleicht bei einer Wiederherstellung. Normalerweise fügen wir bei einem Fehler etwas Kontext, Protokoll und Rückgabe hinzu. In diesen Fällen können Randnotizen Ihren Code lesbarer machen.
Ich weiß nicht, ob es die beste Syntax ist, besonders gefällt mir der Block im zweiten Teil nicht, eine Randnotiz muss klein sein, eine Zeile sollte reichen

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

@billyh

  1. Wie andere gesagt haben, ist die Prüfung und Zuweisung von Fehlerwerten undurchsichtig und ungewöhnlich; und

Seien Sie bitte konkreter: "undurchsichtig und ungewöhnlich" sind schrecklich subjektiv. Können Sie einige Codebeispiele nennen, bei denen der Vorschlag Ihrer Meinung nach verwirrend wäre?

  1. Das =? Syntax ist auch ungewöhnlich. […]

IMO ist das ein Feature. Wenn jemand einen ungewöhnlichen Operator sieht, vermute ich, dass er eher dazu neigt, nachzusehen, was er tut, anstatt nur etwas anzunehmen, das möglicherweise korrekt ist oder nicht.

  1. Die Rückgabe eines gültigen Werts zusammen mit dem Fehler ist häufig genug, dass jede neue Lösung auch diesen Fall behandeln sollte.

Es tut?

Lesen Sie den Vorschlag sorgfältig durch: =? führt Aufgaben aus, bevor Block ausgewertet wird, sodass er auch für diesen Fall verwendet werden könnte:

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

Und wie @nigeltao bemerkte, können Sie immer das vorhandene Muster 'n, err := r.Read(buf)` verwenden. Das Hinzufügen einer Funktion, die beim Scoping und Boilerplate für den allgemeinen Fall hilft, bedeutet nicht, dass wir es auch für ungewöhnliche Fälle verwenden müssen.

Vielleicht sollten wir, anstatt verschiedene Syntaxvorschläge zu diskutieren, anfangen, nach unterstützenden Daten zu suchen?

Sehen Sie sich die zahlreichen Ausgaben (und ihre Beispiele) an, die Ian im ursprünglichen Beitrag verlinkt hat.
Siehe auch https://github.com/golang/go/wiki/ExperienceReports#error -handling.

Wenn Sie aus diesen Berichten konkrete Erkenntnisse gewonnen haben, teilen Sie diese bitte mit.

@urandom

Ich persönlich finde nicht, dass der Fehlerteil in einem Block sein sollte. Das würde unweigerlich dazu führen, dass es in 3 Zeilen statt in 1 verteilt wird.

Der Block hat einen zweifachen Zweck:

  1. einen klaren visuellen und grammatikalischen Bruch zwischen dem fehlererzeugenden Ausdruck und seinem Handler zu schaffen, und
  2. um eine breitere Palette von Fehlerbehandlungen zu ermöglichen ( gemäß dem erklärten Ziel von

3 Zeilen gegenüber 1 ist nicht einmal eine Sprachänderung: Wenn die Anzahl der Zeilen Ihr größtes Anliegen ist, könnten wir dies mit einer einfachen Änderung in gofmt .

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

Wir haben bereits return und panic ; Das Hinzufügen von raise zu diesen scheint zu viele Möglichkeiten hinzuzufügen, um eine Funktion mit zu geringem Gewinn zu verlassen.

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

errors.ReplaceIfNil(err, err2) würde eine sehr ungewöhnliche Pass-by-Referenz-Semantik erfordern.
Sie könnten stattdessen err Zeiger übergeben, nehme ich an:

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

aber es kommt mir trotzdem sehr seltsam vor. Konstruiert das or Token einen Ausdruck, eine Anweisung oder etwas anderes? (Ein konkreterer Vorschlag wäre hilfreich.)

@carlmjohnson

Wie wäre die konkrete Syntax und Semantik Ihrer guard … else Anweisung? Für mich sieht es sehr nach =? oder :: wenn die Token und die variablen Positionen vertauscht sind. (Auch hier würde ein konkreterer Vorschlag helfen: Was ist die tatsächliche Syntax und Semantik, die Sie im Sinn haben?)

@bcmills
Das hypothetische ReplaceIfNil wäre einfach:

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

Daran ist nichts Ungewöhnliches. Vielleicht der Name...

or wäre ein binärer Operator, wobei der linke Operand entweder eine IdentifierList oder ein PrimaryExpr wäre. Im ersteren Fall wird es auf den ganz rechten Bezeichner reduziert. Es erlaubt dann die Ausführung des rechten Operanden, wenn der linke kein Standardwert ist.

Aus diesem Grund brauchte ich danach ein weiteres Token, um die Standardwerte zurückzugeben, für alle außer dem letzten Parameter in der Funktion Result, der danach den Wert des Ausdrucks annehmen würde.
IIRC, es gab vor nicht allzu langer Zeit einen anderen Vorschlag, der die Sprache ein '...' oder etwas hinzufügen würde, das die mühsame Initialisierung der Standardwerte ersetzen würde. Aus diesem Grund könnte das Ganze so aussehen:

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

Was den Block betrifft, so verstehe ich, dass er eine breitere Handhabung ermöglicht. Ich bin mir persönlich nicht sicher, ob der Anwendungsbereich dieses Vorschlags darin bestehen sollte, alle möglichen Szenarien abzudecken, anstatt hypothetische 80 % abzudecken. Und ich persönlich glaube, dass es wichtig ist, wie viele Zeilen ein Ergebnis benötigt (obwohl ich nie gesagt habe, dass dies meine größte Sorge ist, nämlich die Lesbarkeit oder deren Fehlen, wenn obskure Token wie =? verwendet werden). Wenn dieser neue Vorschlag im allgemeinen Fall mehrere Zeilen umfasst, sehe ich persönlich seine Vorteile gegenüber etwas wie:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • wenn die oben definierten Variablen außerhalb des if Bereichs verfügbar gemacht werden sollen.
    Und das würde eine Funktion mit nur ein paar solcher Anweisungen aufgrund des visuellen Rauschens dieser Fehlerbehandlungsblöcke immer noch schwerer lesbar machen. Und das ist eine der Beschwerden, die Leute haben, wenn sie die Fehlerbehandlung in go diskutieren.

@urandom

or wäre ein binärer Operator, wobei der linke Operand entweder eine IdentifierList oder ein PrimaryExpr wäre. […] Es erlaubt dann die Ausführung des rechten Operanden, wenn der linke kein Standardwert ist.

Die binären Operatoren von Go sind Ausdrücke, keine Anweisungen, daher würden viele Fragen aufwerfen, or einem binären Operator zu machen. (Was ist die Semantik von or als Teil eines größeren Ausdrucks, und wie passt das zu den Beispielen, die Sie mit := gepostet haben?)

Angenommen, es handelt sich tatsächlich um eine Anweisung, was ist der rechte Operand? Wenn es ein Ausdruck ist, welchen Typ hat es, und kann raise als Ausdruck in anderen Kontexten verwendet werden? Wenn es sich um eine Anweisung handelt, welche Semantik hat sie außer einem raise ? Oder schlagen Sie vor, dass or raise im Wesentlichen eine einzelne Anweisung ist (zB or raise als Syntaxalternative zu :: oder =? )?

Kann ich schreiben

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

?

Kann ich schreiben

f(r.Read(buf) or raise err)

?

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

Nein, dies wäre wegen des zweiten raise ungültig. Wenn das nicht vorhanden war, sollte die gesamte Transformationskette durchlaufen und das Endergebnis an den Aufrufer zurückgegeben werden. Obwohl eine solche Semantik als Ganzes wahrscheinlich nicht benötigt wird, da Sie einfach schreiben können:

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

Wenn wir meinen ursprünglichen Kommentar annehmen - wobei oder den letzten Wert der linken Seite annehmen würde, so dass, wenn es ein Standardwert wäre, der letzte Ausdruck zum Rest der Ergebnisliste ausgewertet würde; dann sollte das ja gültig sein. Wenn in diesem Fall r.Read einen Fehler zurückgibt, wird dieser Fehler an den Aufrufer zurückgegeben. Andernfalls würde n an f

Bearbeiten:

Sofern mich die Begriffe nicht verwirren, stelle ich mir or als binären Operator vor, dessen Operanden vom gleichen Typ sein müssen (aber etwas magisch, wenn der linke Operand eine Liste von Dingen ist, und in diesem Fall nimmt es das letzte Element der genannten Liste von Dingen). raise wäre ein unärer Operator, der seinen Operanden nimmt und von der Funktion zurückkehrt, wobei der Wert dieses Operanden als Wert des letzten Rückgabearguments verwendet wird, wobei die vorhergehenden Standardwerte haben. Sie könnten dann technisch raise in einer eigenständigen Anweisung verwenden, um von einer Funktion zurückzukehren, auch bekannt als return ..., err

Dies wird der Idealfall sein, aber ich bin auch damit einverstanden, dass or raise nur eine Syntaxalternative zu =? , solange es auch eine einfache Anweisung anstelle eines Blocks akzeptiert, um die meisten Anwendungsfälle weniger ausführlich abdecken. Oder wir können auch eine aufgeschobene Grammatik verwenden, bei der ein Ausdruck akzeptiert wird. Dies würde die meisten Fälle abdecken wie:

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

und komplexe Fälle:

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

Wenn ich noch ein wenig über meinen Vorschlag nachdenke, lasse ich das bisschen über Vereinigungs-/Summentypen fallen. Die Syntax, die ich vorschlage, ist

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

Bei einem Ausdruck wird der Ausdruck ausgewertet und wenn das Ergebnis ungleich true bei booleschen Ausdrücken oder dem Leerwert bei anderen Ausdrücken ist, wird BLOCK ausgeführt. Bei einer Zuweisung wird der zuletzt zugewiesene Wert für != true / != nil ausgewertet. Nach einer Guard-Anweisung werden alle Zuweisungen im Gültigkeitsbereich vorgenommen (es wird kein neuer Blockbereich erstellt [außer vielleicht für die letzte Variable?]).

In Swift muss der BLOCK für guard Anweisungen eine von return , break , continue oder throw . Ich habe mich noch nicht entschieden, ob mir das gefällt oder nicht. Es scheint einen Mehrwert zu schaffen, da ein Leser anhand des Wortes guard weiß, was folgen wird.

Folgt jemand Swift gut genug, um zu sagen, ob guard von dieser Community gut angesehen wird?

Beispiele:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

Imho diskutieren hier alle zu viele Probleme auf einmal, aber das häufigste Muster in der Realität ist "Rückgabefehler wie er ist". Warum also nicht das meiste Problem mit etw angehen wie:

code, err ?= fn()

was bedeutet, dass die Funktion bei err != nil zurückgeben sollte?

für := Operator können wir ?:= . einführen

code, err ?:= fn()

Situation mit ?:= scheint aufgrund von Shadowing schlimmer zu sein, da der Compiler die Variable "err" an den gleichnamigen err-Rückgabewert übergeben muss.

Ich bin eigentlich ziemlich aufgeregt, dass einige Leute sich darauf konzentrieren, es einfacher zu machen, richtigen Code zu schreiben, anstatt nur falschen Code zu kürzen.

Einige Notizen:

Ein interessanter "Erfahrungsbericht" von einem der Designer von Midori bei Microsoft zu den Fehlermodellen.

Ich denke, einige Ideen aus diesem Dokument und Swift können wunderbar auf Go2 angewendet werden.

Durch die Einführung eines neuen reservierten throws Schlüsselworts können Funktionen wie folgt definiert werden:

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

Der Versuch, diese Funktion von einer anderen, nicht auslösenden Funktion aufzurufen, führt aufgrund eines nicht behandelten Wurffehlers zu einem Kompilierungsfehler.
Stattdessen sollten wir in der Lage sein, Fehler zu verbreiten, von denen alle zustimmen, dass sie ein häufiger Fall sind, oder damit umgehen können.

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

Für Fälle, in denen wir wissen, dass eine Methode nicht fehlschlagen wird, oder in Tests, können wir ähnlich wie swift try! einführen.

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

Bin mir aber nicht sicher (ähnlich wie schnell):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1. mehrere Rückgabewerte func ReadRune() (ch Rune, size int) throws { ... }
ps2. wir können mit return try Get() oder return try! Get()
ps3. wir können jetzt Anrufe wie buffer.NewBuffer(try Get()) oder buffer.NewBuffer(try! Get())
ps4. Bei Anmerkungen nicht sicher (einfache Möglichkeit, errors.Wrap(err, "context") zu schreiben)
ps5. das sind eigentlich ausnahmen
ps6. Der größte Gewinn sind Kompilierzeitfehler für ignorierte Ausnahmen

Vorschläge, die Sie schreiben, werden im Midori-Link mit all den schlechten beschrieben
Seiten davon ... Und eine offensichtliche Konsequenz von "Würfen" werden "Leute" sein
hasse es". Warum sollte man jedes Mal "Würfe" schreiben für die meisten
Funktionen?

Übrigens, Ihre Absicht, Fehler zu überprüfen und nicht zu ignorieren, kann sein
auch auf Nicht-Fehler-Typen angewendet und imho besser in einem mehr
generalisierte Form (zB gcc __attribute__((warn_unused_result))).

Was die Form des Operators angeht, würde ich entweder eine Kurzform vorschlagen oder
Keyword-Formular wie folgt:

?= fn() ODER check fn() -- Fehler an den Aufrufer weitergeben
!= fn() ODER nofail fn() -- Panik bei Fehler

Am Sa, 26. August 2017 um 12:15 Uhr, nvartolomei [email protected]
schrieb:

Einige Notizen:

Ein interessanter Erfahrungsbericht
http://joeduffyblog.com/2016/02/07/the-error-model/ von einem von
Designer von Midori bei Microsoft über die Fehlermodelle.

Ich denke, einige Ideen aus diesem Dokument und Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
lässt sich wunderbar auf Go2 anwenden.

Durch die Einführung eines neuen reservierten throws-Schlüsselworts können Funktionen wie folgt definiert werden:

func Get() []Byte wirft {
wenn (...) {
Fehler auslösen.New("oops")
}

[]Byte{...} zurückgeben
}

Der Versuch, diese Funktion von einer anderen, nicht werfenden Funktion aufzurufen, wird
zu einem Kompilierungsfehler aufgrund eines nicht behandelten Wurffehlers führen.
Stattdessen sollten wir in der Lage sein, Fehler zu verbreiten, und alle sind sich einig:
allgemeinen Fall, oder behandeln Sie es.

func ScrapeDate() time.Time wirft {
body := Get() // Kompilierungsfehler, nicht behandelter Wurf
body := try Get() // Wir haben über potenzielle Wurfobjekte explizit gesprochen

// ...
}

In Fällen, in denen wir wissen, dass eine Methode nicht versagt, oder in Tests können wir
einführen versuchen! ähnlich wie schnell.

func GetWillNotFail() time.Time {
body := Get() // Kompilierungsfehler, Throwable nicht behandelt
body := try Get() // Kompilierungsfehler, Throwable kann nicht propagiert werden, da GetWillNotFail nicht mit Throws annotiert ist
Körper := versuchen! Get() // funktioniert, gerät aber bei Würfen in Panik != nil

// ...
}

Bin mir aber nicht sicher (ähnlich wie schnell):

func main() {
// 1:
tun {
fmt.Printf("%v", versuchen Sie ScrapeDate())
} catch err { // err enthält gefangene Wurfgegenstände
// ...
}

// 2:
tun {
fmt.Printf("%v", versuchen Sie ScrapeDate())
} catch err.(type) { // ähnlich einer switch-Anweisung
Fallfehler:
// ...
Fall io.EOF
// ...
}
}

ps1. mehrere Rückgabewerte func ReadRune() (ch Rune, size int) throws {
... }
ps2. wir können mit return try Get() oder return try zurückkehren! Werden()
ps3. wir können jetzt Aufrufe wie buffer.NewBuffer(try Get()) oder buffer.NewBuffer(try!
Werden())
ps4. Bei Anmerkungen bin ich mir nicht sicher


Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/21161#issuecomment-325106225 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

Ich denke, der von @jba und @bcmills vorgeschlagene

Betrachtet man dieses Beispiel:

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

Ich denke, doStuff2 ist wesentlich einfacher und schneller zu lesen, weil es:

  1. verschwendet weniger vertikalen Platz
  2. leicht zu lesen ist der glückliche Weg auf der linken Seite
  3. ist einfach die Fehlerbedingungen auf der rechten Seite schnell abzulesen
  4. hat keine err-Variable, die den lokalen Namensraum der Funktion verunreinigt

Für mich allein sieht dieser Vorschlag unvollständig aus und hat zu viel Magie. Wie wäre der Operator ?? definiert? „Erfasst den letzten Rückgabewert, wenn nicht null“? „Erfasst den letzten Fehlerwert, wenn er dem Methodentyp entspricht?“

Das Hinzufügen neuer Operatoren für die Behandlung von Rückgabewerten basierend auf ihrer Position und ihrem Typ sieht wie ein Hack aus.

Am 29. August 2017, 13:03 +0300, schrieb Mikael Gustavsson [email protected] :

Ich denke, der von @jba und @bcmills vorgeschlagene
Betrachtet man dieses Beispiel:
func doStuff() (int,error) {
x, err := f()
wenn err != nil {
return 0, wrapError("f fehlgeschlagen", err)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2() (int,error) {
x := f() ?? (err error) { return 0, wrapError("f failed", err) }
y := g(x) ?? (err error) { return 0, wrapError("g fehlgeschlagen", err) }
kehre zurück, nil
}
Ich denke, doStuff2 ist wesentlich einfacher und schneller zu lesen, weil es:

  1. verschwendet weniger vertikalen Platz
  2. leicht zu lesen ist der glückliche Weg auf der linken Seite
  3. ist einfach die Fehlerbedingungen auf der rechten Seite schnell abzulesen
  4. hat keine err-Variable, die den lokalen Namensraum der Funktion verunreinigt


Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub an oder schalten Sie den Thread stumm.

@nvartolomei

Wie wäre der Operator ?? definiert?

Siehe https://github.com/golang/go/issues/21161#issuecomment -319434101 und https://github.com/golang/go/issues/21161#issuecomment -320758279.

Da @bcmills empfohlen hat, einen ruhenden Thread wiederzubeleben, scheinen Anweisungsmodifikatoren eine vernünftige Lösung für all dies zu bieten, wenn wir in Betracht ziehen, aus anderen Sprachen zu kritisieren. Um das Beispiel von @slvmnd zu nehmen,

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

Nicht ganz so knapp wie die Anweisung und die Fehlerprüfung in einer einzigen Zeile, aber es liest sich einigermaßen gut. (Ich würde vorschlagen, die Zuweisungsform := im if-Ausdruck zu verbieten, sonst würden die Probleme mit dem Bereich wahrscheinlich die Leute verwirren, selbst wenn sie in der Grammatik klar sind) Das Zulassen von "es sei denn" als negierte Version von "if" ist ein bisschen syntaktischer Zucker, aber es funktioniert gut zu lesen und wäre eine Überlegung wert.

Ich würde hier jedoch nicht empfehlen, von Perl zu kritisieren. (Basic Plus 2 ist in Ordnung) Auf diesem Weg liegen Schleifen von Anweisungsmodifikatoren, die, obwohl sie manchmal nützlich sind, eine andere Reihe ziemlich komplexer Probleme mit sich bringen.

eine kürzere Version:
zurück, wenn err != nil
sollte dann auch unterstützt werden.

bei einer solchen Syntax stellt sich die Frage - sollten auch Non-Return-Anweisungen sein?
unterstützt mit solchen "if"-Anweisungen, wie dieser:
func(args) if Bedingung

vielleicht anstatt After-Action zu erfinden – wenn es sich lohnt, Single einzuführen
Linie wenn's?

if err!=nil return
if err!=nil gib 0 zurück, wrapError("failed", err)
if err!=nil do_smth()

es scheint viel natürlicher zu sein als spezielle Formen der Syntax, nicht wahr? Obwohl ich denke
es verursacht viel Mühe beim Parsen :/

Aber... es sind alles nur kleine Optimierungen und keine spezielle Unterstützung für Fehler
Handhabung/Vermehrung.

Am Montag, den 18. September 2017 um 16:14 Uhr schrieb dsugalski [email protected] :

Da @bcmills https://github.com/bcmills die Wiederbelebung empfohlen hat
ruhender Thread, wenn wir in Erwägung ziehen, aus anderen Sprachen zu kritisieren,
es scheint, als ob Anweisungsmodifikatoren für alle eine vernünftige Lösung bieten würden
Dies. Um das Beispiel von @slvmnd https://github.com/slvmnd zu nehmen, wiederholen Sie es mit
Anweisungsmodifikatoren:

func doStuff() (int, err) {
x, err := f()
return 0, wrapError("f failed", err) if err != nil

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

Nicht ganz so knapp wie die Anweisungs- und Fehlerprüfung in einem
Zeile, aber es liest sich einigermaßen gut. (Ich würde vorschlagen, die := Form von . zu verbieten
Zuweisung im if-Ausdruck, andernfalls würden die Scoping-Probleme wahrscheinlich
verwirren die Leute, auch wenn die Grammatik klar ist) Erlaube "es sei denn" als
Die negierte Version von "if" ist ein bisschen syntaktischer Zucker, aber es funktioniert gut zu
gelesen und wäre eine Überlegung wert.

Ich würde hier jedoch nicht empfehlen, von Perl zu kritisieren. (Basic Plus 2 ist
fine) Auf diese Weise liegen Schleifen von Anweisungsmodifikatoren, die zwar manchmal
nützlich, bringen Sie eine weitere Reihe von ziemlich komplexen Themen ein.


Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/21161#issuecomment-330215402 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

Wenn man noch etwas über den Vorschlag von @jba und andere angefordert haben, nämlich dass sich Nicht-Fehlercode sichtbar vom Fehlercode unterscheidet. Könnte immer noch eine interessante Idee sein, wenn es auch erhebliche Vorteile für fehlerfreie Codepfade bietet, aber je mehr ich darüber nachdenke, desto weniger ansprechend erscheint es im Vergleich zu den vorgeschlagenen Alternativen.

Ich bin mir nicht sicher, wie viel visuelle Unterscheidung man von reinem Text erwarten kann. Irgendwann scheint es angebrachter zu sein, dies auf die IDE- oder Codefarbschicht Ihres Texteditors zu legen.

Aber für die textbasierte sichtbare Unterscheidung war der Formatierungsstandard, den wir vor langer Zeit zum ersten Mal hatten, dass IF/UNLESS-Anweisungsmodifikatoren rechtsbündig ausgerichtet werden mussten, was sie gut genug hervorhob. (Allerdings wurde ein Standard gewährt, der auf einem VT-220-Terminal einfacher durchzusetzen und vielleicht visuell besser unterscheidbar war als in Editoren mit flexibleren Fenstergrößen)

Zumindest für mich finde ich, dass der Fall des Anweisungsmodifikators leicht zu unterscheiden ist und sich besser liest als das aktuelle if-Block-Schema. Dies mag bei anderen natürlich nicht der Fall sein -- ich lese den Quellcode genauso wie den englischen Text, damit er in ein vorhandenes komfortables Muster passt, und nicht jeder tut dies.

return 0, wrapError("f failed", err) if err != nil kann geschrieben werden if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) kann genauso geschrieben werden.

Vielleicht ist alles, was hier erforderlich ist, dass gofmt if 's in einer einzigen Zeile in einer einzigen Zeile stehen lässt, anstatt sie auf drei Zeilen zu erweitern?

Da fällt mir noch eine andere Möglichkeit ein. Ein Großteil der Reibung, die ich beim schnellen Schreiben von Wegwerf-Go-Code erlebe, liegt darin, dass ich bei jedem einzelnen Aufruf eine Fehlerprüfung durchführen muss, sodass ich Aufrufe nicht gut verschachteln kann.

Beispielsweise kann ich http.Client.Do nicht für ein neues Anforderungsobjekt aufrufen, ohne zuerst das Ergebnis http.NewRequest einer temporären Variablen zuzuweisen und dann dafür Do aufzurufen.

Ich frage mich, ob wir Folgendes zulassen könnten:

f(y())

funktioniert, auch wenn y (T, error) Tupel zurückgibt. Wenn y einen Fehler zurückgibt, könnte der Compiler die Ausdrucksauswertung abbrechen und veranlassen, dass dieser Fehler von f zurückgegeben wird. Wenn f keinen Fehler zurückgibt, könnte ihm einer gegeben werden.

Dann könnte ich machen:

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

und das Fehlerergebnis wäre ungleich Null, wenn NewRequest oder Do fehlgeschlagen sind.

Dies hat jedoch ein erhebliches Problem - der obige Ausdruck ist bereits gültig, wenn f zwei Argumente oder variadische Argumente akzeptiert. Auch die genauen Regeln dafür sind wahrscheinlich ziemlich kompliziert.

Also im Allgemeinen glaube ich nicht, dass es mir gefällt (ich bin auch von keinem der anderen Vorschläge in diesem Thread begeistert), aber ich dachte, ich würde die Idee trotzdem zur Prüfung wegwerfen.

@rogpeppe oder Sie können einfach json.NewEncoder verwenden

@gbbr Ha ja, schlechtes Beispiel.

Ein besseres Beispiel könnte http.Request sein. Ich habe den Kommentar geändert, um das zu verwenden.

Beeindruckend. Viele Ideen machen die Lesbarkeit des Codes noch schlechter.
Ich bin mit der Herangehensweise einverstanden

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

Nur eine Sache ist wirklich ärgerlich, ist der Bereich der zurückgegebenen Variablen.
In diesem Fall müssen Sie val aber es liegt im Bereich von if .
Sie müssen also else aber Linter wird dagegen sein (und ich auch), und der einzige Weg ist

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

Wäre schön, Zugriff auf Variablen aus dem if Block zu haben:

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

@dmbreaker Dafür ist die meinen früheren Kommentar .

Ich bin ganz dafür, die Fehlerbehandlung in Go zu vereinfachen (obwohl es mir persönlich nicht so viel ausmacht), aber ich denke, dies fügt einer ansonsten einfachen und extrem leicht zu lesenden Sprache ein bisschen Zauberei hinzu.

@gbbr
Was ist das 'dies', auf das Sie sich hier beziehen? Es gibt viele verschiedene Vorschläge, wie man die Dinge angehen kann.

Vielleicht eine zweiteilige Lösung?

Definieren Sie try als "den Wert ganz rechts im Rückgabetupel abziehen; wenn es nicht der Nullwert für seinen Typ ist, geben Sie ihn als den ganz rechten Wert dieser Funktion zurück, während die anderen auf Null gesetzt sind". Dies ist der allgemeine Fall

 a := try ErrorableFunction(b)

und ermöglicht die Verkettung

 a := try ErrorableFunction(try SomeOther(b, c))

(Optional machen Sie es aus Effizienzgründen ungleich null und nicht ungleich null.) Wenn die fehlerhaften Funktionen einen Wert ungleich null/nicht null zurückgeben, "bricht die Funktion mit einem Wert ab". Der Wert ganz rechts der try 'ed-Funktion muss dem ganz rechten Wert der aufrufenden Funktion zuweisbar sein oder es handelt sich um einen Fehler bei der Typüberprüfung während der Kompilierung. (Dies ist also nicht so fest programmiert, dass nur error , obwohl die Community vielleicht davon abraten sollte, anderen "cleveren" Code zu verwenden.)

Erlauben Sie dann, dass try-Returns mit einem defer-ähnlichen Schlüsselwort abgefangen werden, entweder:

catch func(e error) {
    // whatever this function returns will be returned instead
}

oder, vielleicht ausführlicher, aber mehr im Einklang mit der Funktionsweise von Go bereits:

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

Im Fall von catch muss der Parameter der Funktion genau mit dem zurückgegebenen Wert übereinstimmen. Wenn mehrere Funktionen bereitgestellt werden, durchläuft der Wert alle in umgekehrter Reihenfolge. Sie können natürlich einen Wert eingeben, der in eine Funktion des richtigen Typs aufgelöst wird. Im Fall des defer -basierten Beispiels, wenn eine defer set_catch aufruft, erhält die nächste Verzögerungsfunktion dies als ihren Wert von catch() . (Wenn Sie dumm genug sind, ihn dabei auf nil zurückzusetzen, erhalten Sie einen verwirrenden Rückgabewert. Tun Sie das nicht.) Der an set_catch übergebene Wert muss dem zurückgegebenen Typ zuweisbar sein. In beiden Fällen erwarte ich, dass dies wie defer funktioniert, da es sich um eine Anweisung und nicht um eine Deklaration handelt und nur auf Code angewendet wird, nachdem die Anweisung ausgeführt wurde.

Ich neige dazu, die auf Verzögerung basierende Lösung aus Gründen der Einfachheit zu bevorzugen (im Grunde werden dort keine neuen Konzepte eingeführt, es handelt sich eher um eine zweite Art von recover() als um eine neue Sache), aber ich gebe zu, dass es einige Leistungsprobleme geben kann. Ein separates catch-Schlüsselwort könnte mehr Effizienz ermöglichen, da es einfacher ist, vollständig zu überspringen, wenn eine normale Rückgabe auftritt , was meiner Meinung nach fast kostenlos wäre. (Möglicherweise sollten der Quellcode-Dateiname und die Zeilennummer auch von der catch-Funktion zurückgegeben werden? Dies ist zur Kompilierzeit billig und würde einige der Gründe umgehen, warum Leute jetzt einen vollständigen Stack-Trace fordern.)

Beides würde es auch ermöglichen, sich wiederholende Fehlerbehandlungen effektiv an einer Stelle innerhalb einer Funktion zu behandeln und die Fehlerbehandlung leicht als Bibliotheksfunktion anzubieten, was IMHO einer der schlimmsten Aspekte des aktuellen Falls ist, gemäß den obigen Kommentaren von rsc; die mühseligkeit der fehlerbehandlung fördert eher "return err" als eine korrekte behandlung. Ich weiß, dass ich selbst sehr damit zu kämpfen habe.

@thejerf Ein Teil von Ians Punkt mit diesem Vorschlag besteht darin, Möglichkeiten zu erkunden, um Fehlerbausteine ​​zu beheben, ohne Funktionen davon abzuhalten, Kontext hinzuzufügen oder die von ihnen zurückgegebenen Fehler anderweitig zu manipulieren.

Die Aufteilung der Fehlerbehandlung in try und catch scheint dieses Ziel zu erreichen, obwohl ich vermute, dass dies davon abhängt, welche Art von Detailprogrammen normalerweise hinzufügen möchten.

Zumindest würde ich gerne sehen, wie es mit realistischeren Beispielen funktioniert.

Der ganze Sinn meines Vorschlags besteht darin, das Hinzufügen von Kontext oder die Manipulation der Fehler zu ermöglichen, und zwar auf eine Weise, die ich programmatisch für richtiger halte als die meisten Vorschläge hier, bei denen dieser Kontext immer wieder wiederholt wird, was wiederum den Wunsch verhindert, den zusätzlichen Kontext einzufügen .

Um das ursprüngliche Beispiel umzuschreiben,

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

kommt heraus als

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

außer, dass dieses Beispiel für jeden dieser Vorschläge eigentlich zu trivial ist, und ich würde sagen, in diesem Fall würden wir die ursprüngliche Funktion einfach in Ruhe lassen.

Persönlich halte ich die ursprüngliche Chdir-Funktion überhaupt nicht für ein Problem. Ich stimme dies speziell auf den Fall ab, in dem eine Funktion durch langwierige wiederholte Fehlerbehandlung verrauscht wird, nicht für eine Ein-Fehler-Funktion. Ich würde auch sagen, dass, wenn Sie eine Funktion haben, bei der Sie buchstäblich für jeden möglichen Anwendungsfall etwas anderes tun, die richtige Antwort wahrscheinlich darin besteht, weiter zu schreiben, was wir bereits haben. Ich vermute jedoch, dass dies bei den meisten Menschen bei weitem der seltene Fall ist, mit der Begründung, dass, wenn dies der übliche Fall wäre, es von vornherein keine Beschwerde geben würde. Das Geräusch der Fehlerüberprüfung ist nur deshalb von Bedeutung, weil man in einer Funktion immer wieder "meistens das Gleiche" machen möchte.

Ich vermute auch, dass das meiste von dem, was die Leute wollen, erfüllt wird

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

Wenn wir das Problem beseitigen, eine einzelne if-Anweisung gut aussehen zu lassen, weil sie zu klein ist, um sich darum zu kümmern, und das Problem einer Funktion wegräumen, die wirklich etwas Einzigartiges für jede Fehlerrückgabe tut, mit der Begründung, dass A : das ist eigentlich ein ziemlich seltener Fall und B: in diesem Fall ist der Boilerplate-Overhead im Vergleich zur Komplexität des einzigartigen Handhabungscodes eigentlich nicht so bedeutend, vielleicht kann das Problem auf etwas reduziert werden, das eine Lösung hat.

will ich auch unbedingt sehen

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

oder ein Äquivalent möglich sein, wenn das funktioniert.

Und von all den Vorschlägen auf der Seite... sieht das nicht immer noch nach Go aus? Es sieht eher nach Go aus als das aktuelle Go.

Um ehrlich zu sein, habe ich den größten Teil meiner professionellen Ingenieurserfahrung mit PHP gemacht (ich weiß), aber die Hauptattraktion für Go war immer die Lesbarkeit. Obwohl ich einige Aspekte von PHP mag, verachte ich am meisten den "letzten" "abstrakten" "statischen" Unsinn und die Anwendung überkomplizierter Konzepte auf einen Code, der eine Sache macht.

Als ich diesen Vorschlag sah, hatte ich sofort das Gefühl, ein Stück zu betrachten und eine doppelte Einstellung zu machen und wirklich darüber nachzudenken, was dieses Stück Code sagt / tut. Ich glaube nicht, dass dieser Code lesbar ist und nicht wirklich zur Sprache beiträgt. Mein erster Instinkt ist, nach links zu schauen, und ich denke, dies gibt immer nil . Mit dieser Änderung müsste ich jetzt jedoch nach links und rechts schauen, um das Verhalten des Codes zu bestimmen, was mehr Zeit beim Lesen und mehr mentales Modell bedeutet.

Dies bedeutet jedoch nicht, dass es keinen Raum für Verbesserungen bei der Fehlerbehandlung in Go gibt.

Es tut mir leid, dass ich den gesamten Thread (noch) nicht gelesen habe (er ist super lang), aber ich sehe Leute, die alternative Syntax wegwerfen, also möchte ich meine Idee teilen:

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Ich hoffe, ich habe oben nichts übersehen, das dies zunichte macht. Ich verspreche, dass ich eines Tages alle Kommentare durchgehen werde :)

Der Operator müsste meiner Meinung nach nur für den Typ error werden, um das semantische Durcheinander der Typkonvertierung zu vermeiden.

Interessant, @buchanae , aber bringt es uns viel

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Ich sehe, dass es a zu entkommen, während es im aktuellen Zustand auf die Blöcke then und else beschränkt ist.

@ object88 Sie haben Recht, die Änderung ist subtil, ästhetisch und subjektiv. Persönlich möchte ich von Go 2 zu diesem Thema nur eine subtile Änderung der Lesbarkeit.

Persönlich finde ich es lesbarer, weil die Zeile nicht mit if beginnt und das !=nil nicht benötigt. Die Variablen befinden sich am linken Rand, wo sie sich auf den (meisten?) anderen Zeilen befinden.

Toller Punkt zum Umfang von a , das hatte ich nicht bedacht.

In Anbetracht der anderen Möglichkeiten dieser Grammatik scheint dies möglich zu sein.

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

und wahrscheinlich

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

die sind vielleicht, wo es auseinander fällt.

Vielleicht sollte die Rückgabe eines Fehlers Teil jedes Funktionsaufrufs in Go sein, also können Sie sich vorstellen:
```
a := halloWorld(); irren? {
return fmt.Errorf("helloWorld fehlgeschlagen: %s", err)
}

Wie wäre es mit einer echten Ausnahmebehandlung? Ich meine Try, catch, endlich stattdessen wie viele moderne Sprachen?

Nein, es macht Code implizit und unklar (wenn auch wirklich etwas kürzer)

Am Do, 23 Nov 2017 um 07:27, Kamyar Miremadi [email protected]
schrieb:

Wie wäre es mit einer echten Ausnahmebehandlung? Ich meine Versuch, fang, endlich
stattdessen wie viele moderne Sprachen?


Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/21161#issuecomment-346529787 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

Zurück zum WriteToGCS -Beispiel up-thread von @mpvl möchte ich (erneut) vorschlagen, dass das Commit-/Rollback-Muster nicht häufig genug ist, um eine größere Änderung in der Fehlerbehandlung von Go zu rechtfertigen. Es ist nicht schwer, das Muster in einer Funktion zu erfassen ( Playground-Link ):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

Dann können wir das Beispiel schreiben als

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

Ich würde eine einfachere Lösung vorschlagen:

func someFunc() error {
    ^err := someAction()
    ....
}

Für mehrere Mehrfachfunktionsrückgaben:

func someFunc() error {
    result, ^err := someAction()
    ....
}

Und für mehrere Rückgabeargumente:

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^-Zeichen bedeutet Rückgabe, wenn der Parameter nicht null ist.
Grundsätzlich "Fehler im Stapel nach oben verschieben, wenn es passiert"

Irgendwelche Nachteile dieser Methode?

@gladkikhartem
Wie würde man den Fehler ändern, bevor er zurückgegeben wird?

@urandom
Das Einschließen von Fehlern ist eine wichtige Aktion, die meiner Meinung nach explizit durchgeführt werden sollte.
Beim Go-Code geht es um Lesbarkeit, nicht um Magie.
Ich möchte die Fehlerumhüllung klarer halten

Aber gleichzeitig möchte ich den Code loswerden, der nicht viele Informationen enthält und nur den Platz einnimmt.

if err != nil {
    return err
}

Es ist wie ein Go-Klischee - Sie wollen es nicht lesen, Sie möchten es einfach überspringen.

Was ich in dieser Diskussion bisher gesehen habe, ist eine Kombination aus:

  1. Reduzierung der Syntaxausführlichkeit
  2. Verbesserung des Fehlers durch Hinzufügen von Kontext

Dies steht im Einklang mit der ursprünglichen Problembeschreibung von

1. Reduzierung der Syntaxausführlichkeit

Ich mag die Idee von @gladkikhartem , sogar in seiner ursprünglichen Form, die ich hier melde, da sie bearbeitet / erweitert wurde:

 result, ^ := someAction()

Im Rahmen einer Funktion:

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Diese kurze Syntax – oder in der von @gladkikhartem vorgeschlagenen err^ – würde den Syntax-Ausführlichkeitsteil des Problems ansprechen (1).

2. Fehlerkontext

Für den zweiten Teil, der mehr Kontext hinzufügt, könnten wir ihn vorerst sogar komplett vergessen und später vorschlagen, jedem Fehler automatisch einen Stacktrace hinzuzufügen, wenn ein spezieller contextError Typ verwendet wird. Ein solcher neuer nativer Fehlertyp könnte vollständige oder kurze Stacktraces aufweisen (stellen Sie sich ein GO_CONTEXT_ERROR=full ) und mit der error Schnittstelle kompatibel sein, während er die Möglichkeit bietet, zumindest die Funktion und den Dateinamen aus dem obersten Aufrufstapel zu extrahieren Eintrag.

Wenn ein contextError , sollte Go irgendwie den Aufruf-Stacktrace genau an der Stelle anhängen, an der der Fehler erzeugt wird.

Nochmals mit einem Funktionsbeispiel:

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Nur der Typ wurde von error in contextError geändert, was wie folgt definiert werden könnte:

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(beachten Sie, wie sich dieses Stack() https://golang.org/pkg/runtime/debug/#Stack unterscheidet, da wir hoffen würden, hier eine Nicht-Byte-Version des goroutine-Aufrufstapels zu haben)

Die Methode Cause() würde als Ergebnis der Verschachtelung nil oder das vorherige contextError .

Ich bin mir der potenziellen Auswirkungen auf den Speicher durch das Herumtragen von Stapeln wie diesem sehr wohl bewusst, daher habe ich auf die Möglichkeit hingewiesen, einen Standard-Short-Stack zu haben, der nur 1 oder wenige weitere Einträge enthält. Ein Entwickler würde normalerweise volle Stracktraces in Entwicklungs-/Debug-Versionen aktivieren und ansonsten die Vorgabe (kurze Stacktraces) belassen.

Stand der Technik:

Nur Denkanstöße.

@gladkikhartem @gdm85

Ich glaube, Sie haben den Sinn dieses Vorschlags übersehen. Nach Ians ursprünglichem Beitrag:

Es ist bereits leicht (vielleicht zu einfach), den Fehler zu ignorieren (siehe #20803). Viele existierende Vorschläge zur Fehlerbehandlung machen es einfacher, den Fehler unverändert zurückzugeben (zB #16225, #18721, #21146, #21155). Einige machen es einfacher, den Fehler mit zusätzlichen Informationen zurückzugeben.

Fehler unverändert zurückzugeben ist oft falsch und in der Regel zumindest nicht hilfreich. Wir möchten eine sorgfältige Fehlerbehandlung fördern: Nur den Anwendungsfall „Return unverändert“ zu behandeln, würde die Anreize in die falsche Richtung lenken.

@bcmills Wenn Kontext (in Form eines Stack-Trace) hinzugefügt wird, wird der Fehler mit zusätzlichen Informationen zurückgegeben. Würde das Anhängen einer für Menschen lesbaren Nachricht, zB "Fehler beim Einfügen des Datensatzes" als "sorgfältige Fehlerbehandlung" angesehen? Wie kann man entscheiden, an welcher Stelle in der Aufrufliste solche Nachrichten hinzugefügt werden sollen (in jeder Funktion, oben/unten usw.)? Dies sind alles häufige Fragen beim Codieren von Verbesserungen bei der Fehlerbehandlung.

Dem "return unmodified" könnte wie oben erläutert mit "return unmodified with stacktrace" standardmäßig entgegengewirkt werden und (in einem reaktiven Stil) bei Bedarf eine menschenlesbare Nachricht hinzugefügt werden. Ich habe nicht angegeben, wie eine solche menschenlesbare Nachricht hinzugefügt werden könnte, aber man kann sehen, wie das Umhüllen in pkg/errors für einige Ideen funktioniert.

"Fehler unverändert zurückzugeben ist oft falsch": Daher schlage ich einen Upgrade-Pfad für den faulen Anwendungsfall vor, der derselbe Anwendungsfall ist, der derzeit als schädlich bezeichnet wird.

@bcmills
Ich stimme #20803 zu 100% zu, dass Fehler immer behandelt oder explizit ignoriert werden sollten (und ich habe keine Ahnung, warum dies nicht früher getan wurde ...)
Ja, ich habe den Vorschlag nicht angesprochen und muss es auch nicht. Ich interessiere mich für die tatsächlich vorgeschlagene Lösung, nicht für die Absichten dahinter, da die Absichten nicht mit den Ergebnissen übereinstimmen. Und wenn ich sehe || solche || Sachen vorgeschlagen werden - es macht mich wirklich traurig.

Wenn das Einbetten von Informationen wie Fehlercodes und Fehlermeldungen in Fehler einfach und transparent wäre - Sie müssen keine sorgfältige Fehlerbehandlung fördern - werden die Leute dies selbst tun.
Machen Sie zum Beispiel einfach Fehler zu einem Alias. Wir könnten jede Art von Zeug zurückgeben und es ohne Casting außerhalb der Funktion verwenden. Würde das Leben so viel einfacher machen.

Ich finde es toll, dass Go mich daran erinnert, mit Fehlern umzugehen, aber ich hasse es, wenn Design mich zu etwas ermutigt, das fragwürdig ist.

@gdm85
Stack-Trace automatisch zu einem Fehler hinzuzufügen ist eine schreckliche Idee, schauen Sie einfach nach und Java-Stack-Traces.
Wenn Sie Fehler selbst einschließen, ist es viel einfacher, zu navigieren und zu verstehen, was schief läuft. Das ist der springende Punkt beim Einwickeln.

@gladkikhartem Ich bin nicht der Meinung, dass eine Form des "automatischen Wrappings" viel schlechter zu navigieren und zu verstehen wäre, was schief läuft. Ich verstehe auch nicht genau, worauf Sie sich in Java-Stack-Traces beziehen (ich vermute Ausnahmen? ästhetisch hässlich? welches spezifische Problem?), aber um in einer konstruktiven Richtung zu diskutieren: Was könnte eine gute Definition von "sorgfältig behandeltem Fehler" sein?

Ich bitte beide, mein Verständnis der Best Practices von Go zu verbessern (je mehr oder weniger kanonisch sie sein mögen) und weil ich der Meinung bin, dass eine solche Definition der Schlüssel sein könnte, um einen Vorschlag zur Verbesserung der aktuellen Situation zu machen.

@gladkikhartem Ich weiß, dass dieser Vorschlag bereits überall vorhanden ist, aber lasst uns bitte alles tun, um ihn auf die Ziele zu konzentrieren, die ich anfangs festgelegt habe. Wie ich beim Posten dieses Problems sagte, gibt es bereits mehrere verschiedene Vorschläge zur Vereinfachung von if err != nil { return err } , und hier wird die Syntax diskutiert, die nur diesen speziellen Fall verbessert. Danke.

@ianlancetaylor
Entschuldigung, wenn ich die Diskussion aus dem Weg geräumt habe.

Wenn Sie einem Fehler Kontextinformationen hinzufügen möchten, würde ich vorschlagen, diese Syntax zu verwenden:
(und erzwingen Sie, dass die Benutzer nur einen Fehlertyp für eine Funktion verwenden, um den Kontext zu extrahieren)

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

Das Symbol ^ wird verwendet, um Fehlerparameter anzuzeigen sowie die Funktionsdefinition von der Fehlerbehandlung für "someAction() { }" zu unterscheiden
{ } könnte weggelassen werden, wenn der Fehler unverändert zurückgegeben wird

Ich füge einige weitere Ressourcen hinzu, um auf meine eigene Einladung zu antworten, um "sorgfältige Fehlerbehandlung" besser zu definieren:

So mühsam der aktuelle Ansatz auch ist, ich denke, er ist weniger verwirrend als die Alternativen, obwohl eine Zeile if-Anweisungen funktionieren könnte? Könnte sein?

blah, err := doSomething()
if err != nil: return err

...oder auch...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

Jemand hat das vielleicht schon angesprochen, aber es gibt viele, viele Beiträge und ich habe viele davon gelesen, aber nicht alle. Das heißt, ich persönlich denke, es wäre besser, explizit als implizit zu sein.

Ich war ein Fan von eisenbahnorientierter Programmierung, die Idee stammt von Elixirs with Anweisung.
else Block wird ausgeführt, sobald e == nil kurzgeschlossen ist.

Hier ist mein Vorschlag mit Pseudocode voraus:

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

@ardhitama Ist das nicht wie Try catch, außer dass "With" wie "Try" ist und dass "Else" wie "Catch" ist?
Warum nicht eine Ausnahmebehandlung wie Java oder C# implementieren?
Wenn ein Programmierer die Ausnahme in dieser Funktion jetzt nicht behandeln möchte, gibt er sie als Ergebnis dieser Funktion zurück. Trotzdem gibt es keine Möglichkeit, einen Programmierer zu zwingen, eine Ausnahme zu behandeln, wenn er dies nicht möchte, und oft ist es auch nicht nötig, aber was wir hier bekommen, sind viele if err!=nil-Anweisungen, die den Code hässlich machen und nicht lesbar (viel Lärm). Ist dies nicht der Grund, warum die Try-Catch-Endlich-Anweisung überhaupt in einer anderen Programmiersprache erfunden wurde?

Also, ich denke, es ist besser, wenn Go Authors "Versuchen", nicht stur zu sein!! und führen Sie in den nächsten Versionen einfach die Anweisung "Try Catch finally" ein. Danke schön.

@KamyarM
Sie können in go keine Ausnahmebehandlung einführen, da es in Go keine Ausnahmen gibt.
Die Einführung von try{} catch{} in Go ist wie die Einführung von try{} catch{} in C - es ist einfach völlig falsch .

@ianlancetaylor
Wie wäre es damit, die Go-Fehlerbehandlung überhaupt nicht zu ändern, sondern das gofmt- Tool wie dieses für die einzeilige Fehlerbehandlung zu ändern?

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

Es ist abwärtskompatibel und Sie können es auf Ihre aktuellen Projekte anwenden

Ausnahmen sind dekorierte goto-Anweisungen, sie verwandeln Ihren Call-Stack in einen Call-Graph und es gibt einen guten Grund, warum die meisten ernsthaften nicht-akademischen Projekte ihre Verwendung sperren oder einschränken. Ein zustandsbehaftetes Objekt ruft eine Methode auf, die die Kontrolle willkürlich auf dem Stapel nach oben überträgt und dann die Ausführung von Anweisungen wieder aufnimmt ... klingt nach einer schlechten Idee, weil es so ist.

@KamyarM Im Wesentlichen ist es das, aber in der Praxis ist es das nicht. Meiner Meinung nach, weil wir hier explizit sind und keine Go-Idiome brechen.

Wieso den?

  1. Ausdrücke innerhalb der with Anweisung können keine neue Variable deklarieren, daher wird explizit angegeben, dass die Absicht besteht, aus Blockvariablen auszuwerten.
  2. Anweisungen innerhalb von with verhalten sich wie innerhalb des Blocks try und catch . Tatsächlich wird es langsamer sein, da es bei jedem nächsten Befehl die Bedingungen von with im schlimmsten Fall auswerten muss.
  3. Es ist beabsichtigt, überflüssige if s zu entfernen und keinen Ausnahmehandler zu erstellen, da der Handler immer lokal ist (der Ausdruck von with und der else Block).
  4. Kein Abwickeln des Stapels erforderlich, da throw

ps. Bitte korrigieren Sie mich, falls ich falsch liege.

@ardhitama
KamyarM hat insofern Recht, als dass with- Anweisung so hässlich aussieht wie try catch und es auch Einrückungsebenen für den normalen Codefluss einführt.
Ganz zu schweigen von der Idee des ursprünglichen Vorschlags, jeden Fehler einzeln zu ändern. Es wird einfach nicht elegant funktionieren mit try catch , mit oder einer anderen Methode, die Anweisungen zusammenfasst.

@gladkikhartem
Ja, daher schlage ich vor, stattdessen "eisenbahnorientierte Programmierung" zu übernehmen und zu versuchen, die Explizitheit nicht zu entfernen. Es ist nur ein weiterer Ansatzpunkt, um das Problem anzugehen, die anderen Lösungen wollen es lösen, indem sie den Compiler nicht automatisch if err != nil für Sie schreiben lassen.

with auch nicht nur für die Fehlerbehandlung, es kann für jeden anderen Kontrollfluss nützlich sein.

@gladkikhartem
Lassen Sie mich klarstellen, dass ich den Block Try Catch Finally schön finde. If err!=nil ... ist eigentlich der hässliche Code.

Go ist nur eine Programmiersprache. Es gibt so viele andere Sprachen. Ich habe herausgefunden, dass viele in der Go-Community es als ihre Religion betrachten und nicht offen dafür sind, sich zu ändern oder Fehler zuzugeben. Das ist falsch.

@gladkikhartem

Ich bin in Ordnung, wenn Go-Autoren es Go++ oder Go# oder GoJava nennen und dort The Try Catch Finally vorstellen ;)

@KamyarM

Die Vermeidung unnötiger Änderungen ist notwendig – entscheidend – für jedes technische Unterfangen. Wenn die Leute in diesem Zusammenhang Veränderung sagen, meinen sie wirklich _Veränderung zum Besseren_, was sie effektiv mit _Argumenten_ vermitteln, die eine Prämisse zu der beabsichtigten Schlussfolgerung führen.

Der Appell _Öffne einfach deinen Geist, Mann!_ ist nicht überzeugend. Ironischerweise versucht es zu sagen, dass etwas, das die meisten Programmierer als uralt und klobig ansehen, _neu und verbessert_ ist.

Es gibt auch viele Vorschläge und Diskussionen, in denen die Go-Community frühere Fehler diskutiert. Aber ich stimme Ihnen zu, wenn Sie sagen, dass Go nur eine Programmiersprache ist. Es steht so auf der Go-Website und an anderen Stellen, und ich habe mit einigen Leuten gesprochen, die es auch bestätigt haben.

Ich habe herausgefunden, dass viele in der Go-Community es als ihre Religion betrachten und nicht offen dafür sind, sich zu ändern oder Fehler zuzugeben.

Go basiert auf akademischer Forschung; persönliche Meinungen sind egal.

Sogar führende Entwickler von Microsofts C#-Compiler haben öffentlich anerkannt, dass _Exceptions_ eine schlechte Methode sind, um Fehler zu verwalten, während sie das Go/Rust-Modell als bessere Alternative anpreisen: http://joeduffyblog.com/2016/02/07/the-error-model/

Sicherlich gibt es Raum für die Verbesserung des Fehlermodells von Go, aber nicht durch die Annahme ausnahmeähnlicher Lösungen, da sie nur eine riesige Komplexität im Austausch für einige fragwürdige Vorteile hinzufügen.

@Dr-Terrible Vielen Dank für den Artikel.

Aber ich habe nirgendwo gefunden, wo GoLang als akademische Sprache erwähnt wird.

Übrigens, um meinen Standpunkt klar zu machen, in diesem Beispiel

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

    err = Operation2()
    if err!=nil{
        return err
    }

    err = Operation3()
    if err!=nil{
        return err
    }

    err = Operation4()
    return err
}

Ist ähnlich wie die Ausnahmebehandlung in C# wie folgt zu implementieren:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

Ist das nicht eine schreckliche Art der Ausnahmebehandlung in C#? Meine Antwort ist ja, ich kenne deine nicht! In Go habe ich keine andere Wahl. Es ist diese schreckliche Wahl oder Autobahn. So ist es in GO und ich habe keine Wahl.

Übrigens, wie auch in dem von Ihnen geteilten Artikel erwähnt, kann jede Sprache eine Fehlerbehandlung wie Go implementieren, ohne dass eine zusätzliche Syntax erforderlich ist, sodass Go tatsächlich keine revolutionäre Art der Fehlerbehandlung implementiert hat. Es hat einfach keine Möglichkeit zur Fehlerbehandlung und daher sind Sie darauf beschränkt, die If-Anweisung zur Fehlerbehandlung zu verwenden.

Übrigens, ich weiß, dass GO ein nicht empfohlenes Panic, Recover , Defer , das Try Catch Finally ähnlich ist, aber meiner persönlichen Meinung nach ist die Try Catch Finally Syntax viel sauberer und besser organisierter Weg der Ausnahmebehandlung.

@Dr-Schrecklich

Bitte überprüfen Sie auch dies:
https://github.com/manucorporat/try

@KamyarM , er hat nicht gesagt, dass Go eine akademische Sprache ist, er sagte, es basiere auf akademischer Forschung. Der Artikel war auch nicht über Go, aber er untersucht das von Go verwendete Paradigma zur Fehlerbehandlung.

Wenn Sie feststellen, dass manucorporat/try für Sie funktioniert, verwenden Sie es bitte in Ihrem Code. Aber die Kosten (Leistung, Sprachkomplexität usw.) für das Hinzufügen von try/catch zur Sprache selbst sind den Kompromiss nicht wert.

@KamyarM
Dein Beispiel ist nicht richtig. Als Alternative

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

wird sein

            Operation1();
            Operation2();
            Operation3();
            Operation4();

Ausnahmebehandlung scheint in diesem Beispiel eine viel bessere Option zu sein. In der Theorie sollte es gut sein, aber in der Praxis
Sie müssen für jeden Fehler, der in Ihrem Endpunkt aufgetreten ist, mit einer genauen Fehlermeldung antworten.
Die gesamte Anwendung in Go besteht normalerweise aus 50% Fehlerbehandlung.

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Und wenn die Leute ein so mächtiges Werkzeug wie trycatch haben, bin ich mir zu 100% sicher, dass sie es zugunsten einer sorgfältigen Fehlerbehandlung überbeanspruchen werden.

Es ist interessant, dass die akademische Welt erwähnt wird, aber Go ist eine Sammlung von Erkenntnissen aus der Praxis. Wenn das Ziel darin besteht, eine fehlerhafte API zu schreiben, die falsche Fehlermeldungen zurückgibt, ist die Ausnahmebehandlung der richtige Weg.

Ich möchte jedoch keinen invalid HTTP header Fehler, wenn meine Anfrage ein fehlerhaftes JSON request body enthält Ihnen.

Bei einer großen API-Abdeckung ist es unmöglich, genügend Fehlerkontext bereitzustellen, um eine sinnvolle Fehlerbehandlung zu erreichen. Das liegt daran, dass jede gute Anwendung in Go zu 50 % eine Fehlerbehandlung aufweist und in einer Sprache zu 90 %, die eine nicht-lokale Steuerungsübertragung zur Behandlung von Fehlern erfordert.

@gladkikhartem

Der von Ihnen erwähnte alternative Weg ist der richtige Weg, um den Code in C# zu schreiben. Es besteht aus nur 4 Zeilen des Codes und zeigt den glücklichen Ausführungspfad. Es hat nicht diese if err!=nil Geräusche. Wenn eine Ausnahme auftritt, kann die Funktion, die sich um diese Ausnahme kümmert, sie mit Try Catch Finally (Es kann dieselbe Funktion selbst oder der Aufrufer oder der Aufrufer des Aufrufers oder der Aufrufer des Aufrufers des Aufrufers des Aufrufers sein ... oder einfach nur ein Ereignishandler, der alle nicht behandelten Fehler in einer Anwendung verarbeitet. Der Programmierer hat verschiedene Möglichkeiten.)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Sieht einfach aus, aber das ist eine knifflige Sache. Ich denke, Sie könnten einen benutzerdefinierten Fehlertyp zusammenstellen, der den Systemfehler, den Benutzerfehler (ohne den internen Zustand an den Benutzer, der möglicherweise nicht die besten Absichten hat) und den HTTP-Code durchsickert.

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Aber probiere es aus

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

Alle diese enthalten eine Art nicht offensichtlicher Magie, die die Dinge für den Leser nicht vereinfacht. In den beiden ersteren Beispielen wird err zu einer Art Pseudo-Schlüsselwort oder einer spontan auftretenden Variablen. In den letzten beiden Beispielen ist überhaupt nicht klar, was dieser Operator : tun soll – wird automatisch ein Fehler zurückgegeben? Ist das RHS des Operators eine einzelne Anweisung oder ein Block?

FWIW, ich würde eine Wrapper-Funktion schreiben, damit Sie return newPathErr("chdir", dir, syscall.Chdir(dir)) ausführen können und automatisch einen Null-Fehler zurückgeben, wenn der dritte Parameter Null ist. :-)

IMO, der beste Vorschlag, den ich gesehen habe, um die Ziele "Fehlerbehandlung in Go vereinfachen" und "Fehler mit zusätzlichen Kontextinformationen zurückzugeben" zu erreichen, stammt von @mrkaspa in #21732:

a, b, err? := f1()

erweitert darauf:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

und ich kann es damit in Panik versetzen:

a, b, err! := f1()

erweitert darauf:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

Dadurch wird die Abwärtskompatibilität gewahrt und alle Schwachstellen der Fehlerbehandlung in go behoben

Dies behandelt keine Fälle wie bufio-Funktionen, die Werte ungleich Null sowie Fehler zurückgeben, aber ich denke, es ist in Ordnung, eine explizite Fehlerbehandlung durchzuführen, wenn Sie sich für die anderen Rückgabewerte interessieren. Und natürlich müssen die fehlerfreien Rückgabewerte der entsprechende Nullwert für diesen Typ sein.

Der ? -Modifikator reduziert die Eingabe von Boilerplate-Fehlern in Funktionen und die ! modifier wird dasselbe für Stellen tun, an denen assert in anderen Sprachen verwendet würde, wie beispielsweise in einigen Hauptfunktionen.

Diese Lösung hat den Vorteil, dass sie sehr einfach ist und nicht viel zu tun versucht, aber ich denke, sie erfüllt die in dieser Vorschlagserklärung dargelegten Anforderungen.

Für den Fall, dass Sie...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

Wenn in foo etwas schief geht, wird der Fehler auf der Call-Site von bar doppelt mit demselben Text umschlossen, ohne dass eine Bedeutung hinzugefügt wird. Zumindest würde ich dem errors.Wrap Teil des Vorschlags widersprechen.

Aber was ist das erwartete Ergebnis?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

Werden a und b auf Nullwerte neu zugewiesen? Wenn ja, ist das Magie, die wir meiner Meinung nach vermeiden sollten. Führen sie die zuvor zugewiesenen Werte aus? (Ich selbst interessiere mich nicht für benannte Rückgabewerte, aber sie sollten für die Zwecke dieses Vorschlags dennoch berücksichtigt werden.)

@dup2X Ja, lass uns die Sprache entfernen, es sollte eher so sein

@object88 Es ist nur natürlich zu erwarten, dass im Fehlerfall alles andere auf Null gesetzt wird. Das ist einfach zu verstehen und hat keine Magie in sich, so ziemlich schon eine Konvention für Go-Code, erfordert wenig zu merken und hat keine Sonderfälle. Wenn wir zulassen, dass Werte erhalten bleiben, wird es kompliziert. Wenn Sie vergessen, nach Fehlern zu suchen, können die zurückgegebenen Werte versehentlich verwendet werden. In diesem Fall kann alles passieren. Wie das Aufrufen von Methoden für eine teilweise zugewiesene Struktur, anstatt bei Null in Panik zu geraten. Programmierer können sogar erwarten, dass im Fehlerfall bestimmte Werte zurückgegeben werden. Meiner Meinung nach wäre es ein Durcheinander und nichts Gutes wäre daraus gewonnen.

Was das Wrapping angeht, denke ich nicht, dass die Standardnachricht etwas Sinnvolles bietet. Es wäre in Ordnung, Fehler einfach zu verketten. Zum Beispiel, wenn Ausnahmen innere Ausnahmen enthalten. Sehr nützlich zum Debuggen von Fehlern tief in einer Bibliothek.

Mit Respekt, ich stimme nicht zu, @creker. Wir haben Beispiele für dieses Szenario in der Go stdlib von Nicht-Nil-Rückgabewerten selbst im Fall eines Nicht-Nil-Fehlers und sind tatsächlich funktionsfähig, wie beispielsweise mehrere Funktionen in der bufio.Reader Struktur . Wir als Go-Programmierer werden aktiv ermutigt, alle Fehler zu überprüfen / zu behandeln; Es fühlt sich ungeheuerlicher an, Fehler zu ignorieren, als Nicht-Null-Rückgabewerte und einen Fehler zu erhalten. In dem von Ihnen zitierten Fall können Sie, wenn Sie eine Null zurückgeben und den Fehler nicht überprüfen, möglicherweise immer noch mit einem ungültigen Wert arbeiten.

Aber abgesehen davon, lassen Sie uns dies ein wenig genauer untersuchen. Wie wäre die Semantik des Operators ? ? Kann es nur auf Typen angewendet werden, die die error Schnittstelle implementieren? Kann es auf andere Typen oder Rückgabeargumente angewendet werden? Wenn es auf Typen angewendet werden kann, die keinen Fehler implementieren, wird es dann durch einen Wert/Zeiger ungleich Null ausgelöst? Kann der ? Operator auf mehr als einen Rückgabewert angewendet werden oder ist das ein Compilerfehler?

@erwbgy
Wenn Sie nur einen Fehler zurückgeben möchten, ohne dass etwas Nützliches daran angehängt ist - es wäre viel einfacher, den Compiler einfach anzuweisen, alle nicht übergebenen Fehler als "if err != nil return..." zu behandeln, zum Beispiel:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

Und es braucht keinen zusätzlichen Verrückten? Symbol in diesem Fall.

@object88
Ich habe versucht, die meisten der hier gezeigten Vorschläge zum Umschließen von Fehlern in echtem Code anzuwenden, und bin mit einem Hauptproblem konfrontiert - der Code wird zu dicht und unlesbar.
Was es tut, ist nur die Codebreite zugunsten der Codehöhe zu opfern.
Das Umschließen von Fehlern mit üblichen if err != nil ermöglicht es tatsächlich, den Code für eine bessere Lesbarkeit zu verbreiten, daher denke ich, dass wir überhaupt nichts für das Umschließen von Fehlern ändern sollten.

@object88

Falls Sie auf Ihrer Site eine Null zurückgeben und den Fehler nicht überprüfen, können Sie immer noch mit einem ungültigen Wert arbeiten.

Aber das führt zu offensichtlichen und leicht zu erkennenden Fehlern wie Panik bei Null. Wenn Sie im Fehlerfall aussagekräftige Werte zurückgeben müssen, sollten Sie dies explizit tun und genau dokumentieren, welcher Wert in welchem ​​Fall verwendet werden kann. Nur zufällige Dinge zurückzugeben, die zufällig in den Variablen bei einem Fehler waren, ist gefährlich und führt zu subtilen Fehlern. Damit ist wiederum nichts gewonnen.

@gladkikhartem das Problem mit if err != nil ist, dass die eigentliche Logik darin vollständig verloren geht und Sie aktiv danach suchen müssen, wenn Sie verstehen möchten, was der Code auf seinem erfolgreichen Weg tut, und sich nicht um die ganze Fehlerbehandlung kümmert . Es ist, als würde man viel C-Code lesen, wo Sie mehrere Zeilen tatsächlichen Code haben und alles andere nur eine Fehlerprüfung ist. Die Leute greifen sogar auf Makrosen zurück, die all das einschließen und zum Ende der Funktion gehen.

Ich sehe nicht, wie Logik in richtig geschriebenem Code zu dicht werden kann. Es ist logisch. Jede Zeile Ihres Codes enthält tatsächlichen Code, der Ihnen wichtig ist. Das ist es, was Sie wollen. Was Sie nicht möchten, ist, Zeilen und Zeilen von Boilerplate zu durchlaufen. Verwenden Sie Kommentare und teilen Sie Ihren Code in Blöcke auf, wenn das hilft. Aber das klingt eher nach einem Problem mit dem tatsächlichen Code und nicht nach der Sprache.

Dies funktioniert im Playground, wenn Sie es nicht neu formatieren:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

Es scheint mir also, dass der ursprüngliche Vorschlag und viele der anderen oben genannten versuchen, eine klare Abkürzungssyntax für diese 100 Zeichen zu entwickeln und gofmt davon abzuhalten, darauf zu bestehen, Zeilenumbrüche hinzuzufügen und den Block über 3 Zeilen neu zu formatieren.

Stellen wir uns also vor, wir ändern gofmt, um nicht mehr auf mehrzeiligen Blöcken zu bestehen, beginnen mit der obigen Zeile und versuchen, Wege zu finden, sie kürzer und klarer zu machen.

Ich denke nicht, dass der Teil vor dem Semikolon (die Zuweisung) geändert werden sollte, so dass 69 Zeichen übrig bleiben, die wir reduzieren könnten. Davon sind 49 die return-Anweisung, die zurückzugebenden Werte und der Fehlerumbruch, und ich sehe keinen großen Wert darin, die Syntax davon zu ändern (z. B. indem return-Anweisungen optional gemacht werden, was die Benutzer verwirrt).

Es bleibt also übrig, eine Abkürzung für ; if err != nil { _ } wobei der Unterstrich einen Codeblock darstellt. Ich denke , jeder sollte eine Abkürzung explizit enthalten err für Klarheit , auch wenn es die Null Vergleich etwas unsichtbar macht, so dass wir mit den kommenden mit einer Kurzschrift links sind für ; if _ != nil { _ } .

Stellen Sie sich für einen Moment vor, dass wir ein einzelnes Zeichen verwenden. Ich werde § als Platzhalter für das Zeichen auswählen. Die Codezeile wäre dann:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

Ich sehe nicht, wie Sie es viel besser machen könnten, ohne entweder die vorhandene Zuweisungssyntax oder die Rückgabesyntax zu ändern oder unsichtbare Magie zuzulassen. (Es gibt immer noch etwas Magie, da die Tatsache, dass wir Err mit Null vergleichen, nicht ohne weiteres ersichtlich ist.)

Das sind 88 Zeichen, was insgesamt 12 Zeichen in einer 100 Zeichen langen Zeile spart.

Daher meine Frage: Lohnt sich das wirklich?

Bearbeiten: Ich denke, mein Punkt ist, wenn die Leute sich die if err != nil Blöcke von Go ansehen und sagen: "Ich wünschte, wir könnten diesen Mist loswerden", ist 80-90% von dem, worüber sie sprechen, _Zeug, das man von Natur aus hat zu tun, um Fehler zu behandeln_. Der tatsächliche Overhead, der durch die Syntax von Go verursacht wird, ist minimal.

@lpar , Sie folgen größtenteils der gleichen Logik, die ich oben angewendet habe, daher stimme ich Ihrer Argumentation natürlich zu. Aber ich denke, Sie vernachlässigen den visuellen Reiz, den ganzen Fehlerkram richtig zu stellen:

a, b := Frob("one string")  § err { return ... }

ist um einen Faktor lesbarer, der über die bloße Reduzierung der Zeichen hinausgeht.

@lpar Sie können noch mehr Zeichen sparen, wenn Sie ziemlich nutzlose fmt.Errorf entfernen, die Rückkehr zu einer speziellen Syntax ändern und die Aufrufliste für Fehler einführen, damit sie einen tatsächlichen Kontext haben und keine bloßen glorifizierten Zeichenfolgen sind. Das würde dich mit so etwas zurücklassen

a, b, err? := Frob("one string")

Das Problem mit Go-Fehlern war für mich immer ein Mangel an Kontext. Das Zurückgeben und Umschließen von Zeichenfolgen ist überhaupt nicht hilfreich, um festzustellen, wo der Fehler tatsächlich aufgetreten ist. So wurde beispielsweise github.com/pkg/errors für mich zu einem Muss. Bei solchen Fehlern profitiere ich von der Einfachheit der Go-Fehlerbehandlung und den Vorteilen von Ausnahmen, die den Kontext perfekt erfassen und es Ihnen ermöglichen, den genauen Ort des Fehlers zu finden.

Und selbst wenn wir Ihr Beispiel so nehmen, wie es ist, ist die Tatsache, dass die Fehlerbehandlung auf der rechten Seite steht, ein signifikantes Upgrade der Lesbarkeit. Sie müssen nicht mehr mehrere Boilerplate-Zeilen überspringen, um zur tatsächlichen Bedeutung des Codes zu gelangen. Sie können über die Bedeutung der Fehlerbehandlung sagen, was Sie wollen, aber wenn ich Code lese, um ihn zu verstehen, kümmere ich mich nicht um Fehler. Alles was ich brauche ist ein erfolgreicher Weg. Und wenn ich Fehler brauche, suche ich gezielt nach ihnen. Fehler sind naturgemäß Ausnahmefälle und sollten so wenig Platz wie möglich einnehmen.

Ich denke, die Frage, ob fmt.Errorf im Vergleich zu errors.Wrap "nutzlos" ist, ist orthogonal zu diesem Problem, da beide ungefähr gleich ausführlich sind. (In tatsächlichen Anwendungen verwende ich auch nichts, ich verwende etwas anderes, das auch den Fehler und die Codezeile, in der er aufgetreten ist, protokolliert.)

Ich denke, es kommt darauf an, dass einige Leute die Fehlerbehandlung auf der rechten Seite wirklich mögen. Ich bin einfach nicht so überzeugt, selbst wenn ich einen Perl- und Ruby-Hintergrund habe.

@lpar Ich verwende errors.Wrap weil es die Aufrufliste automatisch erfasst - ich benötige all diese Fehlermeldungen nicht wirklich. Ich interessiere mich mehr für den Ort, an dem es passiert ist und vielleicht, welche Argumente an die Funktion übergeben wurden, die den Fehler erzeugt hat. Sie sagen sogar, dass Sie ähnliches tun - Codezeilen protokollieren, um Ihren Fehlermeldungen einen Kontext zu geben. Angesichts der Tatsache, dass wir uns Möglichkeiten vorstellen können, Boilerplate zu reduzieren und Fehlern mehr Kontext zu geben (das ist hier so ziemlich der Vorschlag).

Was die Fehler angeht, die auf der rechten Seite stehen. Für mich geht es nicht nur um den Ort, sondern darum, die kognitive Belastung zu reduzieren, die es braucht, um einen mit Fehlerbehandlung übersäten Code zu lesen. Ich halte nicht das Argument, dass Fehler so wichtig sind, dass Sie möchten, dass sie so viel Platz einnehmen wie sie. Ich würde es eigentlich vorziehen, wenn sie so oft wie möglich weggehen. Sie sind nicht die Hauptgeschichte.

@creker

Dies beschreibt eher einen trivialen Entwicklerfehler als einen Fehler in einem Produktionssystem, der aufgrund einer schlechten Benutzereingabe einen Fehler erzeugt. Wenn Sie nur die Zeilennummer und den Dateipfad benötigen, um den Fehler zu bestimmen, haben Sie wahrscheinlich nur den Code geschrieben und wissen bereits, was falsch ist.

@, da es Ausnahmen ähnlich ist. In den meisten Fällen reichen Aufruflisten und Ausnahmemeldungen aus, um den Ort und die Ursache des Fehlers zu bestimmen. In komplexeren Fällen wissen Sie zumindest, wo der Fehler aufgetreten ist. Ausnahmen gewähren Ihnen diesen Vorteil standardmäßig. Bei Go müssen Sie entweder Fehler verketten, im Grunde den Call-Stack emulieren, oder den eigentlichen Call-Stack einbeziehen.

In richtig geschriebenem Code würden Sie die genaue Ursache meistens anhand der Zeilennummer und des Dateipfads erkennen, da der Fehler zu erwarten wäre. Sie haben tatsächlich einen Code geschrieben, um dies zu verhindern. Wenn etwas Unerwartetes passiert, dann ja, Call-Stack würde Ihnen nicht die Ursache geben, aber es würde den Suchraum erheblich reduzieren.

@wie

Meiner Erfahrung nach werden Benutzereingabefehler fast sofort behandelt. Die wirklich problematischen Produktionsfehler treten tief im Code auf (zB ist ein Dienst ausgefallen, was dazu führt, dass ein anderer Dienst Fehler ausgibt), und es ist sehr nützlich, einen richtigen Stack-Trace zu erhalten. Für das, was es wert ist, sind die Java-Stack-Traces äußerst nützlich beim Debuggen von Produktionsproblemen, nicht die Nachrichten.

@creker
Fehler sind nur Werte , und sie sind Teil der Funktionsein- und -ausgänge. Sie konnten nicht "unerwartet" sein.
Wenn Sie herausfinden möchten, warum die Funktion einen Fehler verursacht hat - verwenden Sie Tests, Protokollierung usw.

@gladkikhartem in der realen Welt ist das nicht so einfach. Ja, Sie erwarten Fehler in dem Sinne, dass die Funktionssignatur einen Fehler als Rückgabewert enthält. Aber was ich erwarte, ist, genau zu wissen, warum es passiert ist und was es verursacht hat, damit Sie tatsächlich wissen, was zu tun ist, um es zu reparieren oder gar nicht zu beheben. Schlechte Benutzereingaben lassen sich normalerweise ganz einfach beheben, indem Sie sich die Fehlermeldung ansehen. Wenn Sie Protokollpuffer verwenden und ein erforderliches Feld nicht gesetzt ist, wird dies erwartet und ist wirklich einfach zu beheben, wenn Sie alles, was Sie auf der Leitung erhalten, richtig validieren.

An dieser Stelle verstehe ich nicht mehr, worüber wir streiten. Stacktrace oder eine Kette von Fehlermeldungen sind bei richtiger Implementierung ziemlich ähnlich. Sie reduzieren den Suchraum und bieten Ihnen nützlichen Kontext, um einen Fehler zu reproduzieren und zu beheben. Wir müssen über Möglichkeiten nachdenken, die Fehlerbehandlung zu vereinfachen und gleichzeitig genügend Kontext bereitzustellen. Ich befürworte in keiner Weise, dass Einfachheit wichtiger ist als der richtige Kontext.

Das ist das Java-Argument – ​​verschieben Sie den gesamten Fehlercode an eine andere Stelle, damit Sie ihn nicht ansehen müssen. Ich denke, es ist fehlgeleitet; nicht nur, weil Javas Mechanismus dafür weitgehend versagt hat, sondern weil mir beim Betrachten des Codes das Verhalten bei einem Fehler genauso wichtig ist wie das Verhalten, wenn alles funktioniert.

Das argumentiert niemand. Lassen Sie uns das, was hier besprochen wird, nicht mit der Ausnahmebehandlung verwechseln, bei der sich die gesamte Fehlerbehandlung an einem Ort befindet. Es "weitgehend gescheitert" zu nennen, ist nur eine Meinung, aber ich glaube nicht, dass Go jemals darauf zurückkommen wird. Die Go-Fehlerbehandlung ist einfach anders und kann verbessert werden.

@creker Ich habe versucht, das gleiche zu sagen und gebeten, zu klären, was als sinnvolle / nützliche Fehlermeldung angesehen wird.

Die Wahrheit ist, ich würde jeden Tag einen Fehlernachrichtentext mit variabler Qualität (der die Voreingenommenheit des Entwicklers hat, der ihn in diesem Moment und mit diesem Wissen schreibt) im Austausch für die Aufrufliste und die Funktionsargumente verschenken. Bei einem Fehlermeldungstext ( fmt.Errorf oder errors.New ) durchsucht man am Ende den Text im Quellcode, während man Aufrufstapel/Backtraces liest (die anscheinend gehasst werden und hoffentlich nicht aus ästhetischen Gründen) entspricht der direkten Suche nach Datei-/Zeilennummer ( errors.Wrap und ähnlich).

Zwei verschiedene Stile, aber der Zweck ist der gleiche: zu versuchen, in Ihrem Kopf zu reproduzieren, was zur Laufzeit unter diesen Bedingungen passiert ist.

Zu diesem Thema bietet die Ausgabe #19991 vielleicht eine gültige Zusammenfassung für einen Ansatz zur zweiten Art der Definition von bedeutsamen Fehlern.

Verschieben Sie den gesamten Fehlercode woanders, damit Sie ihn nicht ansehen müssen

@lpar , wenn Sie auf meinen Punkt zum Verschieben der Fehlerbehandlung nach rechts antworten: Es gibt einen großen Unterschied zwischen Fußnoten/Endnoten (Java) und Randnotizen (mein Vorschlag). Randnotizen erfordern nur einen winzigen Augenwechsel, ohne den Kontext zu verlieren.

@gdm85

am Ende durchsuchen Sie den Text im Quellcode

Genau das, was ich mit Stacktraces und verketteten Fehlermeldungen meinte, sind ähnlich. Beide zeichnen einen Pfad auf, den sie zu dem Fehler genommen haben. Nur im Falle von Nachrichten können Sie völlig nutzlose Nachrichten erhalten, die von überall im Programm stammen können, wenn Sie nicht sorgfältig genug schreiben. Der einzige Vorteil von verketteten Fehlern ist die Möglichkeit, Werte von Variablen aufzuzeichnen. Und selbst das könnte bei Funktionsargumenten oder gar Variablen im Allgemeinen automatisiert werden und würde, zumindest für mich, fast alles abdecken, was ich an Fehlern benötige. Sie wären immer noch Werte, Sie können sie bei Bedarf immer noch ändern. Aber irgendwann würden Sie sie wahrscheinlich protokollieren und den Stack-Trace sehen zu können, ist äußerst nützlich.

Sehen Sie sich an, was Go mit Panik macht. Sie erhalten einen vollständigen Stack-Trace von jeder Goroutine. Ich kann mich nicht erinnern, wie oft es mir geholfen hat, die Ursache des Fehlers zu finden und in kürzester Zeit zu beheben. Es hat mich oft erstaunt, wie einfach es ist. Es fließt perfekt, da die gesamte Sprache sehr vorhersehbar ist, sodass Sie nicht einmal einen Debugger benötigen.

Alles, was mit Java zu tun hat, scheint ein Stigma zu sein, und die Leute bringen oft keine Argumente mit. Es ist schlecht, nur weil. Ich bin kein Java-Fan, aber diese Art von Argumentation hilft niemandem.

Auch hier sind Fehler nicht Sache des Entwicklers, um Fehler zu beheben. Das ist ein Vorteil der Fehlerbehandlung. Die Java-Methode hat Entwicklern gelehrt, dass Fehlerbehandlung genau das ist, aber nicht. Fehler können auf der Anwendungsschicht und darüber hinaus auf einer Flussschicht auftreten. Fehler in Go werden routinemäßig verwendet, um die Wiederherstellungsstrategie eines Systems zu steuern – zur Laufzeit, nicht zur Kompilierzeit.

Dies kann unverständlich sein, wenn Sprachen ihre Flusskontrolle aufgrund eines Fehlers lahmlegen, indem sie den Stapel entwirren und den Speicher für alles verlieren, was sie vor dem Auftreten des Fehlers getan haben. Fehler sind in Go tatsächlich zur Laufzeit nützlich; Ich sehe nicht ein, warum sie Dinge wie Zeilennummern enthalten sollten - der Code kümmert sich kaum darum.

@wie

Auch hier sind Fehler nicht Sache des Entwicklers, um Fehler zu beheben

Das ist völlig und völlig falsch. Fehler sind genau aus diesem Grund. Sie sind nicht darauf beschränkt, aber es ist eine der Hauptanwendungen. Fehler weisen darauf hin, dass etwas mit dem System nicht stimmt und Sie etwas dagegen unternehmen sollten. Bei erwarteten und einfachen Fehlern können Sie versuchen, sie wiederherzustellen, z. B. TCP-Zeitüberschreitung. Für etwas Ernsteres geben Sie es in Protokolle ab und debuggen das Problem später.

Das ist ein Vorteil der Fehlerbehandlung. Die Java-Methode hat Entwicklern gelehrt, dass Fehlerbehandlung genau das ist, aber nicht.

Ich weiß nicht, was Java Ihnen beigebracht hat, aber ich verwende Ausnahmen aus dem gleichen Grund - um die Systemwiederherstellungsstrategie zur Laufzeit zu steuern. Go hat nichts Besonderes in Bezug auf die Fehlerbehandlung.

Dies kann unverständlich sein, wenn Sprachen ihre Flusskontrolle aufgrund eines Fehlers lahmlegen, indem sie den Stack entwirren und den Speicher von allem verlieren, was sie vor dem Auftreten des Fehlers getan haben

Vielleicht für jemanden, nicht für mich.

Fehler sind in Go tatsächlich zur Laufzeit nützlich; Ich sehe nicht ein, warum sie Dinge wie Zeilennummern enthalten sollten - der Code kümmert sich kaum darum.

Wenn Sie Fehler in Ihrem Code beheben möchten, ist dies mit Zeilennummern der richtige Weg. Nicht Java hat uns das gelehrt, C hat genau aus diesem Grund __LINE__ und __FUNCTION__ . Sie möchten Ihre Fehler protokollieren und den genauen Ort aufzeichnen, an dem sie aufgetreten sind. Und wenn etwas schief geht, hat man zumindest einen Ausgangspunkt. Keine zufällige Fehlermeldung, die durch einen nicht behebbaren Fehler verursacht wurde. Wenn Sie diese Art von Informationen nicht benötigen, ignorieren Sie sie. Es tut dir nicht weh. Aber immerhin ist es da und kann bei Bedarf verwendet werden.

Ich verstehe nicht, warum die Leute hier die Konversation immer wieder in Ausnahmen vs. Fehlerwerte verlagern. Niemand hat diesen Vergleich angestellt. Es wurde nur besprochen, dass Stacktraces sehr nützlich sind und viele Kontextinformationen enthalten. Wenn das unverständlich ist, dann leben Sie wahrscheinlich in einem ganz anderen Universum, in dem es keine Nachverfolgung gibt.

Das ist völlig und völlig falsch.

Aber das Produktionssystem, auf das ich mich beziehe, läuft noch, und es verwendet Fehler zur Flusskontrolle, ist in Go geschrieben und ersetzt eine langsamere Implementierung in einer Sprache, die Stack-Traces zur Fehlerweiterleitung verwendet.

Wenn das unverständlich ist, dann leben Sie wahrscheinlich in einem ganz anderen Universum, in dem es keine Nachverfolgung gibt.

Um Call-Stack-Informationen für jede Funktion zu verketten, die einen Fehlertyp zurückgibt, können Sie dies nach eigenem Ermessen tun. Stapelspuren sind langsamer und aus Sicherheitsgründen für die Verwendung außerhalb von Spielzeugprojekten ungeeignet. Es ist ein technisches Foul, sie zu erstklassigen Go-Bürgern zu machen, um lediglich gedankenlose Fehlerfortpflanzungsstrategien zu unterstützen.

Wenn Sie diese Art von Informationen nicht benötigen, ignorieren Sie sie. Es tut dir nicht weh.

Software-Bloat ist der Grund, warum Server in Go neu geschrieben werden. Was Sie nicht sehen, kann dennoch den Durchsatz Ihrer Pipeline beeinträchtigen.

Ich würde Beispiele für tatsächliche Software bevorzugen, die von dieser Funktion profitiert, anstatt eine leicht irrelevante Lektion zum Umgang mit TCP-Timeouts und Log-Dumping.

Stacktraces sind langsamer

Da die Stack-Traces im Fehlerpfad generiert werden, kümmert es niemanden, wie langsam sie sind. Der normale Betrieb der Software wurde bereits unterbrochen.

und aus Sicherheitsgründen nicht für den Einsatz außerhalb von Spielzeugprojekten geeignet

Bisher habe ich noch kein einziges Produktionssystem gesehen, das Stack-Traces aus "Sicherheitsgründen" oder überhaupt deaktiviert hat. Andererseits war es äußerst nützlich, den Weg, den der Code genommen hat, um einen Fehler zu erzeugen, schnell identifizieren zu können. Und dies ist für große Projekte gedacht, bei denen viele verschiedene Teams an der Codebasis arbeiten und niemand das gesamte System vollständig kennt.

Was Sie nicht sehen, kann dennoch den Durchsatz Ihrer Pipeline beeinträchtigen.

Nein, das tut es wirklich nicht. Wie ich bereits sagte, werden Stack-Traces bei Fehlern generiert. Sofern Ihre Software nicht ständig darauf stößt, wird der Durchsatz nicht ein bisschen beeinträchtigt.

Da die Stack-Traces im Fehlerpfad generiert werden, kümmert es niemanden, wie langsam sie sind. Der normale Betrieb der Software wurde bereits unterbrochen.

Unwahr.

  • Im Rahmen des normalen Betriebs können Fehler auftreten.
  • Fehler können behoben werden und das Programm kann fortgesetzt werden, sodass die Leistung weiterhin in Frage gestellt ist.
  • Das Verlangsamen einer Routine zehrt Ressourcen von anderen Routinen, die auf dem glücklichen Weg _arbeiten_.

@ object88 Stellen

@wie

Aber das Produktionssystem, auf das ich mich beziehe, läuft noch, und es verwendet Fehler zur Flusskontrolle, ist in Go geschrieben und ersetzt eine langsamere Implementierung in einer Sprache, die Stack-Traces zur Fehlerweiterleitung verwendet.

Es tut mir leid, aber das ist ein unsinniger Satz, der nichts mit dem zu tun hat, was ich gesagt habe. Werde es nicht beantworten.

Stacktraces sind langsamer

Langsamer, aber wie viel? Ist es wichtig? Ich glaube nicht. Go-Anwendungen sind im Allgemeinen IO-gebunden. CPU-Zyklen hinterher zu jagen ist in diesem Fall albern. Sie haben viel größere Probleme in der Go-Laufzeit, die die CPU auffressen. Es ist kein Argument, nützliche Funktionen wegzuwerfen, die beim Beheben von Fehlern helfen.

aus Sicherheitsgründen nicht für den Einsatz außerhalb von Spielzeugprojekten geeignet.

Ich werde nicht auf nicht vorhandene "Sicherheitsgründe" eingehen. Ich möchte Sie jedoch daran erinnern, dass Anwendungsspuren normalerweise intern gespeichert werden und nur Entwickler darauf zugreifen können. Und der Versuch, die Namen Ihrer Funktionen zu verbergen, ist sowieso Zeitverschwendung. Es ist keine Sicherheit. Ich hoffe, ich muss das nicht weiter ausführen.

Wenn Sie auf Sicherheitsgründen bestehen, möchte ich Sie bitten, zum Beispiel an macOS/iOS zu denken. Sie haben kein Problem damit, Panik- und Crash-Dumps auszulösen, die Stapel aller Threads und Werte aller CPU-Register enthalten. Sehen Sie nicht, dass sie von diesen "Sicherheitsgründen" betroffen sind.

Es ist ein technisches Foul, sie zu erstklassigen Go-Bürgern zu machen, um lediglich gedankenlose Fehlerfortpflanzungsstrategien zu unterstützen.

Könnten Sie etwas subjektiver sein? "Gedankenlose Fehlerfortpflanzungsstrategien" wo hast du das gesehen?

Software-Bloat ist der Grund, warum Server in Go neu geschrieben werden. Was Sie nicht sehen, kann dennoch den Durchsatz Ihrer Pipeline beeinträchtigen.

Nochmal, um wie viel?

Ich würde Beispiele für tatsächliche Software bevorzugen, die von dieser Funktion profitiert, anstatt eine leicht irrelevante Lektion zum Umgang mit TCP-Timeouts und Log-Dumping.

An dieser Stelle spreche ich anscheinend mit niemandem außer einem Programmierer. Tracing profitiert von jeder Software. Es ist eine in allen Sprachen und allen Arten von Software übliche Technik, die beim Beheben von Fehlern hilft. Sie können Wikipedia lesen, wenn Sie mehr darüber erfahren möchten.

Bei so vielen unproduktiven Diskussionen ohne Konsens gibt es keinen eleganten Weg, dieses Problem zu lösen.

@object88
Stack-Traces können langsam sein, wenn Sie alle Goroutinen verfolgen möchten, da Go warten sollte, bis andere Goroutinen die Blockierung aufheben.
Wenn Sie nur goroutine verfolgen, die Sie gerade ausführen, ist es nicht so langsam.

@creker
Das Tracing kommt jeder Software zugute, aber es hängt davon ab, was Sie verfolgen. In den meisten Go-Projekten, an denen ich beteiligt war, war das Tracing von Stacks keine gute Idee, da es um Parallelität geht. Daten bewegen sich hin und her, vieles kommuniziert miteinander und manche Goroutinen sind nur ein paar Zeilen Code. In einem solchen Fall hilft es nicht, einen Stack-Trace zu haben.
Aus diesem Grund verwende ich Fehler, die mit in das Protokoll geschriebenen Kontextinformationen eingeschlossen sind, um denselben Stack-Trace wiederherzustellen, der jedoch nicht an den eigentlichen Goroutine-Stack, sondern an die Anwendungslogik selbst gebunden ist.
Damit ich einfach cat *.log machen konnte | grep "orderID=xxx" und erhalte den Stack-Trace der tatsächlichen Abfolge von Aktionen, die zu einem Fehler geführt haben.
Aufgrund der gleichzeitigen Natur von Go sind kontextreiche Fehler wertvoller als Stacktraces.

@gladkikhartem vielen Dank, dass Sie sich die Zeit genommen haben, ein richtiges Argument zu schreiben. Ich fing an, von diesem Gespräch frustriert zu werden.

Ich verstehe Ihre Argumentation und stimme ihr teilweise zu. Trotzdem muss ich mich mit Stapeln von mindestens 5 Funktionen beschäftigen. Das ist schon groß genug, um zu verstehen, was los ist und wo man anfangen sollte zu suchen. Aber in einer hochgradig gleichzeitigen Anwendung mit vielen sehr kleinen Goroutinen verlieren Stacktraces ihre Vorteile. Dem stimme ich zu.

@creker

Stellen Sie sich echten Produktionscode vor. Wie viele Fehler erwarten Sie? [...] da die meisten Go-Anwendungen IO-gebunden sind, wäre dies kein ernsthaftes Problem.

Gut, dass Sie IO-gebundene Operationen erwähnen. Die Methode io.Reader Read gibt einen fehlerfreien Fehler bei EOF zurück. Das wird also viel auf dem glücklichen Weg passieren.

@urandom

Stack-Traces legen unfreiwillig Informationen offen, die für die Profilerstellung eines Systems wertvoll sind.

  • Benutzernamen
  • Dateisystempfade
  • Typ/Version der Backend-Datenbank
  • Transaktionsablauf
  • Objektstruktur
  • Verschlüsselungsalgorithmen

Ich weiß nicht, ob die durchschnittliche Anwendung den Aufwand für das Sammeln von Stack-Frames in Fehlertypen bemerken würde, aber ich kann Ihnen sagen, dass für leistungskritische Anwendungen viele kleine Go-Funktionen aufgrund des aktuellen Funktionsaufrufs manuell inline eingefügt werden. Die Rückverfolgung wird es noch schlimmer machen.

Ich glaube, das Ziel von Go ist eine einfache und schnelle Software, und die Rückverfolgung wäre ein Schritt zurück. Wir sollten in der Lage sein, kleine Funktionen zu schreiben und Fehler von diesen Funktionen ohne Leistungseinbußen zurückzugeben, die unkonventionelle Fehlertypen und manuelles Inlineing fördern.

@creker

Ich werde es vermeiden, Ihnen Beispiele zu geben, die weitere Dissonanzen verursachen. Es tut mir leid, dass ich Sie frustriert habe.

Ich würde vorschlagen, ein neues Schlüsselwort "returnif" zu verwenden, dessen Name sofort seine Funktion verrät. Außerdem ist es flexibel genug, um in mehr Anwendungsfällen als bei der Fehlerbehandlung eingesetzt werden zu können.

Beispiel 1 (mit benannter Rückgabe):

a, err = etwas (b)
wenn err != nil {
Rückkehr
}

Würde werden:

a, err = etwas (b)
returnif err != nil

Beispiel 2 (keine benannte Rückgabe verwenden):

a, err := etwas(b)
wenn err != nil {
Rückgabe a, ähm
}

Würde werden:

a, err := etwas(b)
returnif err != nil { a, err }

Meinen Sie in Bezug auf Ihr Named-Return-Beispiel...

a, err = something(b)
returnif err != nil

@ambernardino
Warum aktualisieren Sie nicht stattdessen einfach das fmt-Tool und Sie müssen keine Sprachsyntax aktualisieren und neue, nutzlose Schlüsselwörter hinzufügen

a, err := something(b)
if err != nil { return a, err }

oder

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem die Idee ist nicht, jedes Mal, wenn Sie den Fehler verbreiten möchten, das einzugeben, ich würde dies lieber

a, err? := something(b)

@mrkaspa
Die Idee ist, Code lesbarer zu machen. Das Eintippen von Code ist kein Problem, das Lesen schon.

@gladkikhartem rust verwendet diesen Ansatz und ich denke nicht, dass es weniger lesbar ist

@gladkikhartem Ich glaube nicht, dass ? es weniger lesbar macht. Ich würde sagen, die Geräusche werden komplett eliminiert. Das Problem für mich ist, dass es mit dem Rauschen auch die Möglichkeit eliminiert, nützlichen Kontext bereitzustellen. Ich sehe einfach nicht, wo Sie die üblichen Fehlermeldungen oder Wrap-Fehler einfügen könnten. Callstack ist eine naheliegende Lösung, funktioniert aber, wie bereits erwähnt, nicht für jeden.

@mrkaspa
Und ich denke, es macht es weniger lesbar, was kommt als nächstes? Wir versuchen, die beste Lösung zu finden oder nur Meinungen auszutauschen?

@creker
'?' Charakter erhöht die kognitive Belastung des Lesers, weil es nicht so offensichtlich ist, was zurückgegeben wird, und natürlich sollte die Person wissen, was dieses Zeug macht. Und natürlich ? Zeichen wirft Fragen in den Köpfen des Lesers auf.

Wie ich bereits sagte, wenn Sie err != loswerden möchten, könnte der nil-Compiler nicht verwendete Fehlerparameter erkennen und sie selbst weiterleiten.
Und

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

wird lesbarer

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

Die gleiche Magie, nur weniger Dinge zum Tippen und weniger Dinge zum Nachdenken

@gladkikhartem Nun , ich glaube nicht, dass es eine Lösung gibt, bei der die Leser nicht etwas Neues lernen müssen. Das ist die Folge des Sprachwechsels. Wir müssen einen Kompromiss eingehen - entweder leben wir mit einer ausführlichen Syntax, die in primitiven Begriffen genau zeigt, was getan wird, oder wir führen eine neue Syntax ein, die Ausführlichkeit verbergen, etwas Syntaxzucker hinzufügen usw. Es gibt keinen anderen Weg. Alles abzulehnen, was dem Leser etwas zum Lernen hinzufügt, ist kontraproduktiv. Wir könnten genauso gut alle Go2-Probleme schließen und Feierabend machen.

In Ihrem Beispiel werden noch mehr magische Dinge eingeführt und alle Injektionspunkte ausgeblendet, um eine Syntax einzuführen, die es Entwicklern ermöglicht, Fehlerkontext bereitzustellen. Und am wichtigsten ist, dass alle Informationen darüber, welcher Funktionsaufruf einen Fehler auslösen könnte, vollständig ausgeblendet werden. Das riecht immer mehr nach Ausnahmen. Und wenn wir es ernst meinen mit dem erneuten Auslösen von Fehlern, dann sind Stack-Traces ein Muss, denn nur so können Sie den Kontext in diesem Fall beibehalten.

Der ursprüngliche Vorschlag deckt das alles eigentlich schon ziemlich gut ab. Es ist ausführlich genug und bietet Ihnen einen guten Ort, um Fehler einzuschließen und nützlichen Kontext bereitzustellen. Aber eines der Hauptprobleme ist dieses magische „Irrtum“. Ich finde es hässlich, weil es nicht magisch genug und nicht ausführlich genug ist. Es ist irgendwie in der Mitte. Was es besser machen könnte, ist mehr Magie einzuführen.

Was wäre, wenn || einen neuen Fehler erzeugen würde, der den ursprünglichen Fehler automatisch umschließt. So wird das Beispiel so

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

err verschwindet einfach und alle Umbruchvorgänge werden implizit behandelt. Jetzt brauchen wir eine Möglichkeit, auf diese inneren Fehler zuzugreifen. Das Hinzufügen einer anderen Methode wie Inner() error zur error Schnittstelle wird meiner Meinung nach nicht funktionieren. Eine Möglichkeit besteht darin, integrierte Funktionen wie unwrap(error) []error einzuführen. Es gibt einen Ausschnitt aller inneren Fehler in der Reihenfolge zurück, in der sie umschlossen wurden. Auf diese Weise können Sie auf jeden inneren Fehler oder Bereich darüber zugreifen.

Die Implementierung davon ist fragwürdig, da error nur eine Schnittstelle ist und Sie einen Ort benötigen, an dem diese umschlossenen Fehler für jeden benutzerdefinierten error Typ abgelegt werden können.

Für mich erfüllt dies alle Kriterien, ist aber möglicherweise etwas zu magisch. Aber da die Fehlerschnittstelle per Definition ziemlich speziell ist, ist es vielleicht keine schlechte Idee, sie einem erstklassigen Bürger näher zu bringen. Die Fehlerbehandlung ist ausführlich, da es sich nur um einen normalen Go-Code handelt, es gibt nichts Besonderes daran. Das mag auf dem Papier und für auffällige Schlagzeilen gut sein, aber Fehler sind zu speziell, um eine solche Behandlung zu rechtfertigen. Sie brauchen spezielle Gehäuse.

Geht es im ursprünglichen Vorschlag darum, die Anzahl der Fehlerprüfungen oder die Länge jeder einzelnen Prüfung zu reduzieren?

Wenn letzteres der Fall ist, ist es trivial, die Notwendigkeit der Vorschläge mit der Begründung zu widerlegen, dass es nur eine bedingte Aussage und eine Wiederholungsaussage gibt. Manche Leute mögen keine for-Schleifen, sollten wir auch ein implizites Schleifenkonstrukt bereitstellen?

Die bisher vorgeschlagenen Syntaxänderungen haben als interessantes Gedankenexperiment gedient, aber keine davon ist so klar, aussprechbar oder einfach wie das Original. Go ist keine Bash und nichts sollte an Fehlern "magisch" sein.

Immer mehr lese ich diese Vorschläge, immer mehr sehe ich Leute, deren Argumente nichts anderes sind als "es fügt etwas Neues hinzu, also ist es schlecht, unlesbar, lass alles so wie es ist".

@wie der Vorschlag eine Zusammenfassung dessen gibt, was er zu erreichen versucht. Was getan wird, ist ziemlich genau definiert.

so klar, aussprechbar oder einfach wie das Original

Jeder Vorschlag wird eine neue Syntax einführen und für einige Leute neu zu sein klingt genauso wie "unlesbar, kompliziert usw.". Nur weil es neu ist, ist es nicht weniger klar, aussprechbar oder einfach. "||" und "?" Beispiele sind so klar und einfach wie die vorhandene Syntax, wenn Sie wissen, was sie tut. Oder sollten wir uns darüber beschweren, dass "->" und "<-" zu magisch sind und der Leser wissen muss, was sie bedeuten? Ersetzen wir sie durch Methodenaufrufe.

Go ist keine Bash und nichts sollte an Fehlern "magisch" sein.

Das ist völlig unbegründet und gilt für nichts als Argument. Was das mit Bash zu tun hat, ist mir ein Rätsel.

@creker
Ja, ich stimme Ihnen voll und ganz zu, dass das Event mehr Magie eingeführt hat. Mein Beispiel ist nur eine Fortsetzung von ? Operator-Idee, weniger Zeug zu tippen.

Ich stimme zu, dass wir etwas opfern und etwas ändern müssen und natürlich etwas Magie. Es ist nur eine Abwägung der Vor- und Nachteile einer solchen Magie.

Original || Vorschlag ist ziemlich nett und praktisch getestet, aber die Formatierung ist meiner Meinung nach hässlich, ich würde vorschlagen, die Formatierung zu ändern

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PS, was halten Sie von einer solchen Variante der magischen Syntax?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem sehen beide aus Sicht der Lesbarkeit ziemlich gut aus, aber bei letzterem habe ich ein schlechtes Gefühl. Es führt diesen seltsamen Blockbereich ein, bei dem ich mir nicht so sicher bin.

Ich möchte Sie ermutigen, die Syntax nicht isoliert zu betrachten, sondern im Kontext einer Funktion. Diese Methode hat einige verschiedene Fehlerbehandlungsblöcke.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Wie bereinigen die vorgeschlagenen Änderungen diese Funktion?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

Der @bcmills- Vorschlag passt besser.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

Werde weiter daran herumstochern. Vielleicht können wir noch umfassendere Anwendungsbeispiele finden.

@erwbgy , dieser sieht für mich am besten aus, aber ich bin nicht überzeugt, dass die Auszahlung so groß ist.

  • Was sind die fehlerfreien Rückgabewerte? Sind sie immer der Nullwert? Wenn ein zuvor zugewiesener Wert für eine benannte Rückgabe _was_ ist, wird dieser dann überschrieben? Wenn der benannte Wert verwendet wird, um das Ergebnis einer fehlerhaften Funktion zu speichern, wird dieser zurückgegeben?
  • Kann der Operator ? auf einen fehlerfreien Wert angewendet werden? Könnte ich etwas wie (!ok)? oder ok!? tun (was ein wenig seltsam ist, weil Sie Zuweisung und Betrieb bündeln)? Oder ist diese Syntax nur gut für error ?

@rodcorsi , das ReadDir sondern ReadBuildTargetDirectoryForFileInfo oder so etwas albernes langes. Oder vielleicht haben Sie viele Argumente. Die Fehlerbehandlung für preparePath würde ebenfalls weit aus dem Bildschirm geschoben. Auf einem Gerät mit begrenzter horizontaler Bildschirmgröße (oder einem nicht so breiten Ansichtsfenster wie Github) verlieren Sie wahrscheinlich den Teil =? . Wir sind sehr gut im vertikalen Scrollen; nicht so sehr horizontal.

@gladkikhartem , es scheint, als wäre es an ein (nur das letzte?) Argument gebunden, das die error Schnittstelle implementiert. Es sieht sehr nach einer Funktionsdeklaration aus, und das ... fühlt sich einfach komisch an. Gibt es eine Möglichkeit, es an einen Rückgabewert im Stil von ok binden? Insgesamt kaufen Sie nur 1 Zeile.

@object88
Word-Wrapping löst wirklich umfangreiche Codeprobleme. ist es nicht weit verbreitet?

@ object88 bezüglich eines sehr langen Funktionsaufrufs. Befassen wir uns hier mit dem Hauptproblem. Das Problem ist nicht die vom Bildschirm verschobene Fehlerbehandlung. Das Problem ist ein langer Funktionsname und/oder eine große Liste von Argumenten. Das muss behoben werden, bevor Argumente über die Fehlerbehandlung außerhalb des Bildschirms vorgebracht werden können.

Ich habe noch keinen IDE- oder codefreundlichen Texteditor gesehen, der standardmäßig für Zeilenumbruch eingerichtet war. Und ich habe überhaupt keine Möglichkeit gefunden, dies mit Github zu tun, außer das CSS manuell zu hacken, nachdem die Seite geladen wurde.

Und ich denke, dass die Codebreite ein wichtiger Faktor ist – sie spricht für die _Lesbarkeit_, die den Anstoß für diesen Vorschlag gibt. Die Behauptung ist, dass es "zu viel Code" um Fehler gibt. Nicht, dass die Funktionalität nicht vorhanden wäre oder Fehler auf andere Weise implementiert werden müssten, aber dass der Code nicht gut gelesen würde.

@object88
Ja, dieser Code funktioniert für jede Funktion, die als letzten Parameter eine Fehlerschnittstelle zurückgibt.

In Bezug auf das Speichern von Zeilen können Sie einfach nicht mehr Informationen in eine geringere Anzahl von Zeilen einfügen. Code sollte gleichmäßig verteilt sein, nicht zu dicht und kein Platz nach jeder Anweisung.
Ich stimme zu, dass es wie eine Funktionsdeklaration aussieht, aber gleichzeitig dem existierenden if ...; err != nil { Anweisung, damit die Leute nicht zu verwirrt sind.

Die Codebreite ist ein wichtiger Faktor. Was ist, wenn ich einen 80-Zeilen-Editor habe und 80 Codezeilen ein Funktionsaufruf sind und danach ||

Nur der Vollständigkeit halber werfe ich ein Beispiel mit der || Syntax, meinem automagischen Fehlerumbruch und der automatischen Nullstellung von Nicht-Fehler-Rückgabewerten

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Zu Ihrer Frage zu anderen Rückgabewerten. Im Fehlerfall haben sie in allen Fällen den Wert Null. Ich habe bereits behandelt, warum ich das für wichtig halte.

Das Problem ist, dass Ihr Beispiel von vornherein nicht so kompliziert ist. Aber es zeigt immer noch, was dieser Vorschlag, zumindest für mich, bedeutet. Was ich möchte, ist das gebräuchlichste und am häufigsten verwendete Idiom

err := func()
if err != nil {
    return err
}

Wir sind uns alle einig, dass die Art von Code einen großen Teil (wenn nicht sogar den größten) der Fehlerbehandlung im Allgemeinen ausmacht. Es ist also nur logisch, diesen Fall zu lösen. Und wenn Sie etwas mehr mit dem Fehler zu tun haben möchten, wenden Sie etwas Logik an - machen Sie es. Hier sollte die Ausführlichkeit sein, wo es eine tatsächliche Logik gibt, die der Programmierer lesen und verstehen kann. Was wir nicht brauchen, ist, Platz und Zeit mit dem Lesen von gedankenlosen Boilerplates zu verschwenden. Es ist sinnlos, aber immer noch ein wesentlicher Bestandteil des Go-Codes.

Bezüglich früherer Diskussionen über die implizite Rückgabe von Nullwerten. Wenn Sie bei einem Fehler einen aussagekräftigen Wert zurückgeben müssen, können Sie es tun. Auch hier ist die Ausführlichkeit gut und hilft beim Verständnis von Code. Nichts falsch daran, auf syntaktischen Zucker zu verzichten, wenn Sie etwas Komplizierteres tun müssen. Und || ist flexibel genug, um beide Fälle zu lösen. Sie können Nicht-Fehlerwerte weglassen und sie werden implizit auf Null gesetzt. Oder Sie können sie bei Bedarf explizit angeben. Ich erinnere mich, dass es dafür sogar einen separaten Vorschlag gibt, der auch Fälle umfasst, in denen Sie Fehler zurückgeben und alles andere auf Null setzen möchten.

@object88

Die Behauptung ist, dass es "zu viel Code" um Fehler gibt.

Es ist nicht irgendein Code. Das Hauptproblem ist, dass es zu viele bedeutungslose Boilerplates um Fehler und sehr häufige Fälle von Fehlerbehandlung gibt. Ausführlichkeit wichtig, wenn es etwas Wertvolles zu lesen gibt. Es gibt nichts Wertvolles in if err == nil then return err außer dass Sie den Fehler erneut auslösen möchten. Für eine so primitive Logik braucht es zu viel Platz. Und je mehr Sie Logik, Bibliotheksaufrufe, Wrapper usw. haben, die alle sehr gut einen Fehler zurückgeben könnten, desto mehr dominiert diese Boilerplate wichtige Dinge - die eigentliche Logik Ihres Codes. Und diese Logik kann tatsächlich einige wichtige Fehlerbehandlungslogiken enthalten. Aber es geht in dieser sich wiederholenden Natur der meisten Boilerplate um sie herum verloren. Und das kann gelöst werden, und andere moderne Sprachen, mit denen Go konkurriert, versuchen, das zu lösen. Da die Fehlerbehandlung so wichtig ist, handelt es sich nicht nur um einen normalen Code.

@creker
Ich stimme zu, dass, wenn err != nil return err zu viel Boilerplate ist, wir befürchten, dass, wenn wir eine einfache Möglichkeit schaffen, Fehler einfach auf den Stack weiterzuleiten - statistisch gesehen werden Programmierer, insbesondere Junioren, die einfachste Methode verwenden, anstatt es zu tun was in einer bestimmten Situation angemessen ist.
Es ist die gleiche Idee bei der Fehlerbehandlung von Go - es zwingt Sie, eine anständige Sache zu tun.
Daher möchten wir in diesem Vorschlag andere ermutigen, Fehler mit Bedacht zu behandeln und einzuschließen.

Ich würde sagen, dass wir eine einfache Fehlerbehandlung hässlich und langwierig aussehen lassen sollten, aber eine anmutige Fehlerbehandlung mit Wrapping- oder Stack-Traces sieht gut und einfach aus.

@gladkikhartem Ich fand dieses Argument über alte Redakteure immer albern. Wen interessiert das und warum soll die Sprache darunter leiden? Es ist 2018, so ziemlich jeder hat ein großes Display und einen anständigen Editor. Die ganz, ganz kleine Minderheit sollte nicht alle anderen beeinflussen. Es sollte umgekehrt sein - die Minderheit sollte selbst damit fertig werden. Scrollen, Zeilenumbruch, alles.

@gladkikhartem Go hat dieses Problem bereits und ich denke, wir können nichts dagegen tun. Entwickler werden immer faul sein, bis Sie sie mit fehlgeschlagener Kompilierung oder Laufzeitpanik erzwingen, was Go übrigens nicht tut.

Was Go eigentlich tut, ist nichts zu erzwingen. Die Vorstellung, dass Go Sie dazu zwingt, mit Fehlern umzugehen, ist irreführend und war es schon immer. Go-Autoren zwingen Sie in ihren Blog-Posts und Konferenzvorträgen dazu. Die eigentliche Sprache lässt Sie alles tun, was Sie wollen. Und das Hauptproblem hier ist, was Go standardmäßig wählt - standardmäßig wird der Fehler stillschweigend ignoriert. Es gibt sogar einen Vorschlag, das zu ändern. Wenn es bei Go darum ging, Sie zu einer anständigen Sache zu zwingen, sollte es Folgendes tun. Geben Sie entweder zur Kompilierzeit einen Fehler zurück oder geraten Sie zur Laufzeit in Panik, wenn der zurückgegebene Fehler nicht richtig behandelt wird. Soweit ich weiß, macht Rust dies - Fehler treten standardmäßig in Panik auf. Das nenne ich Zwang, das Richtige zu tun.

Was mich eigentlich gezwungen hat, mit Fehlern in Go umzugehen, ist mein Entwicklergewissen, sonst nichts. Aber es ist immer verlockend, nachzugeben. Wenn ich jetzt nicht explizit die Funktionssignatur lese, wird mir niemand sagen, dass sie einen Fehler zurückgibt. Es gibt ein konkretes Beispiel. Ich hatte lange keine Ahnung, dass fmt.Println einen Fehler zurückgibt. Ich habe keine Verwendung für seinen Rückgabewert, ich möchte nur Sachen drucken. Es gibt also keinen Anreiz für mich, mir anzusehen, was es zurückgibt. Das ist das gleiche Problem, das C hat. Fehler sind Werte und Sie können sie so lange ignorieren, wie Sie möchten, bis Ihr Code zur Laufzeit abbricht und Sie nichts davon wissen, weil es bei hilfreicher Panik wie beispielsweise bei unbehandelten Ausnahmen keinen Absturz gibt.

@gladkikhartem von dem, was ich über diesen Vorschlag verstehe, soll er Entwickler nicht dazu ermutigen, Fehler nachdenklich zu

Ich schreibe diesen Vorschlag hauptsächlich, um Leute, die die Go-Fehlerbehandlung vereinfachen möchten, zu ermutigen, über Möglichkeiten nachzudenken, wie sie es einfach machen können, den Kontext um Fehler zu wickeln, und nicht nur den Fehler unverändert zurückzugeben.

@creker
Mein Editor ist 100 Zeichen breit, weil ich Datei-Explorer, Git-Konsole und etc.... habe, in unserem Team schreibt niemand einen Code mit mehr als 100 Zeichen Länge, es ist einfach albern (bis auf wenige Ausnahmen)

Go erzwingt keine Fehlerbehandlung, Linters tun es. (Vielleicht sollten wir dafür einfach einen Linter schreiben?)

Ok, wenn wir keine Lösung finden und jeder den Vorschlag auf seine Weise versteht - warum nicht einige Anforderungen für das, was wir brauchen, angeben? Es wäre viel einfacher, sich zuerst auf die Anforderungen zu einigen und dann eine Lösung zu entwickeln.

Beispielsweise:

  1. Die Syntax des neuen Vorschlags sollte eine return- Anweisung im Text enthalten, sonst ist es für den Leser nicht offensichtlich, was passiert. ( zustimmen nicht zustimmen )
  2. Neuer Vorschlag sollte Funktionen unterstützen, die mehrere Werte zurückgeben (zustimmen / nicht zustimmen)
  3. Neuer Vorschlag sollte weniger Platz beanspruchen ( 1 Zeile, 2 Zeilen, stimme nicht zu)
  4. Neuer Vorschlag sollte mit sehr langen Ausdrücken umgehen können (zustimmen / nicht zustimmen)
  5. Neuer Vorschlag sollte im Fehlerfall mehrere Aussagen zulassen (zustimmen / nicht zustimmen)
  6. .....

@creker , ungefähr 75% meiner Entwicklung erfolgt auf einem 15-Zoll-Laptop in VSCode. Ich maximiere meinen horizontalen Platz, aber es gibt immer noch eine Grenze, besonders wenn ich parallel bearbeite. Ich würde das unter Studenten wetten , es gibt weit mehr Laptops als Desktops.Ich möchte die Zugänglichkeit der Sprache nicht einschränken, da wir davon ausgehen, dass jeder großformatige Monitore hat.

Und leider schränkt Github den Viewport immer noch ein, egal wie groß ein Bildschirm ist.

@gladkikhartem

Anfängerfaulheit ist hier anwendbar, aber die großzügige Verwendung von errors.New in einigen dieser Beispiele zeigt auch ein mangelndes Verständnis der Sprache. Fehler sollten nicht in Rückgabewerten zugewiesen werden, es sei denn, sie sind dynamisch, und wenn diese Fehler in eine vergleichbare Variable mit Paketbereich eingefügt würden, wäre die Syntax auf der Seite kürzer und auch im Produktionscode akzeptabel. Diejenigen, die am meisten unter der Go-Fehlerbehandlung "Boilerplate" leiden, nehmen die meisten Abkürzungen und haben nicht genug Erfahrung, um Fehler richtig zu behandeln.

Es ist nicht offensichtlich, was simplifying error handling ausmacht, aber der Präzedenzfall ist, dass less runes != simple . Ich denke, es gibt der Einfachheit halber ein paar Qualifizierer, die ein Konstrukt quantifizierbar messen können:

  • Die Anzahl der Arten, wie das Konstrukt ausgedrückt wird
  • Die Ähnlichkeit dieses Konstrukts mit anderen Konstrukten und die Kohäsion zwischen diesen Konstrukten
  • Die Anzahl der logischen Operationen, die durch das Konstrukt zusammengefasst werden
  • Die Ähnlichkeit des Konstrukts mit der natürlichen Sprache (dh Abwesenheit von Negation usw.)

Der ursprüngliche Vorschlag erhöht beispielsweise die Anzahl der Möglichkeiten zur Weitergabe von Fehlern von 2 auf 3. Es ähnelt dem logischen ODER, hat jedoch eine andere Semantik. Es fasst eine bedingte Rückgabe geringer Komplexität zusammen (im Vergleich zu copy oder append oder >> ). Die neue Methode ist weniger natürlich als die alte und würde, wenn sie laut ausgesprochen wird, wahrscheinlich abs, err := path(foo) || return err -> if theres an error, it's returning err lauten. In diesem Fall wäre es ein Rätsel, warum es möglich ist, die vertikalen Balken zu verwenden, wenn Sie kann es genauso schreiben, wie es in einem Code-Review laut gesagt wird.

@wie
Stimme voll und ganz zu, dass less runes != simple .
Mit einfach meine ich lesbar und verständlich.
Damit jeder, der go nicht kennt, es lesen und verstehen sollte, was es tut.
Es sollte wie ein Witz sein - Sie müssen es nicht erklären.

Die aktuelle Fehlerbehandlung ist eigentlich verständlich, aber nicht vollständig lesbar, wenn Sie zu viel if err != nil return.

@ object88 das ist in Ordnung. Ich sagte mehr im Allgemeinen, weil dieses Argument ziemlich häufig vorkommt. Stellen wir uns zum Beispiel einen lächerlichen alten Terminalbildschirm vor, der zum Schreiben von Go verwendet werden könnte. Was ist das für ein Argument? Wo ist die Grenze der Lächerlichkeit? Wenn wir es ernst meinen, sollten wir harte Fakten beachten – was ist die beliebteste Bildschirmgröße und Auflösung. Und nur daraus können wir etwas ziehen. Aber Argumente stellen sich normalerweise nur eine Bildschirmgröße vor, die niemand verwendet, aber es gibt eine kleine Möglichkeit, die jemand könnte.

@gladkikhartem nein, Linters zwingen dich nicht, schlagen sie vor. Hier gibt es einen großen Unterschied. Sie sind optional, nicht Teil der Go-Toolchain und machen nur Vorschläge. Erzwingen kann nur zweierlei bedeuten - Kompilierungs- oder Laufzeitfehler. Alles andere ist ein Vorschlag und eine Option zur Auswahl.

Ich stimme zu, wir sollten besser formulieren, was wir wollen, denn der Vorschlag deckt nicht alle Aspekte vollständig ab.

@wie

Die neue Methode ist weniger natürlich als die alte, und wenn sie laut ausgesprochen würde, wäre es wahrscheinlich abs, err := path(foo) || return err -> Wenn ein Fehler auftritt, wird err zurückgegeben. In diesem Fall wäre es ein Rätsel, warum es möglich ist, die vertikalen Balken zu verwenden, wenn Sie es so schreiben können, wie es in einer Codeüberprüfung laut wird.

Die neue Methode ist nur aus einem Grund weniger natürlich - sie ist derzeit kein Teil der Sprache. Es gibt keinen anderen Grund. Stellen Sie sich Go bereits mit dieser Syntax vor - es wäre natürlich, weil Sie damit vertraut sind. Genauso wie Sie mit -> , select , go und anderen Dingen vertraut sind, die in anderen Sprachen nicht vorhanden sind. Warum ist es möglich, vertikale Balken anstelle von Returns zu verwenden? Ich antworte mit einer Frage. Warum gibt es eine Möglichkeit, Slices in einem Aufruf anzuhängen, wenn Sie dasselbe mit Loop tun können? Warum gibt es eine Möglichkeit, Dinge in einem Aufruf von der Reader- zur Writer-Schnittstelle zu kopieren, wenn Sie dasselbe mit loop tun können? etc etc etc Weil Sie möchten, dass Ihr Code kompakter und besser lesbar ist. Sie führen diese Argumente, wenn Go ihnen bereits mit zahlreichen Beispielen widerspricht. Nochmals, lass uns offener sein und nichts abschießen, nur weil es neu und noch nicht in der Sprache ist. Damit werden wir nichts erreichen. Es gibt ein Problem, viele Leute fordern eine Lösung, lasst uns damit umgehen. Go ist keine heilige Idealsprache, die durch alles, was ihr hinzugefügt wird, entweiht wird.

Warum gibt es eine Möglichkeit, Slices in einem Aufruf anzuhängen, wenn Sie dasselbe mit Loop tun können?

Das Schreiben einer if-Anweisungsfehlerprüfung ist trivial, ich würde gerne Ihre Implementierung von append .

Warum gibt es eine Möglichkeit, Dinge in einem Aufruf von der Reader- zur Writer-Schnittstelle zu kopieren, wenn Sie dasselbe mit loop tun können?

Ein Leser und Schreiber abstrahiert die Quellen und Ziele eines Kopiervorgangs, die Pufferungsstrategie und manchmal sogar die Sentinel-Werte in der Schleife. Sie können diese Abstraktion nicht mit einer Schleife und einem Slice ausdrücken.

Sie führen diese Argumente, wenn Go ihnen bereits mit zahlreichen Beispielen widerspricht.

Ich glaube nicht, dass das der Fall ist, zumindest nicht bei diesen Beispielen.

Nochmals, lass uns offener sein und nichts abschießen, nur weil es neu und noch nicht in der Sprache ist.

Da Go eine Kompatibilitätsgarantie hat, sollten Sie neue Funktionen am intensivsten prüfen, da Sie sich für immer damit auseinandersetzen müssen, wenn sie schrecklich sind. Was hier bisher noch niemand gemacht hat, ist ein echter Proof of Concept erstellt und mit einem kleinen Entwicklerteam genutzt.

Wenn Sie sich die Historie einiger Vorschläge (zB Generika) ansehen, werden Sie feststellen, dass die Erkenntnis oft lautet: "Wow, das ist eigentlich keine gute Lösung, lass uns noch nichts ändern". Die Alternative ist eine Sprache voller Vorschläge und keine einfache Möglichkeit, sie rückwirkend zu entfernen.

Was die Sache mit dem breiten vs. dünnen Bildschirm angeht, ist Multitasking eine andere Sache, die man berücksichtigen sollte.

Möglicherweise haben Sie mehrere Fenster nebeneinander, um gelegentlich etwas anderes zu verfolgen, während Sie ein bisschen Code zusammenwerfen, anstatt nur auf den Editor zu starren und den Kontext vollständig in ein anderes Fenster zu wechseln, um eine Funktion nachzuschlagen, vielleicht StackOverflow, und springen Sie ganz zurück zum Editor.

@wie
Stimme voll und ganz zu, dass die meisten vorgeschlagenen Funktionen unpraktisch sind, und ich fange an zu denken, dass || und ? sowas könnte der Fall sein.

@creker
copy() und append() sind keine trivialen Aufgaben zu implementieren

Ich habe Linters auf CI/CD und sie zwingen mich buchstäblich, mit allen Fehlern umzugehen. Sie sind kein Teil der Sprache, aber das ist egal - ich brauche nur Ergebnisse.
(Und übrigens, ich habe eine starke Meinung - wenn jemand keine Linters in Go verwendet - ist er nur ........)

Über die Bildschirmgröße - es ist nicht einmal lustig, im Ernst. Bitte beenden Sie diese irrelevante Diskussion. Ihr Bildschirm könnte so breit sein, wie Sie möchten - es besteht immer die Wahrscheinlichkeit, dass || return &PathError{Err:err} Teile des Codes nicht sichtbar sind. Googeln Sie einfach das Wort "ide" und sehen Sie, welcher Platz für Code verfügbar ist.

Und bitte lesen Sie den Text anderer aufmerksam durch, ich habe nicht gesagt, dass Go Sie dazu zwingt, mit allen Fehlern umzugehen

Es ist die gleiche Idee bei der Fehlerbehandlung von Go - es zwingt Sie, eine anständige Sache zu tun.

@gladkikhartem Go fmt.Println .

Wenn jemand in Go keine Linters verwendet - ist er nur

Vielleicht sein. Aber wenn etwas nicht wirklich erzwungen wird, dann fliegt es nicht. Einige werden es verwenden, andere nicht.

Über die Bildschirmgröße - es ist nicht einmal lustig, im Ernst. Bitte beenden Sie diese irrelevante Diskussion.

Ich bin nicht derjenige, der angefangen hat, Zufallszahlen zu werfen, die die Entscheidungsfindung irgendwie beeinflussen sollten. Ich sage klar, dass ich das Problem verstehe, aber es sollte objektiv sein. Nicht "Ich habe eine 80 Symbole breite IDE, Go sollte das berücksichtigen und alle anderen ignorieren".

Wenn wir über meine Bildschirmgröße sprechen. Visual Studio-Code gibt mir 270 horizontale Symbole. Ich behaupte nicht, dass es normal ist, so viel Platz einzunehmen. Mein Code kann jedoch leicht 120 Zeichen überschreiten, wenn Sie Strukturen mit Kommentaren und besonders lang benannte Feldtypen berücksichtigen. Wenn ich die || Syntax verwenden würde, würde sie im Falle eines 3-5-Argument-Funktionsaufrufs und eines umgebrochenen Fehlers mit benutzerdefinierter Nachricht leicht in 100-120 passen.

Wenn etwas wie || implementiert werden sollte, sollte gofmt Sie wahrscheinlich nicht zwingen, es in eine Zeile zu schreiben. In einigen Fällen kann es sehr gut zu viel Platz beanspruchen.

@erwbgy , dieser sieht für mich am besten aus, aber ich bin nicht überzeugt, dass die Auszahlung so groß ist.

@object88 Die Auszahlung für mich ist, dass es allgemeine Boilerplates für eine einfache Fehlerbehandlung entfernt und nicht versucht, zu viel zu tun. Es macht nur:

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

einfacher:

val, err? := func()

Nichts hindert eine komplexere Fehlerbehandlung auf die derzeitige Art und Weise.

Was sind die fehlerfreien Rückgabewerte? Sind sie immer der Nullwert? Wenn es einen zuvor zugewiesenen Wert für eine benannte Rückgabe gab, wird dieser dann überschrieben? Wenn der benannte Wert verwendet wird, um das Ergebnis einer fehlerhaften Funktion zu speichern, wird dieser zurückgegeben?

Alle anderen Rückgabeparameter sind geeignete Nullwerte. Bei benannten Parametern würde ich erwarten, dass sie alle zuvor zugewiesenen Werte beibehalten, da ihnen garantiert bereits ein Wert zugewiesen wird.

Kann das ? Operator auf einen Nicht-Fehler-Wert angewendet werden? Könnte ich etwas wie (!ok) tun? oder okay!? (was ein bisschen komisch ist, weil du Belegung und Betrieb bündelst)? Oder ist diese Syntax nur gut für Fehler?

Nein, ich denke nicht, dass es sinnvoll ist, diese Syntax für andere als Fehlerwerte zu verwenden.

Ich denke, dass "Muss"-Funktionen aus Verzweiflung nach besser lesbarem Code zunehmen.

sqlx

db.MustExec(schema)

HTML-Vorlage

var t = template.Must(template.New("name").Parse("html"))

Ich schlage den Panik-Operator vor (nicht sicher, ob ich ihn "Operator" nennen soll)

a,  😱 := someFunc(b)

wie, aber vielleicht unmittelbarer als

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 ist wahrscheinlich zu schwer zu tippen, wir könnten so etwas wie !, oder !!, oder . verwenden

a,  !! := someFunc(b)
!! = maybeReturnsError()

Könnte sein !! Panik und ! kehrt zurück

Zeit für meine 2 Cent. Warum können wir nicht einfach debug.PrintStack() Standardbibliothek für Stacktraces verwenden? Die Idee ist, den Stack-Trace nur auf der tiefsten Ebene zu drucken, auf der der Fehler aufgetreten ist.

Warum können wir nicht einfach debug.PrintStack() Standardbibliothek für Stacktraces verwenden?

Fehler können über viele Stapel übertragen werden. Sie können über Kanäle gesendet, in Variablen gespeichert usw. werden. Es ist oft hilfreicher, diese Übergangspunkte zu kennen, als das Fragment zu kennen, in dem der Fehler zuerst generiert wurde.

Außerdem enthält der Stacktrace selbst oft interne (nicht exportierte) Hilfsfunktionen. Das ist praktisch, wenn Sie versuchen, einen unerwarteten Absturz zu beheben, aber nicht hilfreich bei Fehlern, die während des normalen Betriebs auftreten.

Was ist der benutzerfreundlichste Ansatz für komplette Programmierneulinge?

Ich bin von einfacherer Version gefunden. Benötigt nur einen if !err
Nichts besonderes, intuitiv, keine zusätzlichen Satzzeichen, viel kleinerer Code

```gehen
absPath, err := p.preparePath()
Null zurückgeben, Fehler wenn Fehler

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

doSomethingRegardlessOfErr()

// Fehler an einer Stelle behandeln; wenn benötigt; schnappartig ohne einzurücken
wenn irr {
Rückgabe "Fehler ohne Codeverschmutzung", err
}
```

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

Willkommen zurück, gute alte MUMPS-Postbedingungen ;-)

Danke, aber nein danke.

@dmajkic Dies hilft nicht bei "den Fehler mit zusätzlichen Kontextinformationen zurückgeben".

@erwbgy der Titel dieser Ausgabe ist _proposal: Go 2: Fehlerbehandlung vereinfachen mit || err suffix_ mein Kommentar war in diesem Kontext. Entschuldigung, wenn ich in die vorherige Diskussion eingetreten bin.

@cznic Ja . Post-Bedingungen sind kein Go-Way, aber die Vorbedingungen sehen auch verschmutzt aus:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic Hinter dem Vorschlag

@erwbgy Ich habe alle von @ianlancetaylor angegebenen Probleme basieren darauf, neue Schlüsselwörter hinzuzufügen (wie try() ) oder spezielle nicht-alphanumerische Zeichen zu verwenden. Persönlich - ich mag das nicht, da Code, der mit !"#$%& überladen ist, dazu neigt, beleidigend zu wirken, wie Fluchen.

Ich stimme zu und fühle, was die ersten paar Zeilen dieser Ausgabe sagen: zu viel Go-Code geht in die Fehlerbehandlung. Der Vorschlag, den ich gemacht habe, stimmt mit dieser Meinung überein, mit Vorschlägen, die dem, was Go jetzt anfühlt, sehr nahe kommen, ohne dass zusätzliche Schlüsselwörter oder Schlüsselzeichen erforderlich sind.

Wie wäre es mit einem bedingten Aufschub

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

Das würde die Bedeutung von defer erheblich ändern – es wird nur am Ende des Bereichs ausgeführt, nicht etwas, das dazu führt, dass ein Bereich vorzeitig beendet wird.

Wenn sie Try Catch in dieser Sprache vorstellen, werden all diese Probleme auf sehr einfache Weise gelöst.

Sie sollten so etwas einführen. Wenn der Wert von error auf einen anderen Wert als nil gesetzt ist, kann es den aktuellen Workflow unterbrechen und automatisch den catch-Abschnitt auslösen, und dann können der finally-Abschnitt und die aktuellen Bibliotheken ohne Änderung ebenfalls funktionieren. Problem gelöst!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet Das ist besser als nichts, aber wenn sie einfach nur das gleiche Try-Catch-Paradigma verwenden, mit dem jeder vertraut ist, ist es viel besser.

@KamyarM Sie scheinen

Sieht ähnlich aus wie Swift, das auch "Ausnahmen" hat, die nicht ganz wie Ausnahmen funktionieren.

Verschiedene Sprachen haben gezeigt, dass try catch wirklich eine zweitklassige Lösung ist, während ich denke, dass Go das nicht wie mit einer Maybe-Monade und so weiter lösen kann.

@ianlancetaylor Ich

Java-Ausnahmen sind ein Zugunglück, daher muss ich dir hier @KamyarM entschieden widersprechen. Nur weil etwas bekannt ist, heißt das nicht, dass es eine gute Wahl ist.

Was ich meine.

@KamyarM Danke für die Klarstellung. Ausnahmen haben wir ausdrücklich berücksichtigt und abgelehnt. Fehler sind keine Ausnahme; sie passieren aus allen möglichen ganz normalen Gründen. https://blog.golang.org/errors-are-values

Außergewöhnlich oder nicht, aber sie lösen das Problem des Code-Bloat aufgrund von Fehlerbehandlungs-Boilerplate. Das gleiche Problem hat Objective-C lahmgelegt, das ziemlich genau wie Go funktioniert. Fehler sind nur Werte vom Typ NSError, nichts Besonderes an ihnen. Und es hat das gleiche Problem mit vielen ifs und Error Wrapping. Deshalb hat Swift die Dinge geändert. Sie endeten mit einer Mischung aus zweien - es funktioniert wie Ausnahmen, was bedeutet, dass die Ausführung beendet wird und Sie die Ausnahme abfangen sollten. Aber es wickelt den Stapel nicht ab und funktioniert wie eine normale Rückgabe. Das technische Argument gegen die Verwendung von Ausnahmen für den Kontrollfluss trifft also dort nicht zu - diese "Ausnahmen" sind genauso schnell wie die reguläre Rückgabe. Es ist eher ein syntaktischer Zucker. Aber Swift hat damit ein einzigartiges Problem. Viele der Cocoa-APIs sind asynchron (Callbacks und GCD) und mit dieser Art der Fehlerbehandlung einfach nicht kompatibel - Ausnahmen sind ohne etwas wie await nutzlos. Aber so ziemlich der gesamte Go-Code ist synchron und diese "Ausnahmen" könnten tatsächlich funktionieren.

@urandom
Ausnahmen in Java sind nicht schlecht. Das Problem sind schlechte Programmierer, die nicht wissen, wie man es benutzt.

Wenn Ihre Sprache schreckliche Funktionen hat, wird irgendwann jemand diese Funktion verwenden. Wenn Ihre Sprache keine solche Funktion hat, besteht eine Chance von 0%. Es ist einfache Mathematik.

@da ich dir nicht zustimme, dass Try-Catch eine schreckliche Funktion ist. Es ist eine sehr nützliche Funktion und macht unser Leben viel einfacher. Aus diesem Grund kommentieren wir hier. Möglicherweise fügt das Google GoLang-Team eine ähnliche Funktionalität hinzu. Ich persönlich hasse diese if-elses-Fehlerbehandlungscodes in GoLang und ich mag dieses Aufschub-Panik-Wiederherstellen-Konzept nicht so sehr (es ähnelt Try-Catch, aber nicht so organisiert wie bei Try-Catch-Finally-Blöcken). . Es fügt dem Code so viel Rauschen hinzu, dass der Code in vielen Fällen unlesbar wird.

Die Funktionalität zur Behandlung von Fehlern ohne Boilerplate ist bereits in der Sprache vorhanden. Es scheint keine gute Idee zu sein, weitere Funktionen hinzuzufügen, um Anfänger von ausnahmebasierten Sprachen zufrieden zu stellen.

Und wer kommt von C/C++, Objective-C, wo wir genau das gleiche Problem mit Boilerplate haben? Und es ist frustrierend zu sehen, dass eine moderne Sprache wie Go unter genau den gleichen Problemen leidet. Deshalb fühlt sich dieser ganze Hype um Fehler als Werte so falsch und albern an - es wird schon seit Jahren, zig Jahren getan. Es fühlt sich an, als hätte Go nichts aus dieser Erfahrung gelernt. Vor allem mit Blick auf Swift/Rust, der tatsächlich versucht, einen besseren Weg zu finden. Gehen Sie mit bestehenden Lösungen wie Java/C# mit Ausnahmen zufrieden, aber zumindest sind dies viel ältere Sprachen.

@KamyarM Haben Sie schon einmal eisenbahnorientierte Programmierung verwendet? Der BEAM?

Ausnahmen würdest du nicht so loben, wenn du diese nutzen würdest, imho.

@ShalokShalom Nicht viel. Aber ist das nicht nur eine Zustandsmaschine? Im Fehlerfall dies tun und im Erfolgsfall dies? Nun, ich denke, nicht alle Arten von Fehlern sollten wie Ausnahmen behandelt werden. Wenn nur eine Benutzereingabevalidierung erforderlich ist, kann man einfach einen booleschen Wert mit den Details zu den Validierungsfehlern zurückgeben. Ausnahmen sollten sich auf IO- oder Netzwerkzugriffe oder fehlerhafte Funktionseingaben beschränken und für den Fall, dass ein Fehler wirklich kritisch ist und Sie den glücklichen Ausführungspfad um jeden Preis stoppen möchten.

Einer der Gründe, warum manche Leute sagen, dass Try-Catch nicht gut ist, ist seine Leistung. Dies wird wahrscheinlich durch die Verwendung einer Handler-Zuordnungstabelle für jeden Ort verursacht, an dem eine Ausnahme auftreten kann. Ich habe irgendwo gelesen, dass sogar die Ausnahmen schneller sind (Null-Kosten, wenn keine Ausnahmen auftreten, aber viel mehr Kosten verursachen, wenn sie tatsächlich auftreten) im Vergleich zu If Error-Prüfung (Es wird immer geprüft, unabhängig davon, ob ein Fehler vorliegt oder nicht). Abgesehen davon glaube ich nicht, dass es Probleme mit der Try-Catch-Syntax gibt. Es ist nur die Art und Weise, wie es vom Compiler implementiert wird, was es unterscheidet, nicht seine Syntax.

Leute, die von C/C++ kommen, loben Go genau dafür, dass es KEINE Ausnahmen gibt und
für eine weise Wahl, Widerstand gegen diejenigen, die behaupten, sie sei „modern“ und
Gott sei Dank für den lesbaren Workflow (insbesondere nach C++).

Am Di, 17. April 2018 um 03:46 Uhr, Antonenko Artem [email protected]
schrieb:

Und wer kommt von C/C++, Objective-C, wo wir dasselbe haben?
genaues Problem mit Boilerplate? Und es ist frustrierend, eine moderne Sprache zu sehen
wie Go leiden unter genau den gleichen Problemen. Deshalb dieser ganze Hype
um Fehler herum, da Werte sich so falsch und albern anfühlen - es wurde bereits getan
seit Jahren, zig Jahren. Es fühlt sich an, als hätte Go nichts daraus gelernt
Erfahrung. Vor allem mit Blick auf Swift/Rust, der tatsächlich versucht, zu finden
ein besserer Weg. Mit vorhandener Lösung wie Java/C# abgerechnet mit
Ausnahmen, aber zumindest sind das viel ältere Sprachen.


Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/21161#issuecomment-381793840 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

@kirillx Ich habe nie gesagt, dass ich Ausnahmen wie in C++ möchte. Bitte lesen Sie meinen Kommentar noch einmal. Und was hat das mit C zu tun, wo die Fehlerbehandlung noch schrecklicher ist? Sie haben nicht nur Tonnen von Boilerplate, sondern es fehlen auch Verzögerungen und mehrere Rückgabewerte, die Sie dazu zwingen, Werte mit Zeigerargumenten zurückzugeben und goto zu verwenden, um Ihre Bereinigungslogik zu organisieren. Go verwendet das gleiche Fehlerkonzept, löst jedoch einige der Probleme mit Verzögerung und mehreren Rückgabewerten. Aber Boilerplate ist immer noch da. Andere moderne Sprachen wollen auch keine Ausnahmen, wollen sich aber wegen seiner Ausführlichkeit auch nicht mit dem C-Stil zufrieden geben. Deshalb haben wir diesen Vorschlag und so viel Interesse an diesem Problem.

Menschen, die sich für Ausnahmen einsetzen, sollten diesen Artikel lesen: https://ckwop.me.uk/Why-Exceptions-Suck.html

Der Grund, warum Ausnahmen im Java/C++-Stil von Natur aus schlecht sind, hat nichts mit der Leistung bestimmter Implementierungen zu tun. Ausnahmen sind schlecht, weil sie "on error goto" von BASIC sind, wobei die gotos in dem Kontext unsichtbar sind, in dem sie wirksam werden können. Ausnahmen verstecken die Fehlerbehandlung dort, wo Sie sie leicht vergessen können. Javas geprüfte Ausnahmen sollten dieses Problem lösen, aber in der Praxis taten sie das nicht, weil die Leute die Ausnahmen einfach abgefangen und gegessen oder überall Stack-Traces abgelegt haben.

Ich schreibe die meiste Woche Java, und ich möchte ausdrücklich keine Ausnahmen im Java-Stil in Go sehen, egal wie leistungsstark sie sind.

@lpar Sind nicht alle for-Schleifen, while-Schleifen, if elses , switch case , bricht ab und setzt GoTo-Dinge fort. Was bleibt dann von einer Programmiersprache übrig?

Während, for und if/else beinhalten keinen Ausführungsfluss, der unsichtbar an einen anderen Ort springt, ohne dass eine Markierung darauf hinweist, dass dies der Fall ist.

Was ist anders, wenn jemand den Fehler einfach an den vorherigen Anrufer in GoLang weitergibt und dieser Anrufer ihn einfach an den vorherigen Anrufer zurückgibt usw. (außer viel Coderauschen)? Wie viele Codes müssen wir suchen und durchlaufen, um zu sehen, wer den Fehler behandelt? Das gleiche gilt für Try-Catch.

Was kann den Programmierer stoppen? Manchmal muss die Funktion einen Fehler wirklich nicht behandeln. Wir möchten den Fehler nur an die Benutzeroberfläche weitergeben, damit ein Benutzer oder der Systemadministrator ihn beheben oder eine Problemumgehung finden kann.

Wenn eine Funktion eine Ausnahme nicht behandeln möchte, verwendet sie einfach keinen try-catch-Block, damit der vorherige Aufrufer damit umgehen kann. Ich glaube nicht, dass die Syntax ein Problem hat. Es ist auch viel sauberer. Die Leistung und die Art und Weise, wie sie in einer Sprache implementiert wird, ist jedoch unterschiedlich.

Wie Sie unten sehen können, müssen wir 4 Codezeilen hinzufügen, um einen Fehler nicht zu behandeln:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

Wenn Sie Fehler zur Behandlung an den Aufrufer zurückgeben möchten, ist das in Ordnung. Der Punkt ist, dass Sie dies an dem Punkt sehen, an dem der Fehler an Sie zurückgegeben wurde.

Erwägen:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

Wenn Sie sich den Code ansehen, wissen Sie, dass beim Aufrufen der Funktion ein Fehler auftreten kann. Sie wissen, dass, wenn der Fehler auftritt, der Codefluss an Punkt A endet. Sie wissen, dass der einzige andere Codefluss an einer anderen Stelle an Punkt B endet. null oder sonst.

Kontrast zu Java:

x = SomeFunc(100, 4)

Aus dem Code lässt sich nicht erkennen, ob beim Aufruf der Funktion ein Fehler auftreten könnte. Wenn ein Fehler auftritt und als Ausnahme ausgedrückt wird, passiert ein goto , und Sie könnten am Ende des umgebenden Codes landen ... oder wenn die Ausnahme nicht abgefangen wird, könnten Sie enden irgendwo am Ende eines ganz anderen Stücks Ihres Codes. Oder Sie könnten im Code einer anderen Person landen. Da der Standard-Ausnahmehandler ersetzt werden kann, können Sie möglicherweise buchstäblich überall landen, basierend auf etwas, das vom Code einer anderen Person ausgeführt wird.

Darüber hinaus gibt es keinen impliziten Vertrag, dass x gültig ist – es ist üblich, dass Funktionen null zurückgeben, um auf Fehler oder fehlende Werte hinzuweisen.

Bei Java können diese Probleme bei jedem einzelnen Aufruf auftreten -- nicht nur bei schlechtem Code, sondern bei _allem_ Java-Code müssen Sie sich darüber Gedanken machen. Aus diesem Grund haben Java-Entwicklungsumgebungen eine Popup-Hilfe, die Ihnen anzeigt, ob die Funktion, auf die Sie zeigen, eine Ausnahme verursacht oder nicht, und welche Ausnahmen sie verursachen kann. Aus diesem Grund hat Java geprüfte Ausnahmen hinzugefügt, sodass Sie bei häufigen Fehlern zumindest eine Warnung haben mussten, dass der Funktionsaufruf eine Ausnahme auslösen und den Programmfluss umleiten könnte. In der Zwischenzeit sind die zurückgegebenen Nullen und die ungeprüfte Natur von NullPointerException solches Problem, dass sie die Klasse Optional zu Java 8 hinzugefügt haben, um zu versuchen, sie zu verbessern, obwohl die Kosten darin bestehen, dies explizit zu umschließen Rückgabewert für jede einzelne Funktion, die ein Objekt zurückgibt.

Meine Erfahrung ist, dass NullPointerException von einem unerwarteten Nullwert, der mir übergeben wurde, die häufigste Art ist, wie mein Java-Code abstürzt, und ich normalerweise mit einem großen Backtrace enden, der fast völlig nutzlos ist, und Fehlermeldung, die die Ursache nicht angibt, da sie weit weg vom Fehlercode generiert wurde. Bei Go habe ich ehrlich gesagt kein Problem mit Null-Dereferenzierungs-Paniken festgestellt, obwohl ich mit Go viel weniger Erfahrung habe. Das bedeutet für mich, dass Java von Go lernen sollte und nicht umgekehrt.

Ich glaube nicht, dass die Syntax ein Problem hat.

Ich glaube nicht, dass irgendjemand sagt, dass die Syntax das Problem mit Ausnahmen im Java-Stil ist.

@lpar , Warum ist eine Null-Dereferenzierungspanik in Go besser als NullPointerException in Java? Was ist der Unterschied zwischen "Panic" und "Throw"? Was ist der Unterschied in ihrer Semantik?

Paniken sind nur wiederherstellbar und Würfe sind fangbar? Richtig?

Ich habe mich gerade an einen Unterschied erinnert, mit Panic können Sie ein Fehlerobjekt oder ein String-Objekt in Panik versetzen oder eine andere Art von Objekten sein (korrigieren Sie mich, wenn ich falsch liege), aber mit throw können Sie ein Objekt vom Typ Exception oder eine Unterklasse werfen nur ausnahmsweise.

Warum ist Null-Dereferenzierungspanik in Go besser als NullPointerException in Java?

Weil ersteres meiner Erfahrung nach fast nie passiert, während letzteres die ganze Zeit passiert, aus den Gründen, die ich erklärt habe.

@lpar Nun, ich habe in letzter Zeit nicht mit Java programmiert und ich denke, das ist eine neue Sache (in den letzten 5 Jahren), aber C # hat einen sicheren Navigationsoperator, um Nullverweise zum Erstellen von Ausnahmen zu vermeiden, aber was hat Go? Ich bin mir nicht sicher, aber ich denke, es hat nichts, um mit solchen Situationen umzugehen. Wenn Sie also Panik vermeiden möchten, müssen Sie dem Code immer noch diese hässlichen verschachtelten if-not-nil-else-Anweisungen hinzufügen.

Im Allgemeinen müssen Sie die Rückgabewerte nicht überprüfen, um festzustellen, ob sie in Go null sind, solange Sie den Fehlerrückgabewert überprüfen. Also keine hässlichen verschachtelten if-Anweisungen.

Null-Dereferenzierung war ein schlechtes Beispiel. Wenn Sie es nicht finden, funktionieren Go und Java genau gleich - Sie erhalten einen Absturz mit Stacktrace. Wie kann Stack Trace nutzlos sein, weiß ich jetzt nicht. Sie kennen den genauen Ort, an dem es passiert ist. Sowohl in C# als auch in Go ist es für mich normalerweise trivial, diese Art von Absturz zu beheben, da die Null-Dereferenzierung meiner Erfahrung nach auf einen einfachen Programmierfehler zurückzuführen ist. In diesem speziellen Fall gibt es nichts von irgendjemandem zu lernen.

@lpar

Weil ersteres meiner Erfahrung nach fast nie passiert, während letzteres die ganze Zeit passiert, aus den Gründen, die ich erklärt habe.

Das ist zufällig und ich habe in Ihrem Kommentar keinen Grund gesehen, dass Java bei null/null irgendwie schlechter ist als Go. Ich habe im Go-Code zahlreiche Abstürze ohne Dereferenzierung beobachtet. Sie sind genau die gleichen wie die Null-Dereferenzierung in C#/Java. Möglicherweise verwenden Sie in Go mehr Werttypen, was hilft (C# hat sie auch), ändert aber nichts.

Was Ausnahmen angeht, schauen wir uns Swift an. Sie haben ein Schlüsselwort throws für Funktionen, die einen Fehler auslösen könnten. Funktion ohne kann nicht werfen. Implementierungstechnisch funktioniert es wie return - wahrscheinlich ist ein Register für die Rückgabe von Fehlern reserviert und jedes Mal, wenn Sie die Funktion werfen, kehrt sie normal zurück, trägt jedoch einen Fehlerwert mit sich. Damit ist das Problem der unerwarteten Fehler gelöst. Sie wissen genau, welche Funktion ausgelöst werden könnte, Sie wissen genau, wo es passieren könnte. Fehler sind Werte und erfordern kein Abwickeln des Stapels. Sie werden nur zurückgegeben, bis Sie sie fangen.

Oder etwas Ähnliches wie Rust, wo Sie einen speziellen Ergebnistyp haben, der ein Ergebnis und einen Fehler enthält. Fehler können ohne explizite bedingte Anweisungen weitergegeben werden. Plus eine Menge Mustervergleichsgüte, aber das ist wahrscheinlich nicht für Go.

Beide Sprachen nehmen beide Lösungen (C und Java) und kombinieren sie zu etwas Besserem. Fehlerausbreitung durch Ausnahmen + Fehlerwerte und offensichtlicher Codefluss von C + kein hässlicher Boilerplate-Code, der nichts Nützliches tut. Daher halte ich es für ratsam, sich diese speziellen Implementierungen anzusehen und sie nicht vollständig abzulehnen, nur weil sie in gewisser Weise Ausnahmen ähneln. Es gibt einen Grund, warum Ausnahmen in so vielen Sprachen verwendet werden, weil sie eine positive Seite haben. Andernfalls würden Sprachen sie ignorieren. Vor allem nach C++.

Wie kann Stack Trace nutzlos sein, weiß ich jetzt nicht.

Ich sagte "fast völlig nutzlos". Wie in, benötige ich nur eine Zeile mit Informationen, aber es sind Dutzende von Zeilen lang.

Das ist zufällig und ich habe in Ihrem Kommentar keinen Grund gesehen, dass Java bei null/null irgendwie schlechter ist als Go.

Dann hörst du nicht zu. Gehen Sie zurück und lesen Sie den Teil über implizite Verträge.

Fehler können ohne explizite bedingte Anweisungen weitergegeben werden.

Und genau das ist das Problem – Fehler werden verbreitet und Kontrollfluss ändert sich, ohne dass explizit darauf hingewiesen wird, dass dies passieren wird. Anscheinend denken Sie nicht, dass es ein Problem ist, aber andere sind anderer Meinung.

Ob Ausnahmen, wie sie von Rust oder Swift implementiert werden, unter den gleichen Problemen wie Java leiden, weiß ich nicht, ich überlasse das jemandem, der sich mit den betreffenden Sprachen auskennt.

Und genau das ist das Problem – Fehler werden verbreitet und Kontrollfluss ändert sich, ohne dass explizit darauf hingewiesen wird, dass dies passieren wird.

Das klingt für mich wahr. Wenn ich ein Paket entwickle, das ein anderes Paket konsumiert und dieses Paket eine Ausnahme auslöst, muss _I_ sich das jetzt auch bewusst sein, unabhängig davon, ob ich dieses Feature verwenden möchte. Dies ist eine ungewöhnliche Facette unter den vorgeschlagenen Sprachmerkmalen; die meisten sind Dinge, für die ein Programmierer entscheiden kann oder die sie einfach nicht nach eigenem Ermessen verwenden können. Ausnahmen überschreiten aufgrund ihrer Absicht alle möglichen Grenzen, ob erwartet oder nicht.

Ich sagte "fast völlig nutzlos". Wie in, benötige ich nur eine Zeile mit Informationen, aber es sind Dutzende von Zeilen lang.

Und riesige Go-Traces mit Hunderten von Goroutinen sind irgendwie nützlicher? Ich verstehe nicht, wohin du damit gehst. Java und Go sind hier genau gleich. Und gelegentlich finden Sie es nützlich, den vollständigen Stapel zu beobachten, um zu verstehen, wie Ihr Code dort gelandet ist, wo er abgestürzt ist. C#- und Go-Traces haben mir dabei mehrfach geholfen.

Dann hörst du nicht zu. Gehen Sie zurück und lesen Sie den Teil über implizite Verträge.

Ich habe es gelesen, es hat sich nichts geändert. Meiner Erfahrung nach ist das kein Problem. Dafür ist die Dokumentation in beiden Sprachen da (zB net.ParseIP ). Wenn Sie vergessen zu überprüfen, ob Ihr Wert nil/null ist oder nicht, haben Sie in beiden Sprachen genau das gleiche Problem. In den meisten Fällen gibt Go einen Fehler zurück und C# löst eine Ausnahme aus, sodass Sie sich nicht einmal um nil sorgen müssen. Eine gute API gibt Ihnen nicht einfach null zurück, ohne eine Ausnahme oder etwas auszulösen, um zu sagen, was falsch ist. In anderen Fällen prüfen Sie explizit danach. Die häufigsten Arten von Fehlern mit null sind meiner Erfahrung nach, wenn Sie Protokollpuffer haben, bei denen jedes Feld ein Zeiger/Objekt ist, oder wenn Sie eine interne Logik haben, bei der Klassen-/Strukturfelder je nach internem Zustand null sein können und Sie vergessen, vorher darauf zu prüfen Zugriff. Das ist das gängigste Muster für mich und nichts in Go lindert dieses Problem wesentlich. Ich kann zwei Dinge nennen, die ein bisschen helfen - nützliche leere Werte und Werttypen. Aber es geht mehr um die einfache Programmierung, da Sie nicht jede Variable vor der Verwendung erstellen müssen.

Und genau das ist das Problem – Fehler werden verbreitet und Kontrollfluss ändert sich, ohne dass explizit darauf hingewiesen wird, dass dies passieren wird. Anscheinend denken Sie nicht, dass es ein Problem ist, aber andere sind anderer Meinung.

Das ist ein Problem, ich habe nie etwas anderes gesagt, aber die Leute hier sind so auf Java/C#/C++-Ausnahmen fixiert, dass sie alles ignorieren, was ihnen ein wenig ähnelt. Genau warum verlangt Swift, dass Sie Funktionen mit throws markieren, damit Sie genau sehen können, was Sie von einer Funktion erwarten sollten und wo der Kontrollfluss möglicherweise unterbrochen wird und Sie in Rust verwenden ? um einen Fehler mit verschiedenen Hilfsmethoden explizit zu propagieren, um ihm mehr Kontext zu geben. Beide verwenden das gleiche Konzept von Fehlern als Werte, hüllen es jedoch in syntaktischen Zucker ein, um Boilerplate zu reduzieren.

Und riesige Go-Traces mit Hunderten von Goroutinen sind irgendwie nützlicher?

Mit Go gehen Sie mit Fehlern um, indem Sie sie zusammen mit dem Standort an der Stelle protokollieren, an der sie erkannt werden. Es gibt keine Rückverfolgung, es sei denn, Sie möchten eine hinzufügen. Ich musste das nur einmal tun.

Meiner Erfahrung nach ist das kein Problem.

Nun, meine Erfahrungen sind unterschiedlich, und ich denke, die Erfahrungen der meisten Leute sind unterschiedlich, und als Beweis dafür nenne ich die Tatsache, dass Java 8 optionale Typen hinzugefügt hat.

In diesem Thread hier wurden viele Stärken und Schwächen von Go und seinem Fehlerbehandlungssystem diskutiert, einschließlich einer Diskussion über Ausnahmen oder nicht. Ich empfehle dringend, ihn zu lesen:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

Meine 2 Cent für die Fehlerbehandlung (sorry, wenn eine solche Idee oben erwähnt wurde).

Wir möchten Fehler in den meisten Fällen erneut ausgeben. Dies führt zu solchen Ausschnitten:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Lassen Sie uns einen Nicht-Null-Fehler automatisch erneut ausgeben, wenn er keiner Variablen zugewiesen wurde (ohne zusätzliche Syntax). Der obige Code wird zu:

a := fn()
use(a)

// or just

use(fn())

Der Compiler speichert err in einer impliziten (unsichtbaren) Variable, überprüft sie auf nil und fährt fort (wenn err == nil) oder gibt sie zurück (wenn err != nil) und gibt am Ende nil zurück der Funktion, wenn bei der Ausführung der Funktion wie üblich keine Fehler aufgetreten sind, sondern automatisch und implizit.

Wenn err muss, muss es einer expliziten Variablen zugewiesen und verwendet werden:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

Fehler können so unterdrückt werden:

a, _ := fn()
use(a)

In seltenen (fantastischen) Fällen mit mehr als einem zurückgegebenen Fehler ist eine explizite Fehlerbehandlung obligatorisch (wie jetzt):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

Das ist auch mein Argument - wir wollen Fehler in den meisten Fällen erneut auslösen, das ist normalerweise der Standardfall. Und vielleicht etwas Kontext geben. Mit Ausnahmen wird der Kontext automatisch durch Stacktraces hinzugefügt. Bei Fehlern wie in Go machen wir es von Hand, indem wir eine Fehlermeldung hinzufügen. Warum nicht einfacher machen. Und genau das versuchen andere Sprachen, während sie es mit dem Thema Klarheit in Einklang bringen.

Ich stimme also "Lassen Sie uns einen Nicht-Null-Fehler automatisch neu werfen, wenn er keiner Variablen zugewiesen wurde (ohne zusätzliche Syntax)", aber der letztere Teil nervt mich. Hier liegt die Wurzel des Problems mit Ausnahmen und warum, denke ich, sind die Leute so dagegen, über alles zu sprechen, was auch nur geringfügig mit ihnen zu tun hat. Sie ändern den Kontrollfluss ohne zusätzliche Syntax. Das ist schlecht.

Wenn Sie sich beispielsweise Swift ansehen, wird dieser Code nicht kompiliert

func a() throws {}
func b() throws {
  a()
}

a kann einen Fehler auslösen, also müssen Sie try a() schreiben, um einen Fehler sogar zu propagieren. Wenn Sie throws aus b entfernen, wird es nicht einmal mit try a() kompiliert. Sie müssen den Fehler in b . Dies ist eine viel bessere Methode zur Behandlung von Fehlern, die sowohl das Problem des unklaren Kontrollflusses von Ausnahmen als auch die Ausführlichkeit von Objective-C-Fehlern löst. Letzteres ist ziemlich genau wie Fehler in Go und was Swift ersetzen soll. Was ich nicht mag, ist try, catch Zeug, das auch Swift verwendet. Ich würde es vorziehen, Fehler als Teil eines Rückgabewerts zu hinterlassen.

Was ich also vorschlagen würde, ist, tatsächlich die zusätzliche Syntax zu haben. Damit die Anrufsite von selbst erkennt, dass es sich um eine potenzielle Stelle handelt, an der sich der Kontrollfluss ändern könnte. Was ich auch vorschlagen würde, ist, dass das Nichtschreiben dieser zusätzlichen Syntax zu einem Kompilierungsfehler führen würde. Das würde Sie im Gegensatz zu Go jetzt zwingen, den Fehler zu behandeln. Sie könnten die Möglichkeit hinzufügen, den Fehler mit etwas wie _ einfach stumm zu schalten, da es in einigen Fällen sehr frustrierend wäre, jeden kleinen Fehler zu behandeln. Zum Beispiel printf . Es ist mir egal, wenn es fehlschlägt, etwas zu protokollieren. Go hat schon diese lästigen Importe. Aber das ist zumindest mit Werkzeugen gelöst.

Es gibt zwei Alternativen, um Zeitfehler zu kompilieren, die mir gerade einfallen. Lassen Sie den Fehler wie bei Go now stillschweigend ignorieren. Das mag ich nicht und das war immer mein Problem bei der Go-Fehlerbehandlung. Es erzwingt nichts, das Standardverhalten besteht darin, den Fehler stillschweigend zu ignorieren. Das ist schlecht, so schreibt man keine robusten und einfach zu debuggenden Programme. Ich hatte zu viele Fälle in Objective-C, in denen ich faul war oder keine Zeit hatte und den Fehler ignorierte, nur um von einem Fehler im selben Code getroffen zu werden, aber ohne Diagnoseinformationen darüber, warum es passiert ist. Zumindest würde es mir ermöglichen, das Problem in vielen Fällen direkt dort zu lösen, wenn ich es protokolliere.

Der Nachteil ist, dass Leute anfangen, Fehler zu ignorieren, sozusagen überall try, catch(...) . Das ist eine Möglichkeit, aber gleichzeitig ist es noch einfacher, da Fehler standardmäßig ignoriert werden. Ich denke, Argumente über Ausnahmen gelten hier nicht. Mit Ausnahmen versuchen einige Leute die Illusion zu erreichen, dass ihr Programm stabiler ist. Die Tatsache, dass eine unbehandelte Ausnahme das Programm zum Absturz bringt, ist hier das Problem.

Eine andere Alternative wäre Panik. Aber das ist einfach frustrierend und erinnert an Ausnahmen. Das würde definitiv dazu führen, dass Leute "defensiv" codieren, damit ihr Programm nicht abstürzt. Für mich sollte moderne Sprache zur Kompilierzeit so viel wie möglich erledigen und so wenig Entscheidungen wie möglich der Laufzeit überlassen. Wo Panik angebracht sein könnte, steht ganz oben auf der Anrufliste. Beispielsweise würde die Nichtbehandlung eines Fehlers in der Hauptfunktion automatisch eine Panik auslösen. Gilt das auch für goroutines? Sollte wohl nicht.

Warum Kompromisse eingehen?

@nick-korsakov der ursprüngliche Vorschlag (dieses Problem) möchte Fehlern mehr Kontext hinzufügen:

Es ist bereits leicht (vielleicht zu einfach), den Fehler zu ignorieren (siehe #20803). Viele existierende Vorschläge zur Fehlerbehandlung machen es einfacher, den Fehler unverändert zurückzugeben (zB #16225, #18721, #21146, #21155). Einige machen es einfacher, den Fehler mit zusätzlichen Informationen zurückzugeben.

Siehe auch diesen Kommentar .

In diesem Kommentar schlage ich vor, dass wir, um in dieser Diskussion Fortschritte zu machen (anstatt in Schleifen zu laufen), die Ziele besser definieren sollten, zB was eine sorgfältig behandelte Fehlermeldung ist. Das Ganze ist ziemlich interessant zu lesen, aber es scheint von einem Drei-Sekunden-Goldfisch-Speicherproblem betroffen zu sein (nicht viel fokussiert / vorwärts, nette kreative Syntaxänderungen und Argumente über Ausnahmen / Panik usw.).

Noch ein Fahrradschuppen:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try bedeutet "Zurückgeben, wenn nicht der leere Wert". Dabei gehe ich davon aus, dass errors.Errorf nil zurückgibt, wenn err null ist. Ich denke, das sind ungefähr so ​​viele Einsparungen, wie wir erwarten können, während wir am Ziel einer einfachen Verpackung festhalten.

Die Scannertypen in der Standardbibliothek speichern den Fehlerzustand in einer Struktur, deren Methoden verantwortungsbewusst auf das Vorhandensein eines Fehlers prüfen können, bevor sie fortfahren.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

Durch die Verwendung von Typen zum Speichern des Fehlerzustands ist es möglich, den Code, der einen solchen Typ verwendet, frei von redundanten Fehlerprüfungen zu halten.

Es erfordert auch keine kreativen und bizarren Syntaxänderungen oder unerwarteten Kontrolltransfers in der Sprache.

Ich muss auch etwas wie try/catch vorschlagen, wobei err in try{} definiert ist und wenn err auf einen Wert ungleich null gesetzt ist - der Fluss unterbricht von try{} zu err-Handlerblöcken (falls vorhanden).

Intern gibt es keine Ausnahmen, aber das Ganze sollte näher liegen
zu einer Syntax, die if err != nil break Prüfungen nach jeder Zeile durchführt, in der err zugewiesen werden könnte.
Z.B:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

Ich weiß, es sieht aus wie C++, aber es ist auch bekannt und sauberer als das manuelle if err != nil {...} nach jeder Zeile.

@wie

Der Scannertyp funktioniert, weil er die gesamte Arbeit erledigt und es sich daher leisten kann, seine eigenen Fehler auf dem Weg zu verfolgen. Machen Sie sich bitte nicht vor, dass dies eine universelle Lösung ist.

@carlmjohnson

Wenn wir eine Einzeiler-Behandlung für einfache Fehler wünschen, könnten wir die Syntax ändern, damit die return-Anweisung der Anfang eines einzeiligen Blocks ist.
Es würde den Leuten erlauben zu schreiben:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

Ich denke, die Spezifikation sollte in etwas wie geändert werden (das könnte ziemlich naiv sein :))

Block = "{" StatementList "}" | "return" Expression .

Ich glaube nicht, dass eine spezielle Groß-/Kleinschreibung wirklich besser ist, als nur gofmt zu ändern, um es einfach zu machen, wenn err eine Zeile statt drei überprüft.

@urandom

Fehler, die über einen boxbaren Typ hinausgehen, und seine Aktionen sollten nicht gefördert werden. Für mich weist es auf mangelnden Aufwand hin, Fehlerkontext zwischen Fehlern, die aus verschiedenen, nicht zusammenhängenden Aktionen stammen, einzuschließen oder hinzuzufügen.

Der Scanner-Ansatz ist eines der schlimmsten Dinge, die ich im Zusammenhang mit diesem ganzen Mantra "Fehler sind Werte" gelesen habe:

  1. Es ist in so ziemlich jedem Anwendungsfall nutzlos, der viel Fehlerbehandlung erfordert. Funktionen, die mehrere externe Pakete aufrufen, profitieren davon nicht.
  2. Es ist ein verworrenes und unbekanntes Konzept. Die Einführung wird zukünftige Leser nur verwirren und Ihren Code komplizierter machen, als er sein müsste, nur damit Sie Mängel im Sprachdesign umgehen können.
  3. Es verbirgt Logik und versucht, Ausnahmen zu ähneln, indem es das Schlimmste herausnimmt (komplexer Kontrollfluss), ohne irgendwelche Vorteile zu ziehen.
  4. In einigen Fällen werden dadurch Rechenressourcen verschwendet. Jeder Anruf wird Zeit mit nutzlosen Fehlerprüfungen verschwenden müssen, die vor Ewigkeiten stattgefunden haben.
  5. Es verbirgt die genaue Stelle, an der der Fehler aufgetreten ist. Stellen Sie sich einen Fall vor, in dem Sie ein Dateiformat analysieren oder serialisieren. Sie hätten eine Kette von Lese-/Schreibaufrufen. Stellen Sie sich vor, der erste scheitert. Woran würden Sie erkennen, wo genau der Fehler aufgetreten ist? Welches Feld wurde analysiert oder serialisiert? "IO-Fehler", "Timeout" - diese Fehler wären in diesem Fall nutzlos. Sie können jedem Lese-/Schreibzugriff einen Kontext bereitstellen (zB Feldname). Aber an diesem Punkt ist es besser, den ganzen Ansatz einfach aufzugeben, da er gegen Sie arbeitet.

In einigen Fällen werden dadurch Rechenressourcen verschwendet.

Benchmarks? Was genau ist eine "Computerressource"?

Es verbirgt die genaue Stelle, an der der Fehler aufgetreten ist.

Nein, tut es nicht, weil Fehler ungleich Null nicht überschrieben werden

Funktionen, die mehrere externe Pakete aufrufen, profitieren davon nicht.
Es ist ein verworrenes und unbekanntes Konzept
Der Scanner-Ansatz ist eines der schlimmsten Dinge, die ich im Zusammenhang mit dieser ganzen "Fehler sind Werte" gelesen habe.

Mein Eindruck ist, dass Sie den Ansatz nicht verstehen. Es ist logisch äquivalent zu einer regelmäßigen Fehlerprüfung in einem eigenständigen Typ. Ich schlage vor, Sie studieren das Beispiel genau, damit es vielleicht das Schlimmste ist, was Sie verstehen, anstatt nur das Schlechteste, was Sie _gelesen_ haben.

Entschuldigung, ich werde dem Stapel meinen eigenen Vorschlag hinzufügen. Ich habe das meiste hier gelesen, und obwohl ich einige der Vorschläge mag, habe ich das Gefühl, dass sie zu viel versuchen. Das Problem ist Fehler Boilerplate. Mein Vorschlag ist einfach, diese Boilerplate auf der Syntaxebene zu eliminieren und die Art und Weise der Fehlerweitergabe in Ruhe zu lassen.

Vorschlag

Reduzieren Sie die Fehler-Boilerplate, indem Sie die Verwendung des _! Tokens als syntaktischen Zucker aktivieren, um eine Panik auszulösen, wenn ein error Wert ungleich Null zugewiesen wird

val, err := something.MayError()
if err != nil {
    panic(err)
}

könnte werden

val, _! := something.MayError()

und

if err := something.MayError(); err != nil {
    panic(err)
}

könnte werden

_! = something.MayError()

Natürlich steht das jeweilige Symbol zur Debatte. Ich habe auch _^ , _* , @ und andere in Betracht gezogen. Ich habe _! als De-facto-Vorschlag gewählt, weil ich dachte, dass es auf einen Blick am bekanntesten wäre.

Syntaktisch wäre _! (oder das gewählte Token) ein Symbol vom Typ error das in dem Geltungsbereich verfügbar ist, in dem es verwendet wird. Es beginnt mit nil , und jedes Mal, wenn es zugewiesen wird, wird eine nil Prüfung durchgeführt. Wenn es auf einen Wert von error ungleich Null gesetzt ist, wird eine Panik ausgelöst. Da _! (oder wiederum das gewählte Token) kein syntaktisch gültiger Bezeichner in go wäre, wäre eine Namenskollision kein Problem. Diese ätherische Variable würde nur in Bereichen eingeführt, in denen sie verwendet wird, ähnlich wie benannte Rückgabewerte. Wenn ein syntaktisch gültiger Bezeichner benötigt wird, könnte möglicherweise ein Platzhalter verwendet werden, der zur Kompilierzeit in einen eindeutigen Namen umgeschrieben wird.

Rechtfertigung

Einer der häufigsten Kritikpunkte, die ich bei go sehe, ist die Ausführlichkeit der Fehlerbehandlung. Fehler an API-Grenzen sind keine schlechte Sache. Die Fehler an die API-Grenzen bringen zu müssen, kann jedoch mühsam sein, insbesondere bei tief rekursiven Algorithmen. Um die zusätzliche Ausführlichkeitsfehlerausbreitung zu umgehen, die rekursiven Code einführt, können Panics verwendet werden. Ich glaube, dass dies eine ziemlich häufig verwendete Technik ist. Ich habe es in meinem eigenen Code verwendet und es in freier Wildbahn gesehen, einschließlich im Parser von go. Manchmal haben Sie die Validierung an anderer Stelle in Ihrem Programm durchgeführt und erwarten, dass ein Fehler null ist. Wenn ein Fehler ungleich Null empfangen würde, würde dies Ihre Invariante verletzen. Wenn eine Invariante verletzt wird, ist es akzeptabel, in Panik zu geraten. In komplexem Initialisierungscode ist es manchmal sinnvoll, Fehler in Panik zu verwandeln und sie zu reparieren, um sie mit mehr Kontextkenntnis irgendwo zurückzugeben. In all diesen Szenarien besteht die Möglichkeit, Fehler-Boilerplate zu reduzieren.

Mir ist klar, dass es die Philosophie von go ist, Panik so weit wie möglich zu vermeiden. Sie sind kein Werkzeug zur Fehlerweiterleitung über API-Grenzen hinweg. Sie sind jedoch ein Merkmal der Sprache und haben legitime Anwendungsfälle, wie die oben beschriebenen. Paniken sind eine fantastische Möglichkeit, die Fehlerausbreitung in privatem Code zu vereinfachen, und eine Vereinfachung der Syntax würde viel dazu beitragen, den Code sauberer und möglicherweise klarer zu machen. Ich glaube, dass _! (oder @ oder `_^ usw.) Ein Token kann die Menge an Code, die geschrieben/gelesen werden muss, um Folgendes zu vermitteln/verstehen, drastisch reduzieren:

  1. es könnte ein fehler sein
  2. Wenn es einen Fehler gibt, erwarten wir ihn nicht
  3. Wenn ein Fehler auftritt, wird er wahrscheinlich in der Kette verarbeitet

Wie bei jeder Syntaxfunktion besteht die Möglichkeit des Missbrauchs. In diesem Fall verfügt die go-Community bereits über eine Reihe von Best Practices für den Umgang mit Panik. Da dieser Syntaxzusatz syntaktischer Zucker für Panik ist, können diese Best Practices auf seine Verwendung angewendet werden.

Dies erleichtert neben der Vereinfachung der akzeptablen Anwendungsfälle für Panik auch das schnelle Prototyping in go. Wenn ich eine Idee habe, die ich im Code festhalten möchte, und nur möchte, dass Fehler das Programm zum Absturz bringen, während ich herumspiele, könnte ich diese Syntaxergänzung anstelle des "Wenn-Fehler-Panik"-Formulars verwenden. Wenn ich mich in den frühen Phasen der Entwicklung in weniger Zeilen ausdrücken kann, kann ich meine Ideen schneller in Code umsetzen. Sobald ich eine vollständige Idee im Code habe, gehe ich zurück und refaktoriere meinen Code, um Fehler an den entsprechenden Grenzen zurückzugeben. Ich würde keine Panik im Produktionscode lassen, aber sie können ein leistungsstarkes Entwicklungswerkzeug sein.

Paniken sind nur Ausnahmen mit einem anderen Namen, und eine Sache, die ich an Go liebe, ist, dass Ausnahmen außergewöhnlich sind. Ich möchte nicht zu weiteren Ausnahmen ermutigen, indem ich ihnen syntaktischen Zucker gebe.

@carlmjohnson Eines von zwei Dingen muss wahr sein:

  1. Panik ist ein Teil der Sprache mit legitimen Anwendungsfällen, oder
  2. Panik hat keine legitimen Anwendungsfälle und sollte daher aus der Sprache entfernt werden

Ich vermute, die Antwort ist 1.
Ich bin auch nicht der Meinung, dass "Paniken nur Ausnahmen mit einem anderen Namen sind". Ich denke, diese Art von Handbewegungen verhindert eine echte Diskussion. Es gibt wichtige Unterschiede zwischen Panik und Ausnahmen, wie in den meisten anderen Sprachen zu sehen ist.

Ich verstehe die reflexartige "Panik sind schlecht"-Reaktion, aber persönliche Gefühle bezüglich des Gebrauchs von Panik ändern nichts an der Tatsache, dass Panik verwendet wird und tatsächlich nützlich ist. Der go-Compiler verwendet Panik, um sowohl im Parser als auch in der Typprüfungsphase (zuletzt nachgesehen) aus tief rekursiven Prozessen auszusteigen.
Sie zu verwenden, um Fehler durch tief rekursiven Code zu verbreiten, scheint nicht nur eine akzeptable Verwendung zu sein, sondern eine Verwendung, die von den Go-Entwicklern befürwortet wird.

Panik kommuniziert etwas Bestimmtes:

hier ist etwas schief gelaufen , auf das hier nicht vorbereitet war

Es wird immer Stellen im Code geben, an denen das zutrifft. Besonders früh in der Entwicklung. Go wurde modifiziert, um die Refactoring-Erfahrung zuvor zu verbessern: das Hinzufügen von Typaliasen. In der Lage zu sein, unerwünschte Fehler mit Panik zu verbreiten, bis Sie herausfinden können, ob und wie sie näher an der Quelle behandelt werden können, kann das Schreiben und schrittweise Refactoring von Code viel weniger ausführlich machen.

Ich habe das Gefühl, dass die meisten Vorschläge hier große Änderungen der Sprache vorschlagen. Dies ist der transparenteste Ansatz, den ich finden konnte. Es ermöglicht, dass das gesamte aktuelle kognitive Modell der Fehlerbehandlung in go intakt bleibt und gleichzeitig eine Syntaxreduktion für einen bestimmten, aber häufigen Fall ermöglicht wird. Best Practices schreiben derzeit vor, dass "Go-Code nicht über API-Grenzen hinweg in Panik geraten sollte". Wenn ich öffentliche Methoden in einem Paket habe, sollten sie Fehler zurückgeben, wenn etwas schief geht, außer in seltenen Fällen, in denen der Fehler nicht behebbar ist (z. B. unveränderliche Verletzungen). Diese Ergänzung der Sprache würde diese bewährte Vorgehensweise nicht ersetzen. Dies ist einfach eine Möglichkeit, Boilerplate im internen Code zu reduzieren und das Skizzieren von Ideen klarer zu machen. Es erleichtert sicherlich das lineare Lesen von Code.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

ist viel lesbarer als

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Es gibt wirklich keinen grundlegenden Unterschied zwischen einer Panik in Go und einer Ausnahme in Java oder Python usw. abgesehen von der Syntax und dem Fehlen einer Objekthierarchie (was sinnvoll ist, da Go keine Vererbung hat). Wie sie funktionieren und wie sie verwendet werden, ist die gleiche.

Natürlich hat Panik einen legitimen Platz in der Sprache. Paniken sind für Handhabungsfehler gedacht, die nur aufgrund von Programmierfehlern auftreten sollten, die ansonsten nicht behebbar sind. Wenn Sie beispielsweise in einem Integer-Kontext durch Null dividieren, gibt es keinen möglichen Rückgabewert und es ist Ihre eigene Schuld, nicht zuerst auf Null zu prüfen, also gerät es in Panik. Wenn Sie ein Slice außerhalb der Grenzen lesen, versuchen Sie, nil als Wert usw. zu verwenden. Diese Dinge werden durch Programmierfehler verursacht – nicht durch eine erwartete Bedingung, wie z und sprengen Sie den Stapel. Go bietet einige Hilfsfunktionen, die wie template.Must in Panik geraten, weil davon ausgegangen wird, dass diese mit hartcodierten Strings verwendet werden, bei denen jeder Fehler durch Programmierfehler verursacht werden müsste. Nicht genügend Arbeitsspeicher ist per se kein Programmierfehler, aber es ist auch nicht behebbar und kann überall passieren, also ist es kein Fehler, sondern eine Panik.

Die Leute benutzen manchmal auch Panik, um den Stack kurzzuschließen, aber das ist im Allgemeinen aus Gründen der Lesbarkeit und Leistung verpönt, und ich sehe keine Chance, Go zu ändern, um seine Verwendung zu fördern.

Go Panics und die ungeprüften Ausnahmen von Java sind ziemlich identisch und existieren aus den gleichen Gründen und um die gleichen Anwendungsfälle zu behandeln. Ermutigen Sie die Leute nicht, Panik für andere Fälle zu verwenden, da diese Fälle die gleichen Probleme haben wie Ausnahmen in anderen Sprachen.

Die Leute verwenden manchmal auch Panik, um den Stack kurzzuschließen, aber das ist im Allgemeinen aus Gründen der Lesbarkeit und Leistung verpönt

Zuallererst ist das Problem der Lesbarkeit etwas, das diese Syntaxänderung direkt anspricht:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

vs

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Abgesehen von der Lesbarkeit im Moment wird als weiterer Grund die Leistung angegeben.
Ja, es stimmt, dass die Verwendung von Panics- und Defer-Anweisungen eine Leistungseinbuße nach sich zieht, aber in vielen Fällen ist dieser Unterschied für die ausgeführte Operation vernachlässigbar. Festplatten- und Netzwerk-IO werden im Durchschnitt viel länger dauern als jede potenzielle Stack-Magie, um Verzögerungen/Paniken zu verwalten.

Ich höre diesen Punkt oft nachplappern, wenn ich über Panik spreche, und ich finde es unaufrichtig zu sagen, dass Panik eine Leistungsverschlechterung ist. Sie KÖNNEN sicherlich sein, aber sie müssen nicht sein. Genau wie viele andere Dinge in einer Sprache. Wenn Sie in einer engen Schleife in Panik geraten, in der der Leistungseinbruch wirklich wichtig wäre, sollten Sie nicht auch innerhalb dieser Schleife aufschieben. Tatsächlich sollte jede Funktion, die selbst in Panik gerät, im Allgemeinen nicht ihre eigene Panik bekommen. Ebenso würde eine heute geschriebene go-Funktion nicht sowohl einen Fehler als auch eine Panik zurückgeben. Das ist unklar, albern und nicht die beste Vorgehensweise. Vielleicht sind wir es so gewohnt, Ausnahmen in Java, Python, Javascript usw. zu sehen, aber so werden Paniken im Allgemeinen nicht im Go-Code verwendet, und ich glaube nicht, dass das Hinzufügen eines Operators speziell für den Fall der Weitergabe eines Fehlers erforderlich ist das Aufrufen der Anrufliste über Panik wird die Art und Weise ändern, wie Leute Panik verwenden. Sie benutzen sowieso Panik. Der Sinn dieser Syntaxerweiterung besteht darin , die Tatsache

Können Sie mir einige Beispiele für problematischen Code nennen, der Ihrer Meinung nach durch diese Syntaxfunktion aktiviert würde und die derzeit nicht möglich sind/gegen Best Practices verstoßen? Wenn jemand den Benutzern seines Codes Fehler per Panic/Recover mitteilt, ist das derzeit verpönt und würde es natürlich auch weiterhin bleiben, selbst wenn eine solche Syntax hinzugefügt würde. Wenn du könntest, beantworte bitte folgendes:

  1. Welche Missbräuche würden Ihrer Meinung nach durch eine Syntaxerweiterung wie diese entstehen?
  2. Was sagt var1, err := trySomeTask1(); if err != nil { panic(err) } , was var1, _! := trySomeTask1() nicht tut? Wieso den?

Mir scheint, der Kern Ihres Arguments ist, dass "Paniken schlecht sind und wir sie nicht verwenden sollten".
Ich kann die Gründe dafür nicht auspacken und diskutieren, wenn sie nicht geteilt werden.

Diese Dinge werden durch Programmierfehler verursacht – nicht durch eine erwartete Bedingung, wie z. B. ein Netzwerkausfall oder eine Datei mit schlechten Berechtigungen –, also geraten sie in Panik und sprengen den Stapel.

Ich, wie die meisten Gopher, mag die Idee von Fehlern als Werten. Ich glaube, dass es hilft, klar zu kommunizieren, welche Teile einer API ein Ergebnis garantieren und welche fehlschlagen können, ohne die Dokumentation lesen zu müssen.

Es ermöglicht Dinge wie das Sammeln von Fehlern und das Anreichern von Fehlern mit weiteren Informationen. Das ist alles sehr wichtig an API-Grenzen, wo sich Ihr Code mit Benutzercode überschneidet. Innerhalb dieser API-Grenzen ist es jedoch oft nicht erforderlich, all dies mit Ihren Fehlern zu tun. Vor allem, wenn Sie den glücklichen Pfad erwarten und anderen Code haben, der für die Behandlung des Fehlers verantwortlich ist, wenn dieser Pfad fehlschlägt.

Manchmal ist es nicht die Aufgabe Ihres Codes, einen Fehler zu behandeln.
Wenn ich eine Bibliothek schreibe, ist es mir egal, ob der Netzwerkstapel ausgefallen ist - das liegt außerhalb meiner Kontrolle als Bibliotheksentwickler. Ich werde diese Fehler an den Benutzercode zurückgeben.

Sogar in meinem eigenen Code gibt es Zeiten, in denen ich einen Code schreibe, dessen einzige Aufgabe darin besteht, die Fehler an eine übergeordnete Funktion zurückzugeben.

Angenommen, Sie haben eine http.HandlerFunc-Antwort, die eine Datei von der Festplatte liest - dies wird fast immer funktionieren, und wenn es fehlschlägt, ist es wahrscheinlich, dass das Programm nicht richtig geschrieben wurde (Programmiererfehler) oder ein Problem mit dem Dateisystem vorliegt außerhalb des Verantwortungsbereichs des Programms. Sobald http.HandlerFunc in Panik gerät, wird es beendet und ein Basishandler wird diese Panik abfangen und eine 500 an den Client schreiben. Wenn ich diesen Fehler zu irgendeinem Zeitpunkt anders behandeln möchte, kann ich _! durch err ersetzen und mit dem Fehlerwert tun, was ich will. Die Sache ist die, während der Laufzeit des Programms werde ich das wahrscheinlich nicht tun müssen. Wenn ich auf solche Probleme stoße, ist der Handler nicht der Teil des Codes, der für die Behandlung dieses Fehlers verantwortlich ist.

Ich kann if err != nil { panic(err) } oder if err != nil { return ..., err } für Dinge wie E/A-Ausfälle, Netzwerkausfälle usw. in meine Handler schreiben und kann dies normalerweise tun. Wenn ich einen Fehler überprüfen muss, kann ich das immer noch tun. Die meiste Zeit schreibe ich jedoch nur if err != nil { panic(err) } .

Oder ein anderes Beispiel: Wenn ich rekursiv nach einem Trie suche (etwa in einer HTTP-Router-Implementierung), deklariere ich eine Funktion func (root *Node) Find(path string) (found Value, err error) . Diese Funktion wird eine Funktion zurückstellen, um alle Panics wiederherzustellen, die beim Herunterfahren des Baums erzeugt werden. Was ist, wenn das Programm fehlerhafte Versuche erstellt? Was ist, wenn einige IO fehlschlagen, weil das Programm nicht als Benutzer mit den richtigen Berechtigungen ausgeführt wird? Diese Probleme sind nicht das Problem meines Trie-Suchalgorithmus - es sei denn, ich mache es später explizit -, aber es sind mögliche Fehler, auf die ich stoßen kann. Wenn Sie sie ganz nach oben im Stack zurückgeben, führt dies zu einer Menge zusätzlicher Ausführlichkeit, einschließlich des Haltens von idealerweise mehreren Null-Fehlerwerten auf dem Stack. Stattdessen kann ich einen Fehler bis zu dieser öffentlichen API-Funktion in Panik versetzen und an den Benutzer zurückgeben. Im Moment führt dies noch zu dieser zusätzlichen Ausführlichkeit, muss aber nicht.

Andere Vorschläge diskutieren, wie ein Rückgabewert als etwas Besonderes behandelt werden soll. Es ist im Wesentlichen der gleiche Gedanke, aber anstatt bereits in die Sprache integrierte Funktionen zu verwenden, versuchen sie, das Verhalten der Sprache für bestimmte Fälle zu ändern. In Bezug auf die einfache Umsetzung wird diese Art von Vorschlag (syntaktischer Zucker für etwas, das bereits unterstützt wird) am einfachsten sein.

Bearbeiten um hinzuzufügen:
Ich bin nicht mit dem Vorschlag verheiratet, den ich so geschrieben habe, wie er geschrieben ist, aber ich denke, dass es wichtig ist, das Problem der Fehlerbehandlung aus einem neuen Blickwinkel zu betrachten. Niemand schlägt etwas so Romantisches vor, und ich möchte sehen, ob wir unser Verständnis des Themas neu definieren können. Das Problem ist, dass es zu viele Stellen gibt, an denen Fehler explizit behandelt werden, wenn dies nicht erforderlich ist, und Entwickler möchten eine Möglichkeit haben, sie ohne zusätzlichen Boilerplate-Code im Stack zu verbreiten. Es stellt sich heraus, dass Go bereits über diese Funktion verfügt, aber es gibt keine schöne Syntax dafür. Dies ist eine Diskussion über das Umschließen vorhandener Funktionen in eine weniger ausführliche Syntax, um die Sprache ergonomischer zu gestalten, ohne das Verhalten zu ändern. Ist das nicht ein Gewinn, wenn wir es schaffen?

@mccolljr Danke, aber eines der Ziele dieses Vorschlags besteht darin, die Leute zu ermutigen, neue Wege zur Behandlung aller drei Fälle der Fehlerbehandlung zu entwickeln: Fehler ignorieren, Fehler unverändert zurückgeben, Fehler mit zusätzlichen Kontextinformationen zurückgeben. Ihr Panikvorschlag geht nicht auf den dritten Fall ein. Es ist ein wichtiger.

@mccolljr Ich denke, die API-Grenzen sind viel häufiger als Sie meinen. Ich sehe nicht, dass innerhalb der API-Aufrufe der übliche Fall ist. Wenn überhaupt, könnte es umgekehrt sein (einige Daten wären hier interessant). Daher bin ich mir nicht sicher, ob die Entwicklung einer speziellen Syntax für Aufrufe innerhalb der API die richtige Richtung ist. Darüber hinaus ist die Verwendung von return ed-Fehlern anstelle von panic ed-Fehlern innerhalb einer API normalerweise ein guter Weg (insbesondere wenn wir einen Plan für dieses Problem entwickeln). panic ed-Fehler haben ihren Nutzen, aber Situationen, in denen sie nachweislich besser sind, scheinen selten zu sein.

Ich glaube nicht, dass das Hinzufügen eines Operators speziell für den Fall, dass ein Fehler in der Aufrufliste über Panik verbreitet wird, die Art und Weise ändern wird, wie Leute Panik verwenden.

Ich glaube, du irrst dich. Die Leute werden nach Ihrer Kurzschrift-Bedienung greifen, weil es so praktisch ist, und dann werden sie am Ende viel mehr in Panik geraten als zuvor.

Ob Panik manchmal oder selten nützlich ist und ob sie über oder innerhalb der API-Grenzen nützlich sind, sind Ablenkungsmanöver. Es gibt viele Aktionen, die man bei einem Fehler ausführen kann. Wir suchen nach einer Möglichkeit, den Fehlerbehandlungscode zu verkürzen, ohne eine Aktion den anderen vorzuziehen.

aber in vielen Fällen ist dieser Unterschied für die durchgeführte Operation vernachlässigbar

Das stimmt zwar, aber ich denke, es ist ein gefährlicher Weg. Auf den ersten Blick vernachlässigbar, würde es sich häufen und später, wenn es bereits spät ist, zu Engpässen führen. Ich denke, wir sollten die Leistung von Anfang an im Auge behalten und versuchen, bessere Lösungen zu finden. Die bereits erwähnten Swift und Rust haben eine Fehlerausbreitung, implementieren sie jedoch im Grunde genommen als einfache Rückgaben, die in syntaktischen Zucker verpackt sind. Ja, es ist einfach, eine vorhandene Lösung wiederzuverwenden, aber ich würde es vorziehen, alles so zu belassen, wie es ist, als zu vereinfachen und die Leute dazu zu ermutigen, Paniken zu verwenden, die sich hinter unbekanntem syntaktischem Zucker verbergen, der versucht, die Tatsache zu verbergen, dass es sich im Grunde genommen um Ausnahmen handelt.

Auf den ersten Blick vernachlässigbar, würde es sich häufen und später, wenn es bereits spät ist, zu Engpässen führen.

Nein Danke. Imaginäre Leistungsengpässe sind geometrisch vernachlässigbare Leistungsengpässe.

Nein Danke. Imaginäre Leistungsengpässe sind geometrisch vernachlässigbare Leistungsengpässe.

Bitte lassen Sie Ihre persönlichen Gefühle aus diesem Thema heraus. Du hast offensichtlich ein Problem mit mir und möchtest nichts Nützliches mitbringen, also ignoriere einfach meine Kommentare und lass eine Stimme ab, wie du es bei so ziemlich jedem Kommentar zuvor getan hast. Sie müssen diese unsinnigen Antworten nicht weiter posten.

Ich habe kein Problem mit Ihnen, Sie behaupten nur Leistungsengpässe ohne Daten, die das belegen, und ich weise mit Worten und Daumen darauf hin.

Leute, bitte halte das Gespräch respektvoll und themenbezogen. Bei diesem Problem geht es um die Behandlung von Go-Fehlern.

https://golang.org/conduct

Ich möchte den Teil "Fehler zurückgeben / mit zusätzlichem Kontext" noch einmal aufgreifen, da ich davon ausgehen, dass das Ignorieren des Fehlers bereits durch das bereits vorhandene _ abgedeckt ist.

Ich schlage ein aus zwei Wörtern bestehendes Schlüsselwort vor, dem (optional) eine Zeichenfolge folgen kann. Der Grund, warum es ein Schlüsselwort mit zwei Wörtern ist, ist zweifach. Erstens ist es im Gegensatz zu einem von Natur aus kryptischen Operator einfacher zu verstehen, was er tut, ohne zu viel Vorwissen zu haben. Ich habe "oder Blase" ausgewählt, weil ich hoffe, dass das Wort or ohne zugewiesenen Fehler dem Benutzer anzeigt, dass der Fehler hier behandelt wird, wenn er nicht null ist. Einige Benutzer werden or mit der Behandlung eines falschen Wertes aus anderen Sprachen (Perl, Python) in Verbindung bringen, und das Lesen von data := Foo() or ... könnte ihnen unbewusst sagen, dass data unbrauchbar ist, wenn das or Teil der Anweisung ist erreicht. Zweitens kann das Schlüsselwort bubble obwohl es relativ kurz ist, für den Benutzer bedeuten, dass etwas nach oben geht (der Stapel). Das Wort up könnte auch passend sein, obwohl ich nicht sicher bin, ob das ganze or up verständlich genug ist. Schließlich ist das Ganze ein Schlüsselwort, erstens, weil es besser lesbar ist und zweitens, weil dieses Verhalten nicht von einer Funktion selbst geschrieben werden kann (Sie können möglicherweise Panik aufrufen, um der Funktion zu entkommen, in der Sie sich befinden, aber dann können Sie halte dich nicht selbst auf, jemand anderes muss sich erholen).

Das Folgende dient nur der Fehlerfortpflanzung und darf daher nur in Funktionen verwendet werden, die einen Fehler zurückgeben, und die Nullwerte aller anderen Rückgabeargumente:

Um einen Fehler zurückzugeben, ohne ihn in irgendeiner Weise zu ändern:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

Um einen Fehler mit einer zusätzlichen Meldung zurückzugeben:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

Und schließlich führen Sie einen globalen Adaptermechanismus zur benutzerdefinierten Fehlermodifikation ein:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

Schließlich ist für die wenigen Stellen, an denen eine wirklich komplexe Handhabung erforderlich ist, der bereits vorhandene ausführliche Weg bereits der beste Weg.

Interessant. Ein globaler Bubble-Handler gibt Leuten, die Stack-Traces wollen, einen Platz, um den Call nach einem Trace zu platzieren, was ein netter Vorteil dieser Methode ist. OTOH, wenn es die Signatur func(string, error) error hat, bedeutet dies, dass das Bubbling mit dem eingebauten Fehlertyp und nicht mit einem anderen Typ durchgeführt werden muss, wie etwa einem konkreten Typ, der error implementiert.

Außerdem legt die Existenz von or bubble die Möglichkeit von or die oder or panic . Ich bin mir nicht sicher, ob das ein Feature oder ein Bug ist.

Im Gegensatz zu einem von Natur aus kryptischen Operator ist es einfacher zu verstehen, was er tut, ohne zu viel Vorwissen zu haben

Das ist vielleicht gut, wenn Sie es zum ersten Mal treffen. Aber es immer wieder zu lesen und zu schreiben - es scheint zu ausführlich zu sein und braucht zu viel Platz, um eine ziemlich einfache Sache zu vermitteln - unbehandelter Fehler mit Blasen im Stapel. Operatoren sind zunächst kryptisch, aber sie sind prägnant und haben einen guten Kontrast zu all dem anderen Code. Sie trennen die Hauptlogik klar von der Fehlerbehandlung, da es sich tatsächlich um ein Trennzeichen handelt. So viele Wörter in einer Zeile zu haben, beeinträchtigt meiner Meinung nach die Lesbarkeit. Füge sie zumindest in orbubble oder lasse einen von ihnen fallen. Ich sehe keinen Sinn darin, dort zwei Schlüsselwörter zu haben. Es macht Go zu einer gesprochenen Sprache und wir wissen, wie das geht (VB zum Beispiel)

Ich bin kein großer Fan von globalen Adaptern. Wenn mein Paket einen benutzerdefinierten Prozessor festlegt und Ihres auch einen benutzerdefinierten Prozessor festlegt, wer gewinnt dann?

@object88
Ich denke, es ähnelt dem Standard-Logger. Sie legen die Ausgabe nur einmal (in Ihrem Programm) fest und das betrifft alle Pakete, die Sie verwenden.

Die Fehlerbehandlung unterscheidet sich stark von der Protokollierung; einer beschreibt die informative Ausgabe des Programms, der andere verwaltet den Programmablauf. Wenn ich den Adapter so einrichte, dass er eine Sache in meinem Paket macht, dass ich den logischen Fluss richtig verwalten muss, und ein anderes Paket oder Programm dies ändert, sind Sie an einem schlechten Ort.

Bitte bringt den Try Catch Endlich zurück und wir brauchen keine Kämpfe mehr. Es macht alle glücklich. Es ist nichts falsch daran, Funktionen und Syntaxen von anderen Programmiersprachen auszuleihen. Java hat es geschafft und C# hat es auch gemacht und beide sind wirklich erfolgreiche Programmiersprachen. GO-Community (oder Autoren) bitte seien Sie offen für Änderungen, wenn dies erforderlich ist.

@KamyarM , ich stimme respektvoll nicht zu; try/catch macht _nicht_ alle glücklich. Selbst wenn Sie dies in Ihrem Code implementieren wollten, bedeutet eine ausgelöste Ausnahme, dass jeder, der Ihren Code verwendet, Ausnahmen behandeln muss. Dies ist keine Sprachänderung, die für Ihren Code lokalisiert werden kann.

@object88
Tatsächlich scheint mir ein Bubble-Prozessor die informative Fehlerausgabe des Programms zu beschreiben, wodurch es sich nicht wesentlich von einem Logger unterscheidet. Und ich kann mir vorstellen, dass Sie in Ihrer gesamten Anwendung eine einzige Fehlerdarstellung wünschen und sich nicht von Paket zu Paket unterscheiden.

Obwohl Sie vielleicht ein kurzes Beispiel geben können, fehlt mir vielleicht etwas.

Vielen Dank für Ihre Daumen nach unten. Genau von diesem Thema rede ich. Die GO-Community ist nicht offen für Veränderungen und ich spüre das und ich mag das wirklich nicht.

Dies hat wahrscheinlich nichts mit diesem Fall zu tun, aber ich habe an einem anderen Tag nach dem Go-Äquivalent des ternären Operators von C++ gesucht und bin auf diesen alternativen Ansatz gestoßen:

v := map[bool]int{true: erster_Ausdruck, falsch: zweiter_Ausdruck} [Bedingung]
statt einfach
v= Bedingung ? erster_ausdruck : zweiter_ausdruck;

Welche der 2 Formen bevorzugst du? Ein unlesbarer Code oben (Go My Way) mit wahrscheinlich vielen Leistungsproblemen oder die zweite einfache Syntax in C++ (Highway)? Ich bevorzuge die Typen von der Autobahn. Ich weiß nicht, wie es dir geht.

Also, um es zusammenzufassen, bringen Sie bitte neue Syntaxen mit, leihen Sie sie sich von anderen Programmiersprachen aus. Daran ist nichts auszusetzen.

Mit freundlichen Grüßen,

Die GO-Community ist nicht offen für Veränderungen und ich spüre das und ich mag das wirklich nicht.

Ich denke, dies beschreibt die Einstellung, die dem zugrunde liegt, was Sie erleben, falsch. Ja, die Community produziert viel Push-Back, wenn jemand Try/Catch oder ?: vorschlägt. Aber das liegt nicht daran, dass wir gegen neue Ideen resistent sind. Wir haben fast alle Erfahrung mit Sprachen mit diesen Funktionen. Wir kennen sie gut und jemand von uns benutzt sie seit Jahren täglich. Unser Widerstand beruht darauf, dass dies _alte Ideen_ sind, keine neuen. Wir haben bereits eine Änderung angenommen: eine Änderung weg von Try/Catch und eine Änderung weg von der Verwendung von ?:. Wogegen wir resistent sind, ist, _zurück_ zu ändern, um diese Dinge zu verwenden, die wir bereits verwendet haben und die wir nicht mochten.

Tatsächlich scheint mir ein Bubble-Prozessor die informative Fehlerausgabe des Programms zu beschreiben, wodurch es sich nicht wesentlich von einem Logger unterscheidet. Und ich kann mir vorstellen, dass Sie in Ihrer gesamten Anwendung eine einzige Fehlerdarstellung wünschen und sich nicht von Paket zu Paket unterscheiden.

Was wäre, wenn jemand Bubbling verwenden wollte, um Stack-Traces zu übergeben und dann eine Entscheidung zu treffen. Wenn der Fehler beispielsweise von einer Dateioperation herrührt, schlagen Sie fehl, aber wenn er vom Netzwerk stammt, warten Sie und versuchen Sie es erneut. Ich könnte mir vorstellen, dafür eine Logik in einen Fehlerhandler einzubauen, aber wenn es nur einen Fehlerhandler pro Laufzeit gibt, wäre das ein Rezept für Konflikte.

@urandom , vielleicht ist dies ein triviales Beispiel, aber nehmen wir an, mein Adapter gibt eine andere Struktur zurück, die error implementiert, von der ich erwarte, dass sie an anderer Stelle in meinem Code verwendet wird. Wenn ein anderer Adapter kommt und meinen Adapter ersetzt, funktioniert mein Code nicht mehr richtig.

@KamyarM Die Sprache und ihre Redewendungen gehören zusammen. Wenn wir über Änderungen an der Fehlerbehandlung nachdenken, meinen wir nicht nur die Änderung der Syntax, sondern (möglicherweise) auch die Struktur des Codes selbst.

Try-Catch-finally wäre eine sehr invasive Änderung: Es würde die Art und Weise, wie Go-Programme strukturiert sind, grundlegend verändern. Im Gegensatz dazu sind die meisten anderen Vorschläge, die Sie hier sehen, lokal für jede Funktion: Fehler sind immer noch explizit zurückgegebene Werte, der Kontrollfluss vermeidet nicht-lokale Sprünge usw.

Um Ihr Beispiel eines ternären Operators zu verwenden: Ja, Sie können heute einen mit einer Karte vortäuschen, aber ich hoffe, Sie werden das nicht im Produktionscode finden. Es folgt nicht den Redewendungen. Stattdessen sehen Sie normalerweise mehr wie:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

Es ist nicht so, dass wir uns keine Syntax ausleihen wollen, sondern wir müssen uns überlegen, wie sie in den Rest der Sprache und den Rest des heute bereits existierenden Codes passen würde.

@KamyarM Ich verwende sowohl Go als auch Java und möchte ausdrücklich _nicht_, dass Go die Ausnahmebehandlung aus Java kopiert. Wenn Sie Java möchten, verwenden Sie Java. Und bitte führen Sie die Diskussion über ternäre Operatoren zu einem geeigneten Thema, zB #23248.

@lpar Wenn ich also für eine Firma arbeite und sie sich aus irgendeinem unbekannten Grund für GoLang als Programmiersprache entschieden habe, muss ich einfach meinen Job kündigen und mich für eine Java-Sprache bewerben!? Komm schon Mann!

@bcmills Sie können den dort vorgeschlagenen Code zählen. Ich denke, das sind 6 Codezeilen anstelle von einer, und wahrscheinlich erhalten Sie dafür einige zyklomatische Komplexitätspunkte für den Code (Ihr verwendet Linter. oder?).

@carlmjohnson und @bcmills Jede Syntax, die alt und ausgereift ist, bedeutet nicht, dass sie schlecht ist. Tatsächlich denke ich, dass die if else-Syntax viel älter ist als die ternäre Operatorsyntax.

Gut, dass du dieses GO-Idiom-Ding mitgebracht hast. Ich denke, das ist nur eines der Probleme dieser Sprache. Immer wenn eine Änderungsanfrage gestellt wird, sagt jemand, nein, das ist gegen das Go-Idiom. Ich sehe es nur als Entschuldigung, Veränderungen zu widerstehen und neue Ideen zu blockieren.

@KamyarM bitte sei höflich. Wenn Sie mehr darüber erfahren möchten, wie man die Sprache klein hält, empfehle ich https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

Auch ein allgemeiner Kommentar, der nichts mit der jüngsten Diskussion über Try/Catch zu tun hat.

Es gab viele Vorschläge in diesem Thread. Für mich selbst gesprochen habe ich immer noch nicht das Gefühl, dass ich das zu lösende Problem/die zu lösenden Probleme richtig verstanden habe. Ich würde gerne mehr über sie hören.

Ich würde mich auch freuen, wenn jemand die wenig beneidenswerte, aber wichtige Aufgabe übernehmen möchte, eine organisierte und zusammengefasste Liste der besprochenen Probleme zu führen.

@josharian Ich habe da nur offen gesprochen. Ich wollte die genauen Probleme in der Sprache oder Community zeigen. Betrachten Sie das als mehr Kritik. GoLang ist offen für Kritik, stimmt das?

@KamyarM Wenn Sie für ein Unternehmen arbeiten würden, das Rust für seine Programmiersprache ausgewählt hat, würden Sie zum Rust Github gehen und anfangen, Speicherverwaltung und Zeiger im C++-Stil zu fordern, damit Sie sich nicht mit dem Ausleihprüfer befassen müssen?

Der Grund, warum Go-Programmierer keine Ausnahmen im Java-Stil wollen, hat nichts mit mangelnder Vertrautheit mit ihnen zu tun. Ich bin 1988 über Lisp zum ersten Mal auf Ausnahmen gestoßen, und ich bin sicher, dass es andere Leute in diesem Thread gibt, die noch früher darauf gestoßen sind – die Idee geht auf die frühen 1970er Jahre zurück.

Das gleiche gilt noch mehr für ternäre Ausdrücke. Lesen Sie mehr über die Geschichte von Go – Ken Thompson, einer der Schöpfer von Go, implementierte den ternären Operator in der Sprache B (Vorgänger von C) 1969 bei Bell Labs. Ich denke, man kann mit Sicherheit sagen, dass er sich seiner Vorteile und Tücken bewusst war, wenn er darüber nachdachte ob es in Go aufgenommen werden soll.

Go ist offen für Kritik, aber wir verlangen, dass Diskussionen in den Go-Foren höflich sind. Aufrichtig sein ist nicht dasselbe wie unhöflich sein. Siehe den Abschnitt "Gopher-Werte" von https://golang.org/conduct. Danke.

@lpar Ja, wenn Rust so ein Forum hat würde ich das machen ;-) Ernsthaft würde ich das machen. Weil ich möchte, dass meine Stimme gehört wird.

@ianlancetaylor Habe ich vulgäre Wörter oder Sprache verwendet? Habe ich diskriminierende Sprache verwendet oder jemanden gemobbt oder unerwünschte sexuelle Annäherungsversuche gemacht? Ich glaube nicht.
Komm schon, Mann, wir reden hier nur über die Programmiersprache Go. Es geht nicht um eine Religion oder Politik oder ähnliches.
Ich war offen. Ich wollte, dass meine Stimme gehört wird. Ich denke, deshalb gibt es dieses Forum. Damit Stimmen gehört werden. Vielleicht gefällt Ihnen mein Vorschlag oder meine Kritik nicht. Kein Problem. Aber ich denke, Sie müssen mich reden und diskutieren lassen, sonst können wir alle zu dem Schluss kommen, dass alles perfekt ist und es kein Problem gibt und daher keine weiteren Diskussionen erforderlich sind.

@josharian Danke für den Artikel, den werde ich mir mal anschauen.

Nun, ich habe mir meine Kommentare angeschaut, um zu sehen, ob da etwas Schlimmes ist. Das einzige, was ich vielleicht beleidigt hätte (ich nenne das übrigens immer noch Kritik) sind die Redewendungen der Programmiersprache GoLang! Haha!

Um zu unserem Thema zurückzukehren: Wenn Sie meine Stimme hören, ziehen Sie bitte Go-Autoren in Betracht, die Try-Catch-Blöcke zurückzubringen. Überlassen Sie es dem Programmierer, zu entscheiden, ob er es an der richtigen Stelle verwendet oder nicht (Sie haben bereits etwas Ähnliches, ich meine die Panikverzögerung, dann warum nicht versuchen Sie Catch, das Programmierern vertrauter ist?).
Ich habe eine Problemumgehung für die aktuelle Go-Error-Behandlung aus Gründen der Abwärtskompatibilität vorgeschlagen. Ich sage nicht, dass das die beste Option ist, aber ich denke, es ist praktikabel.

Ich werde mich zurückziehen, um mehr zu diesem Thema zu diskutieren.

Vielen Dank für die Gelegenheit.

@KamyarM Sie verwechseln unsere Bitten, höflich zu bleiben, mit unserer

Nochmals: Seien Sie bitte höflich. Bleiben Sie bei technischen Argumenten. Vermeiden Sie Ad-hominem-Argumente, die eher Menschen als Ideen angreifen. Wenn Sie wirklich nicht verstehen, was ich meine, bin ich bereit, es offline zu besprechen; Maile mir. Danke.

Ich werfe meine 2c ein und hoffe, dass sie nicht buchstäblich etwas in den anderen N hundert Kommentaren wiederholt (oder die Diskussion über den Vorschlag von urandom aufgreift).

Ich mag die ursprüngliche Idee, die gepostet wurde, aber mit zwei grundlegenden Optimierungen:

  • Syntaktisches Bikeshedding: Ich bin fest davon überzeugt, dass alles, was einen impliziten Kontrollfluss hat, ein eigener Operator sein sollte und nicht eine Überlastung eines bestehenden Operators. Ich werfe ?! raus, aber ich bin mit allem zufrieden, was nicht leicht mit einem vorhandenen Operator in Go verwechselt werden kann.

  • Das RHS dieses Operators sollte eher eine Funktion als einen Ausdruck mit einem willkürlich eingefügten Wert annehmen. Dies würde es Entwicklern ermöglichen, einen ziemlich knappen Code zur Fehlerbehandlung zu schreiben, während sie sich über ihre Absichten im Klaren sind und flexibel sind, was sie tun können, z

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

Das RHS sollte niemals ausgewertet werden, wenn kein Fehler auftritt, daher weist dieser Code keine Schließungen oder was auch immer auf dem glücklichen Pfad zu.

Es ist auch ziemlich einfach, dieses Muster zu "überladen", um in interessanteren Fällen zu funktionieren. Ich habe drei Beispiele im Kopf.

Erstens könnten wir return bedingt haben, wenn das RHS ein func(error) (error, bool) , so (wenn wir dies zulassen, sollten wir einen anderen Operator als die unbedingten Rückgaben verwenden Verwenden Sie ?? , aber meine Aussage "Ist mir egal, solange es eindeutig ist" gilt immer noch):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

Alternativ könnten wir RHS-Funktionen akzeptieren, deren Rückgabetypen denen der äußeren Funktion entsprechen, wie folgt:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

Und schließlich, wenn wir wirklich wollen, können wir dies verallgemeinern, um mit mehr als nur Fehlern zu arbeiten, indem wir den Argumenttyp ändern:

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

...Was ich persönlich sehr nützlich finden würde, wenn ich etwas Schreckliches tun muss, wie zum Beispiel ein map[string]interface{} Hand zu entschlüsseln.

Um das klarzustellen, zeige ich die Erweiterungen hauptsächlich als Beispiele. Ich bin mir nicht sicher, welche von ihnen (wenn überhaupt) eine gute Balance zwischen Einfachheit, Klarheit und allgemeinem Nutzen finden.

Ich möchte den Teil "Fehler zurückgeben / mit zusätzlichem Kontext" noch einmal aufgreifen, da ich davon ausgehen, dass das Ignorieren des Fehlers bereits durch das bereits vorhandene _ abgedeckt ist.

Ich schlage ein aus zwei Wörtern bestehendes Schlüsselwort vor, dem (optional) eine Zeichenfolge folgen kann.

@urandom der erste teil deines Vorschlags ist akzeptabel, man könnte immer damit beginnen und das BubbleProcessor für eine zweite Überarbeitung belassen . Die von @object88 geäußerten Bedenken sind IMO gültig; Ich habe kürzlich Ratschläge wie "Sie sollten den Standard-Client/Transport von http nicht überschreiben" gesehen, dies würde ein weiterer davon werden.

Es gab viele Vorschläge in diesem Thread. Für mich selbst gesprochen habe ich immer noch nicht das Gefühl, dass ich das zu lösende Problem/die zu lösenden Probleme richtig verstanden habe. Ich würde gerne mehr über sie hören.

Ich würde mich auch freuen, wenn jemand die wenig beneidenswerte, aber wichtige Aufgabe übernehmen möchte, eine organisierte und zusammengefasste Liste der besprochenen Probleme zu führen.

Könntest du @ianlancetaylor dich ernennt? :blush: Ich weiß nicht, wie andere Themen geplant/diskutiert werden, aber vielleicht wird diese Diskussion nur als "Box mit Vorschlägen" verwendet?

@KamyarM

@bcmills Sie können den dort vorgeschlagenen Code zählen. Ich denke, das sind 6 Codezeilen anstelle von einer, und wahrscheinlich erhalten Sie dafür einige zyklomatische Komplexitätspunkte für den Code (Ihr verwendet Linter. oder?).

Das Ausblenden der zyklomatischen Komplexität macht es schwieriger, sie zu erkennen, aber es entfernt sie nicht (erinnern Sie sich an strlen ?). Genauso wie das "Verkürzen" der Fehlerbehandlung es einfacher macht, die Semantik der Fehlerbehandlung zu ignorieren - aber schwerer zu erkennen.

Alle Anweisungen oder Ausdrücke in der Quelle, die die Flusssteuerung umleiten, sollten offensichtlich und knapp sein, aber wenn es sich um eine Entscheidung zwischen offensichtlich oder knapp handelt, sollte in diesem Fall das Offensichtliche bevorzugt werden.

Gut, dass du dieses GO-Idiom-Ding mitgebracht hast. Ich denke, das ist nur eines der Probleme dieser Sprache. Immer wenn eine Änderungsanfrage gestellt wird, sagt jemand, nein, das ist gegen das Go-Idiom. Ich sehe es nur als Entschuldigung, Veränderungen zu widerstehen und neue Ideen zu blockieren.

Es gibt einen Unterschied zwischen neu und nützlich. Glaubst du, dass, weil du eine Idee hast, die bloße Existenz davon Zustimmung verdient? Schauen Sie sich zur Übung bitte den Issue Tracker an und stellen Sie sich Go heute vor, wenn jede einzelne Idee genehmigt wurde, unabhängig davon, was die Community dachte.

Vielleicht glauben Sie, dass Ihre Idee besser ist als die anderen. Hier kommt die Diskussion ins Spiel. Anstatt das Gespräch dazu zu verkommen, darüber zu reden, dass das gesamte System aufgrund von Redewendungen kaputt ist, sprechen Sie Kritik direkt, Punkt für Punkt an oder finden Sie einen Mittelweg zwischen Ihnen und Ihren Kollegen.

@gdm85
Ich habe den Prozessor für eine Art Anpassung des zurückgegebenen Fehlers hinzugefügt. Und obwohl ich glaube, dass es ein bisschen wie die Verwendung des Standard-Loggers ist, da Sie ihn die meiste Zeit verwenden können, habe ich gesagt, dass ich offen für Vorschläge bin. Und um es festzuhalten, ich glaube nicht, dass der Standard-Logger und der Standard-HTTP-Client auch nur aus der Ferne in dieselbe Kategorie fallen.

Ich mag auch den Vorschlag von ? wie in Rust, obwohl ich das immer noch für kryptisch halte). Würde das lesbarer aussehen:

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

Und hoffentlich wird sein Vorschlag auch eine Standardimplementierung einer Funktion ähnlich seinem returnErrorf irgendwo im errors Paket enthalten. Vielleicht errors.Returnf() .

@KamyarM
Sie haben hier bereits Ihre Meinung geäußert und keinen Kommentar oder eine mitfühlende Reaktion auf die Ausnahmeursache erhalten. Ich sehe nicht, was das Wiederholen desselben bewirken soll, außer die anderen Diskussionen zu stören. Und wenn das Ihr Ziel ist, ist es einfach nicht cool.

@josharian , ich versuche die Diskussion kurz zusammenzufassen. Es wird voreingenommen sein, da ich einen Vorschlag in der Mischung habe, und unvollständig, da ich nicht in der Lage bin, den gesamten Thread erneut zu lesen.

Das Problem, das wir zu beheben versuchen, ist die visuelle Unordnung, die durch die Go-Fehlerbehandlung verursacht wird. Hier ist ein gutes Beispiel ( Quelle ):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

Mehrere Kommentatoren in diesem Thread glauben nicht, dass dies behoben werden muss; Sie sind froh, dass die Fehlerbehandlung aufdringlich ist, denn der Umgang mit Fehlern ist genauso wichtig wie der Umgang mit dem Nicht-Fehler-Fall. Für sie lohnt sich keiner der Vorschläge hier.

Vorschläge, die versuchen, Code wie diesen zu vereinfachen, teilen sich in einige Gruppen auf.

Einige schlagen eine Form der Ausnahmebehandlung vor. Angesichts der Tatsache, dass Go am Anfang die Ausnahmebehandlung hätte wählen können und sich dagegen entschieden hat, scheint es unwahrscheinlich, dass diese akzeptiert werden.

Viele der Vorschläge hier wählen eine Standardaktion, wie das Zurückkehren von der Funktion (der ursprüngliche Vorschlag) oder Panik, und schlagen eine Syntax vor, mit der sich diese Aktion leicht ausdrücken lässt. Meiner Meinung nach scheitern all diese Vorschläge, weil sie eine Aktion auf Kosten anderer privilegieren. Ich verwende regelmäßig Rückgaben, t.Fatal und log.Fatal , um Fehler zu behandeln, manchmal alles am selben Tag.

Andere Vorschläge bieten keine Möglichkeit, den ursprünglichen Fehler zu erweitern oder einzuschließen, oder erschweren das Einschließen erheblich. Diese sind ebenfalls unzureichend, da das Umschließen die einzige Möglichkeit ist, Fehlern Kontext hinzuzufügen, und wenn wir das Überspringen zu einfach machen, wird es noch seltener als jetzt durchgeführt.

Die meisten der verbleibenden Vorschläge fügen etwas Zucker und manchmal ein bisschen Magie hinzu, um die Dinge zu vereinfachen, ohne die möglichen Aktionen oder die Fähigkeit zum Umwickeln einzuschränken. Die Vorschläge von @bcmills fügen eine minimale Menge an Zucker und null Magie hinzu, um die Lesbarkeit leicht zu erhöhen und auch um einen bösen Fehler zu vermeiden.

Einige andere Vorschläge fügen eine Art eingeschränkten nicht-lokalen Kontrollfluss hinzu, wie einen Abschnitt zur Fehlerbehandlung am Anfang oder Ende einer Funktion.

Nicht zuletzt erkennt @mpvl , dass die Fehlerbehandlung bei Panik sehr schwierig werden kann. Er schlägt eine radikalere Änderung der Go-Fehlerbehandlung vor, um die Korrektheit und Lesbarkeit zu verbessern. Er hat ein überzeugendes Argument, aber am Ende denke ich, dass seine Fälle keine drastischen Änderungen erfordern und mit bestehenden Mechanismen gehandhabt werden können.

Entschuldigung an alle, deren Ideen hier nicht vertreten sind.

Ich habe das Gefühl, dass mich jemand nach dem Unterschied zwischen Zucker und Magie fragen wird. (Das frage ich mich selbst.)

Sugar ist eine Syntax, die Code verkürzt, ohne die Regeln der Sprache grundlegend zu ändern. Der kurze Zuweisungsoperator := ist Zucker. Ebenso der ternäre Operator ?: C

Magie ist eine heftigere Störung der Sprache, wie das Einführen einer Variablen in einen Gültigkeitsbereich, ohne sie zu deklarieren, oder eine nicht-lokale Kontrollübertragung durchzuführen.

Die Linie ist definitiv verschwommen.

Danke dafür, @jba. Sehr hilfreich. Um nur die Highlights herauszuheben, die bisher identifizierten Probleme sind:

die visuelle Unordnung, die durch die Go-Fehlerbehandlung verursacht wird

und

Fehlerbehandlung kann bei Panik sehr knifflig werden

Wenn es andere grundlegend andere Probleme (keine Lösungen) gibt, die @jba und ich übersehen haben,

@josharian Möchten Sie die Scoping-Probleme (https://github.com/golang/go/issues/21161#issuecomment-319277657) als eine Variante des „visuellen Clutter“-Problems oder als separates Problem betrachten?

@bcmills scheint mir anders zu sein, da es um subtile Korrektheitsprobleme geht, im Gegensatz zu Ästhetik / Ergonomie (oder höchstens Korrektheitsproblemen mit Code-Massen). Danke! Möchten Sie meinen Kommentar bearbeiten und eine einzeilige Zusammenfassung davon hinzufügen?

Ich habe das Gefühl, dass mich jemand nach dem Unterschied zwischen Zucker und Magie fragen wird. (Das frage ich mich selbst.)

Ich verwende diese Definition von Magie: Betrachten Sie ein wenig Quellcode, wenn Sie herausfinden können, was er durch eine Variante des folgenden Algorithmus tun soll:

  1. Schlagen Sie alle Bezeichner, Schlüsselwörter und Grammatikkonstrukte nach, die in der Zeile oder in der Funktion vorhanden sind.
  2. Informationen zu Grammatikkonstrukten und Schlüsselwörtern finden Sie in der offiziellen Sprachdokumentation.
  3. Für Identifikatoren sollte es einen klaren Mechanismus geben, um sie anhand der Informationen im betrachteten Code zu finden, unter Verwendung der Bereiche, in denen sich der Code derzeit befindet, wie durch die Sprache definiert, aus der Sie die Definition des Identifikators erhalten können zur Laufzeit genau sein.

Wenn dieser Algorithmus _zuverlässig_ das richtige Verständnis dafür liefert, was der Code tun wird, ist das nicht magisch. Wenn dies nicht der Fall ist, steckt eine gewisse Magie darin. Wie rekursiv Sie es anwenden müssen, wenn Sie versuchen, Dokumentationsreferenzen und Bezeichnerdefinitionen bis hin zu anderen Bezeichnerdefinitionen zu verfolgen, beeinflusst die _Komplexität_, aber nicht die _Magie_ der fraglichen Konstrukte/Codes.

Beispiele für Magie sind: Bezeichner ohne klaren Pfad zurück zu ihrem Ursprung, weil Sie sie ohne Namensraum importiert haben (Punktimporte in Go, insbesondere wenn Sie mehr als einen haben). Jede Fähigkeit, die eine Sprache haben kann, um nicht lokal zu definieren, was ein Operator auflösen wird, beispielsweise in dynamischen Sprachen, in denen Code eine Funktionsreferenz nicht lokal vollständig neu definieren oder neu definieren kann, was die Sprache für nicht vorhandene Bezeichner tut. Objekte, die durch Schemas konstruiert werden, die zur Laufzeit aus einer Datenbank geladen werden, so dass man zur Code-Zeit mehr oder weniger blind hofft, dass sie dort sein werden.

Das Schöne daran ist, dass damit fast die gesamte Subjektivität außer Frage gestellt wird.

Zurück zum aktuellen Thema, es scheint, als ob bereits eine Menge Vorschläge gemacht wurden, und die Chancen stehen, dass jemand anderes dies mit einem anderen Vorschlag löst, der alle dazu bringt, "Ja! Das ist es!" gegen Null gehen.

Mir scheint, das Gespräch sollte vielleicht dahin gehen, die verschiedenen Dimensionen der hier gemachten Vorschläge zu kategorisieren und ein Gefühl für die Priorisierung zu bekommen. Dies möchte ich vor allem im Hinblick darauf sehen, dass widersprüchliche Anforderungen von den Leuten hier vage gestellt werden.

Ich habe zum Beispiel einige Beschwerden über das Hinzufügen zusätzlicher Sprünge im Kontrollfluss gesehen. Aber für mich selbst schätze ich im Sprachgebrauch des sehr ursprünglichen Vorschlags, dass ich || &PathError{"chdir", dir, err} achtmal innerhalb einer Funktion hinzufügen muss, wenn sie üblich sind. (Ich weiß, dass Go nicht so allergisch auf wiederholten Code reagiert wie einige andere Sprachen, aber wiederholter Code birgt ein sehr hohes Risiko für Divergenzfehler.) Aber per Definition, wenn es einen Mechanismus zum Ausschließen einer solchen Fehlerbehandlung gibt, der Code kann nicht ohne Sprünge von oben nach unten, von links nach rechts fließen. Was wird allgemein als wichtiger angesehen? Ich vermute, dass eine sorgfältige Prüfung der Anforderungen, die die Leute implizit an den Code stellen, andere sich gegenseitig widersprechende Anforderungen aufdecken würde.

Aber ganz allgemein habe ich das Gefühl, dass, wenn sich die Community nach all dieser Analyse auf die Anforderungen einigen könnte, die richtige Lösung möglicherweise einfach klar herausfällt oder zumindest die richtige Lösungsmenge so offensichtlich eingeschränkt wird, dass das Problem wird handhabbar.

(Da es sich um einen Vorschlag handelt, möchte ich auch darauf hinweisen, dass das aktuelle Verhalten im Allgemeinen der gleichen Analyse wie die neuen Vorschläge unterzogen werden sollte. Das Ziel ist eine signifikante Verbesserung, nicht eine Perfektion; zwei oder drei signifikante Verbesserungen abzulehnen, weil keine davon perfekt sind, ist ein Weg zur Lähmung.Alle Vorschläge sind sowieso abwärtskompatibel, also in den Fällen, in denen der aktuelle Ansatz ohnehin schon am besten ist (imho, der Fall, in dem jeder Fehler legitimerweise anders behandelt wird, was meiner Erfahrung nach selten ist, aber vorkommt) , der aktuelle Ansatz wird weiterhin verfügbar sein.)

Ich habe darüber nachgedacht, seit ich zum zweiten Mal if err !=nil in einer Funktion geschrieben habe die Bedingung versagt, wir kehren nicht zurück.

Ich bin mir nicht sicher, wie gut dies in Bezug auf das Parsen/Kompilieren funktionieren würde, aber anscheinend sollte es einfach genug sein, um es als if-Anweisung zu interpretieren, bei der das '?' wird gesehen, ohne die Kompatibilität zu unterbrechen, wo es nicht gesehen wird, also dachte ich, ich würde es als Option wegwerfen.

Darüber hinaus gäbe es dafür andere Verwendungen als die Fehlerbehandlung.

also könntest du sowas machen:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

Wir könnten auch solche Dinge tun, wenn wir etwas Bereinigungscode haben, vorausgesetzt, handleErr hat einen Fehler zurückgegeben:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

Vielleicht folgt dann auch noch, dass man das auf einen Einzeiler reduzieren könnte, wenn man wollte:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

Das vorherige Abrufbeispiel von

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Wäre an Reaktionen auf diesen Vorschlag interessiert, vielleicht kein großer Gewinn bei der Einsparung von Boilerplate, bleibt aber ziemlich explizit und erfordert hoffentlich nur eine kleine abwärtskompatible Änderung (vielleicht einige massiv ungenaue Annahmen in dieser Hinsicht).

Könntest du das vielleicht mit einer separaten Rücksendung aussondern? Schlüsselwort, das zur Klarheit beitragen und das Leben einfacher machen kann, da man sich keine Gedanken über die Kompatibilität mit return machen muss (wenn man die ganzen Tools bedenkt), diese könnten dann einfach intern als if/return-Anweisungen umgeschrieben werden, was uns Folgendes liefert:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Da scheint kein großer Unterschied zu sein

return err != nil ? err

und

 if err != nil { return err }

Manchmal möchten Sie vielleicht auch etwas anderes tun als zurückzugeben, z. B. panic oder log.Fatal anrufen.

Ich habe darüber nachgedacht, seit ich letzte Woche einen Vorschlag gemacht habe, und ich bin zu dem Schluss gekommen, dass ich @thejerf zustimme: Wir haben Vorschlag um Vorschlag diskutiert, ohne wirklich einen Schritt zurückzutreten und zu prüfen, was uns gefällt über jeden, Abneigung gegenüber jedem und welche Prioritäten für eine "richtige" Lösung gelten.

Die am häufigsten genannten Anforderungen sind, dass Go am Ende des Tages in der Lage sein muss, 4 Fälle der Fehlerbehandlung zu bewältigen:

  1. Fehler ignorieren
  2. Fehler unverändert zurückgeben
  3. Fehler mit hinzugefügtem Kontext zurückgeben
  4. In Panik geraten (oder das Programm beenden)

Vorschläge scheinen in eine von drei Kategorien zu fallen:

  1. Kehren Sie zu einem try-catch-finally Stil der Fehlerbehandlung zurück.
  2. Fügen Sie neue Syntax/Builins hinzu, um alle 4 oben aufgeführten Fälle zu behandeln
  3. Das Behaupten, dass go behandelt einige Fälle gut genug, und Syntax / Built-Ins vorschlagen, um bei den anderen Fällen zu helfen.

Die Kritik an den gegebenen Vorschlägen scheint zwischen Bedenken hinsichtlich der Lesbarkeit des Codes, nicht offensichtlichen Sprüngen, implizitem Hinzufügen von Variablen zu Gültigkeitsbereichen und Prägnanz aufgeteilt zu sein. Persönlich denke ich, dass es in der Kritik an Vorschlägen viele persönliche Meinungen gegeben hat. Ich sage nicht, dass das schlecht ist, aber mir scheint, dass es kein wirklich objektives Kriterium gibt, um Vorschläge zu bewerten.

Ich bin wahrscheinlich nicht die Person, die versucht, diese Kriterienliste zu erstellen, aber ich denke, es wäre sehr hilfreich, wenn jemand diese zusammenstellen würde. Ich habe versucht, mein bisheriges Verständnis der Debatte zu skizzieren, um 1. das, was wir gesehen haben, 2. was daran falsch ist, 3. warum diese Dinge falsch sind und 4. was wir tun, aufzuschlüsseln stattdessen gerne sehen. Ich denke, ich habe eine anständige Menge der ersten 3 Elemente erfasst, aber ich habe Probleme, eine Antwort für Element 4 zu finden, ohne auf "das, was Go derzeit hat" zurückzugreifen.

@jba hat

@ianlancetaylor oder jemand anderes, der näher an dem Projekt beteiligt ist als ich, würden Sie sich wohl fühlen, einen "formalen" (alles in einem Kommentar, organisiert und etwas umfassenden, aber in keiner Weise verbindlichen) Satz von Kriterien hinzuzufügen, die erfüllt werden müssen? Wenn wir diese Kriterien diskutieren und 4-6 Aufzählungspunkte festlegen, die Vorschläge erfüllen müssen, können wir die Diskussion vielleicht mit etwas mehr Kontext neu starten?

Ich glaube nicht, dass ich einen umfassenden formalen Kriterienkatalog schreiben kann. Das Beste, was ich tun kann, ist eine unvollständige Liste von Dingen, die wichtig sind, die nur ignoriert werden sollten, wenn dies einen erheblichen Vorteil bietet.

  • Gute Unterstützung für 1) das Ignorieren eines Fehlers; 2) Zurückgeben eines Fehlers unverändert; 3) Umschließen eines Fehlers mit zusätzlichem Kontext.
  • Obwohl der Fehlerbehandlungscode klar sein sollte, sollte er die Funktion nicht dominieren. Der Code zur Fehlerbehandlung sollte leicht zu lesen sein.
  • Vorhandener Go 1-Code sollte weiterhin funktionieren, oder es muss zumindest möglich sein, Go 1 mit absoluter Zuverlässigkeit mechanisch auf den neuen Ansatz zu übersetzen.
  • Der neue Ansatz soll Programmierer ermutigen, Fehler richtig zu behandeln. Im Idealfall sollte es einfach sein, das Richtige zu tun, was immer das Richtige in jeder Situation ist.
  • Jeder neue Ansatz sollte kürzer und/oder weniger repetitiv sein als der aktuelle Ansatz, dabei aber klar bleiben.
  • Die Sprache funktioniert heute und jede Änderung ist mit Kosten verbunden. Der Nutzen der Änderung muss die Kosten eindeutig wert sein. Es sollte nicht nur eine Wäsche sein, es sollte deutlich besser sein.

Ich hoffe, die Notizen irgendwann hier selbst sammeln zu können, möchte aber auf einen IMHO weiteren großen Stolperstein in dieser Diskussion eingehen, die Trivialität der Beispiele.

Ich habe dies aus einem echten Projekt von mir extrahiert und für die externe Veröffentlichung bereinigt. (Ich denke, die stdlib ist nicht die beste Quelle, da sie unter anderem Protokollierungsprobleme vermisst.)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(Lassen Sie uns den Code nicht zu sehr auf den Kopf stellen. Ich kann Sie natürlich nicht aufhalten, aber denken Sie daran, dass dies nicht mein echter Code ist und ein wenig verstümmelt wurde. Und ich kann Sie nicht davon abhalten, Ihren eigenen Beispielcode zu posten.)

Beobachtungen:

  1. Dies ist kein perfekter Code; Ich gebe oft nackte Fehler zurück, weil es so einfach ist, genau die Art von Code, mit der wir Probleme haben. Vorschläge sollten sowohl nach ihrer Prägnanz als auch danach bewertet werden, wie leicht sie die Behebung dieses Codes nachweisen können.
  2. Die if forwardPort == 0 Klausel wird _absichtlich_ durch Fehler fortgesetzt, und ja, das ist das wahre Verhalten, nicht etwas, das ich für dieses Beispiel hinzugefügt habe.
  3. Dieser Code gibt ENTWEDER einen gültigen, verbundenen Client zurück ODER er gibt einen Fehler und keine Ressourcenlecks zurück, daher ist die Behandlung von .Close() (nur wenn die Funktion fehlerhaft ist) beabsichtigt. Beachten Sie auch, dass die Fehler von Close verschwinden, wie es in echtem Go ganz typisch ist.
  4. Die Portnummer ist an anderer Stelle eingeschränkt, sodass url.Parse (durch Prüfung) nicht fehlschlagen kann.

Ich würde nicht behaupten, dass dies jedes mögliche Fehlerverhalten demonstriert, aber es deckt eine ganze Bandbreite ab. (Ich verteidige Go on HN und so oft, indem ich darauf hinweist, dass, wenn mein Code mit dem Kochen fertig ist, es auf meinen Netzwerkservern oft der Fall ist, dass ich _alle Arten_ von Fehlerverhalten habe; untersuche meinen eigenen Produktionscode von 1/3 bis die Hälfte der Fehler etwas anderes getan hat, als einfach zurückgegeben zu werden.)

Ich werde auch meinen eigenen (aktualisierten) Vorschlag, der auf diesen Code angewendet wird, (erneut) posten (es sei denn, jemand überzeugt mich, dass er vorher etwas noch Besseres hat), aber um das Gespräch nicht zu monopolisieren, werde ich warten zumindest am Wochenende. (Dies ist weniger Text, als es zu sein scheint, da es sich um einen großen Teil der Quelle handelt, aber trotzdem ....)

Die Verwendung von try wobei try nur eine Abkürzung für if != nil Return ist, reduziert den Code um 6 von 59 Zeilen, was etwa 10 % entspricht.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

Bemerkenswerterweise wollte ich an mehreren Stellen try x() schreiben, aber ich konnte nicht, da ich err setzen musste, damit die Verzögerungen richtig funktionieren.

Einer noch. Wenn try etwas ist, das auf Zuweisungszeilen passieren kann, geht es auf 47 Zeilen zurück.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

Leicht magisch (fühlen Sie sich frei, -1), aber unterstützt mechanische Übersetzung

So (etwas aktualisiert) würde mein Vorschlag aussehen:

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

Definitionen:

pop ist für Funktionsausdrücke zulässig, bei denen der Wert ganz rechts ein Fehler ist. Es ist definiert als "wenn der Fehler nicht Null ist, kehre aus der Funktion mit den Nullwerten für alle anderen Werte in den Ergebnissen und diesem Fehler zurück, andernfalls erzeuge einen Satz von Werten ohne den Fehler". pop hat keine privilegierte Interaktion mit erroring() ; eine normale Rückgabe eines Fehlers ist für erroring() weiterhin sichtbar. Dies bedeutet auch, dass Sie für die anderen Rückgabewerte Werte ungleich Null zurückgeben und trotzdem die verzögerte Fehlerbehandlung verwenden können. Die Metapher besteht darin, das Element ganz rechts aus der "Liste" der Rückgabewerte zu entfernen.

erroring() ist so definiert, dass es den Stack hinauf zur ausgeführten verzögerten Funktion geht und dann zum vorherigen Stack-Element (die Funktion, in der die Verzögerung ausgeführt wird, in diesem Fall NewClient), um aktuell auf den Wert des zurückgegebenen Fehlers zuzugreifen in Bearbeitung. Wenn die Funktion keinen solchen Parameter hat, geben Sie entweder panic oder nil zurück (je nachdem, was sinnvoller ist). Dieser Fehlerwert muss nicht von pop ; es ist alles, was einen Fehler von der Zielfunktion zurückgibt.

seterr(error) können Sie den zurückgegebenen Fehlerwert ändern. Es wird dann der Fehler sein, der bei allen zukünftigen erroring() Aufrufen angezeigt wird, was, wie hier gezeigt, dieselbe auf Verzögerung basierende Verkettung ermöglicht, die jetzt durchgeführt werden kann.

Ich verwende hier das Hashicorp-Wrapping und Multierror; fügen Sie nach Belieben Ihre eigenen cleveren Pakete ein.

Selbst mit der definierten Zusatzfunktion ist die Summe kürzer. Ich erwarte, dass sich die beiden Funktionen über zusätzliche Verwendungen hinweg amortisieren, daher sollten diese nur teilweise zählen.

Beachten Sie, dass ich die ForwardPort-Behandlung einfach in Ruhe lasse, anstatt zu versuchen, mehr Syntax darum herum zu stauen. Ausnahmsweise ist es in Ordnung, wenn dies ausführlicher ist.

Das Interessanteste an diesem Vorschlag kann IMHO nur gesehen werden, wenn man sich vorstellt, dies mit herkömmlichen Ausnahmen zu schreiben. Es endet damit, dass es ziemlich tief verschachtelt wird, und die Behandlung der _Sammlung_ der Fehler, die auftreten können, ist bei der Ausnahmebehandlung ziemlich mühsam. (So ​​wie in echtem Go-Code die .Close-Fehler in der Regel ignoriert werden, werden Fehler, die in den Ausnahmehandlern selbst auftreten, in ausnahmebasiertem Code ignoriert.)

Dies erweitert bestehende Go-Muster wie defer und die Verwendung von Fehlern-als-Werten, um einfache korrekte Fehlerbehandlungsmuster zu erstellen, die in einigen Fällen entweder mit dem aktuellen Go oder mit Ausnahmen schwer auszudrücken sind, erfordert keine radikale Operation zur Laufzeit (glaube ich nicht) und außerdem _erfordert_ eigentlich kein Go 2.0.

Zu den Nachteilen gehören die Angabe von erroring , pop und seterr als Schlüsselwörter, wodurch der Overhead von defer für diese Funktionalitäten entsteht, die Tatsache, dass die faktorisierte Fehlerbehandlung einiges umgeht zu den Handhabungsfunktionen, und dass es nichts tut, um eine korrekte Handhabung zu "erzwingen". Obwohl ich mir nicht sicher bin, ob letzteres möglich ist, da Sie durch die (richtige) Anforderung, abwärtskompatibel zu sein, immer das aktuelle tun können.

Sehr interessante Diskussion hier.

Ich möchte die Fehlervariable auf der linken Seite belassen, damit keine magisch erscheinenden Variablen eingeführt werden. Wie der ursprüngliche Vorschlag möchte ich die Behandlung des Fehlerzeugs auf der gleichen Linie. Den || -Operator würde ich nicht verwenden, da er für mich "zu boolesch" aussieht und irgendwie die "Rückgabe" versteckt.

Ich würde es also mit dem erweiterten Schlüsselwort "return?" besser lesbar machen. In C# wird das Fragezeichen an einigen Stellen verwendet, um Verknüpfungen zu erstellen. Z.B. statt zu schreiben:

if(foo != null)
{ foo.Bar(); }

du kannst einfach schreiben:
foo?.Bar();

Für Go 2 möchte ich diese Lösung vorschlagen:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

Nur ein Gedanke:

foo, err := myFunc()
err != null ? Rücksendung(err)

Oder

wenn err != null ? Rücksendung(err)

Wenn Sie bereit sind, ein paar geschweifte Klammern zu setzen, müssen wir nichts ändern!

if err != nil { return wrap(err) }

Sie können _alle_ die gewünschte benutzerdefinierte Behandlung haben (oder überhaupt keine), Sie haben zwei Codezeilen aus dem typischen Fall gespeichert, es ist 100% abwärtskompatibel (da es keine Änderungen an der Sprache gibt), es ist kompakter und es ist leicht. Es trifft viele von Ians treibenden Punkten . Die einzige Änderung könnte die gofmt Werkzeugausstattung sein?

Ich habe dies geschrieben, bevor ich Carlmjohnsons Vorschlag gelesen habe, der ähnlich ist ...

Nur ein # vor einem Fehler.

Aber in einer realen Welt Anwendung würden Sie noch schreiben müssen die normalen if err != nil { ... } , so dass Sie Fehler protokollieren kann, das macht den minimalistischen Fehler nutzlos Handhabung, es sei denn Sie eine Rückkehr Middleware durch Anmerkungen hinzufügen könnte genannt after , die ausgeführt wird, nachdem die Funktion zurückkehrt... (wie defer aber mit Argumenten).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

sauberer als:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

Wie wäre es mit einer Swift-Anweisung wie guard , außer dass es statt guard...else guard...return :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Ich mag die Deutlichkeit der go-Fehlerbehandlung. Das einzige Problem, imho, ist, wie viel Platz es braucht. Ich würde 2 Optimierungen vorschlagen:

  1. erlauben die Verwendung von nilfähigen Elementen in einem booleschen Kontext, wobei nil false , Nicht- nil true
  2. unterstützen einzeilige bedingte Anweisungsoperatoren wie && und ||

So

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

kann werden

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

oder noch kürzer

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

und wir können es tun

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

oder vielleicht

i,ok := v.(int)
ok && s *= i

Wenn das Überladen von && und || zu viel Mehrdeutigkeit erzeugt, können möglicherweise einige andere Zeichen (nicht mehr als 2) ausgewählt werden, z. B. ? und # oder ?? und ## oder ?? und !! , was auch immer. Der Punkt besteht darin, eine einzeilige bedingte Anweisung mit minimalen "verrauschten" Zeichen zu unterstützen (keine Klammern, geschweifte Klammern usw. erforderlich). Die Operatoren && und || sind gut, da diese Verwendung in anderen Sprachen einen Präzedenzfall hat.

Dies ist kein Vorschlag zur Unterstützung komplexer einzeiliger bedingter Ausdrücke , sondern nur einzeiliger bedingter Anweisungen.

Außerdem ist dies kein Vorschlag, eine vollständige Palette von "Wahrheit" wie in einigen anderen Sprachen zu unterstützen. Diese Bedingungen würden nur nil/non-nil oder boolesche Werte unterstützen.

Für diese bedingten Operatoren kann es sogar sinnvoll sein, sich auf einzelne Variablen zu beschränken und Ausdrücke nicht zu unterstützen. Alles, was komplexer ist oder eine else Klausel enthält, würde mit standardmäßigen if ... Konstrukten behandelt.

Warum nicht einfach ein Rad erfinden und das bekannte try..catch Formular verwenden, wie @mattn schon sagte? )

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Es scheint keinen Grund zu geben, die abgefangene Fehlerquelle zu unterscheiden, denn wenn Sie diese wirklich benötigen, können Sie immer die alte Form von if err != nil ohne try..catch .

Auch bin ich mir da nicht wirklich sicher, aber kann man vielleicht die Möglichkeit hinzufügen, einen Fehler "auszuwerfen", wenn er nicht behandelt wird?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas aus Sicht der Lesbarkeit ist es sehr wichtig, den Kontrollfluss vollständig zu verstehen. Wenn Sie sich Ihr Beispiel ansehen, können Sie nicht sagen, welche Zeile einen Sprung zum Catch-Block verursachen könnte. Es ist wie eine versteckte goto Anweisung. Kein Wunder, dass moderne Sprachen versuchen, dies explizit zu machen und vom Programmierer verlangen, explizit Stellen zu markieren, an denen der Kontrollfluss aufgrund eines ausgelösten Fehlers abweichen könnte. Ziemlich ähnlich wie return oder goto aber mit viel schönerer Syntax.

@creker ja, da stimme ich dir

Vielleicht sowas wie:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Oder andere Vorschläge wie try a := foo() ..?

@gobwas

Wenn ich im catch-Block lande, woher weiß ich, welche Funktion im try-Block den Fehler verursacht hat?

@urandom Wenn Sie es wissen müssen, möchten Sie wahrscheinlich if err != nil ohne try..catch tun.

@robert-wallis: Ich habe die Guard-Anweisung von Swift bereits früher im Thread erwähnt, aber die Seite ist so groß, dass Github sie nicht mehr standardmäßig lädt. :-PI finde das immer noch eine gute Idee, und im Allgemeinen unterstütze ich es, andere Sprachen nach positiven/negativen Beispielen zu suchen.

@pdk

zulassen, dass nil-fähige Elemente in einem booleschen Kontext verwendet werden, wobei nil gleich false ist, nicht-nil gleich true

Ich sehe, dass dies zu vielen Fehlern führt, wenn das Flag-Paket verwendet wird, if myflag { ... } dem die Leute if *myflag { ... } schreiben wollen und es vom Compiler nicht abgefangen wird.

try/catch ist nur kürzer als if/else, wenn Sie mehrere Dinge hintereinander versuchen, was mehr oder weniger jeder zustimmt, dass es aufgrund von Kontrollflussproblemen usw. schlecht ist.

FWIW, Swifts try/catch löst zumindest das visuelle Problem, nicht zu wissen, welche Anweisungen ausgelöst werden könnten:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@robert-wallis , du hast ein Beispiel:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Bei der ersten Verwendung von guard sieht es sehr nach if err != nil { return &FooError{"Couldn't foo fails.txt", err}} , also bin ich mir nicht sicher, ob das ein großer Gewinn ist.

Bei der zweiten Verwendung ist nicht sofort klar, woher das err kommt. Es sieht fast so aus, als ob es das ist, was von os.Open , was vermutlich nicht Ihre Absicht war? Wäre das genauer?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

In diesem Fall sieht es so aus...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

Aber es hat immer noch weniger visuelle Unordnung. if err = , ; err != nil { - auch wenn es ein Einzeiler ist, für so eine einfache Sache ist immer noch zu viel los

Einverstanden, dass es weniger Unordnung gibt. Aber deutlich weniger, um eine sprachliche Ergänzung zu rechtfertigen? Ich bin mir nicht sicher, ob ich da zustimme.

Ich denke, dass die Lesbarkeit von Try-Catch-Blöcken in Java/C#/... sehr gut ist, da man der "Happy Path"-Sequenz ohne Unterbrechung durch die Fehlerbehandlung folgen kann. Der Nachteil ist, dass Sie im Grunde einen versteckten Goto-Mechanismus haben.

In Go fange ich an, nach der Fehlerbehandlung Leerzeilen einzufügen, um die Fortsetzung der "Happy Path"-Logik sichtbarer zu machen. Also aus diesem Beispiel von golang.org (9 Zeilen)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

das mache ich oft (11 Zeilen)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Nun zurück zum Vorschlag, da ich so etwas schon gepostet habe wäre schön (3 Zeilen)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

Jetzt sehe ich den glücklichen Weg deutlich. Meine Augen sind sich immer noch bewusst, dass auf der rechten Seite Code für die Fehlerbehandlung ist, aber ich muss ihn nur "anschauen", wenn es wirklich notwendig ist.

Eine Frage an alle nebenbei: soll dieser Code kompilieren?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

IMHO sollte der Benutzer gezwungen sein zu sagen, dass er den Fehler absichtlich ignoriert mit:

func main() {
    _ := foobar()
}

Fügen Sie einen Mini-Erfahrungsbericht zu Punkt 3 des ursprünglichen Beitrags von @ianlancetaylor hinzu und geben Sie den Fehler mit zusätzlichen Kontextinformationen zurück .

Bei der Entwicklung einer flac-Bibliothek für Go wollten wir mithilfe des @davecheney-Pakets pkg/errors (https://github.com/mewkiz/flac/issues/22) Kontextinformationen zu Fehlern hinzufügen . Genauer gesagt umschließen wir Fehler, die mit "

Da der Fehler mit Anmerkungen versehen ist, muss ein neuer zugrunde liegender Typ erstellt werden, um diese zusätzlichen Informationen im Fall von error.WithStack zu speichern, der Typ ist error.withStack .

type withStack struct {
    error
    *stack
}

Um den ursprünglichen Fehler normalerweise error.Cause . Auf diese Weise können Sie den ursprünglichen Fehler beispielsweise mit io.EOF .

Ein Benutzer der Bibliothek kann dann etwas in der Art von https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 mit errors.Cause schreiben, um den ursprünglichen Fehler zu überprüfen Wert:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

Das funktioniert in fast allen Fällen gut.

Als wir unsere Fehlerbehandlung so umgestalteten, dass pkg/errors durchgehend für zusätzliche Kontextinformationen verwendet wurden, stießen wir jedoch auf ein ziemlich ernstes Problem. Um das Zero-Padding zu validieren, haben wir einen io.Reader implementiert, der einfach überprüft, ob die gelesenen Bytes null sind, und ansonsten einen Fehler meldet. Das Problem ist, dass unsere Testfälle plötzlich fehlschlugen , nachdem wir ein automatisches Refactoring durchgeführt hatten, um unseren Fehlern kontextbezogene Informationen hinzuzufügen.

Das Problem war, dass der zugrunde liegende Fehlertyp, der von zeros.Read jetzt error.withStack und nicht mehr io.EOF ist. Dies führte später zu Problemen, als wir diesen Reader in Kombination mit io.Copy , der speziell nach io.EOF sucht und nicht weiß, wie man errors.Cause , um einen mit annotierten Fehler zu "entpacken". Kontextinformationen. Da wir die Standardbibliothek nicht aktualisieren können, bestand die Lösung darin, den Fehler ohne kommentierte Informationen zurückzugeben (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

Der Verlust der annotierten Informationen für Schnittstellen, die konkrete Werte zurückgeben, ist zwar ein Verlust, aber damit kann man leben.

Unsere Erfahrung ist, dass wir Glück hatten, denn unsere Testfälle haben dies festgestellt. Der Compiler hat keine Fehler erzeugt, da der Typ zeros immer noch die Schnittstelle io.Reader implementiert. Wir dachten auch nicht, dass wir auf ein Problem stoßen würden, da die hinzugefügte Fehleranmerkung ein maschinell generiertes Umschreiben war. Fügen Sie einfach Kontextinformationen zu Fehlern hinzu, sollte das Programmverhalten im Normalzustand nicht beeinträchtigt werden.

Aber es tat es, und aus diesem Grund möchten wir unseren Erfahrungsbericht zur Überlegung beisteuern; wenn Sie darüber nachdenken, wie Sie das Hinzufügen von Kontextinformationen in die Fehlerbehandlung für Go 2 integrieren können, sodass der Fehlervergleich (wie in Schnittstellenverträgen verwendet) immer noch nahtlos funktioniert.

Freundlich,
Robin

@mewmew , lassen Sie uns dieses Problem zu den Kontrollflussaspekten der Fehlerbehandlung

Ich kenne Ihre Codebasis nicht und mir ist bewusst, dass Sie sagten, dass es sich um eine automatisierte Umgestaltung handelte, aber warum mussten Sie Kontextinformationen in EOF einschließen? Obwohl es vom Typsystem als Fehler behandelt wird, ist EOF eher ein Signalwert, kein tatsächlicher Fehler. Insbesondere in einer io.Reader Implementierung ist dies meistens ein erwarteter Wert. Wäre es nicht besser gewesen, den Fehler nur einzuschließen, wenn er nicht io.EOF ?

Ja, ich schlage vor, dass wir die Dinge einfach so lassen, wie sie sind. Ich hatte den Eindruck, dass das Fehlersystem von Go absichtlich auf diese Weise entwickelt wurde, um Entwickler davon abzuhalten, Fehler in der Aufrufliste zu platzieren. Dass Fehler dort behoben werden sollen, wo sie auftreten, und zu wissen, wann es angebrachter ist, Panik zu verwenden, wenn Sie nicht können.

Ich meine, ist Try-Catch-Throw nicht im Wesentlichen das gleiche Verhalten von panic() und recovery()?

Seufz, wenn wir wirklich versuchen, diesen Weg einzuschlagen. Warum können wir nicht einfach so etwas machen wie

_, ? := foo()
x?, err? := bar()

oder vielleicht sogar sowas wie

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

bei dem die ? wird ein Kurzalias für if var != nil{ return var }.

Wir können sogar eine andere spezielle eingebaute Schnittstelle definieren, die von der Methode erfüllt wird

func ?() bool //looks funky but avoids breakage.

die wir verwenden können, um das Standardverhalten des neuen und verbesserten Bedingungsoperators im Wesentlichen zu überschreiben.

@mortdeus

Ich glaube, ich stimme zu.
Wenn das Problem darin besteht, den glücklichen Pfad auf schöne Weise darzustellen, könnte ein Plugin für eine IDE jede Instanz von if err != nil { return [...] } mit einer Verknüpfung ein-/ausklappen?

Ich habe das Gefühl, dass jetzt jeder Teil wichtig ist. err != nil ist wichtig. return ... ist wichtig.
Es ist ein bisschen mühsam zu schreiben, aber es muss geschrieben werden. Und bremst es die Leute wirklich aus? Was Zeit braucht, ist, über den Fehler nachzudenken und was zurückzugeben, anstatt ihn zu schreiben.

Ich wäre viel mehr an einem Vorschlag interessiert, der es ermöglicht, den Umfang der Variablen err einzuschränken.

Ich denke, meine bedingte Idee ist der sauberste Weg, dieses Problem zu lösen. Ich habe gerade an ein paar andere Dinge gedacht, die diese Funktion würdig genug machen würden, um sie in Go aufzunehmen. Ich werde meine Idee in einem separaten Vorschlag niederschreiben.

Ich sehe nicht, wie das funktionieren könnte:

x, y, err := baz(); zurück ( x? && y? ) || err?

bei dem die ? wird ein Kurzalias für if var == nil{ return var }.

x, y, err := baz(); zurück ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, err := baz(); zurück (x? && y?) || irren?

wird

x, y, err := baz();
if ((x != null && y != null) || err != null)){
Rückgabe x,y, err
}

wenn du x siehst? && j? || irren? Sie sollten denken: "Ist x und y gültig? Was ist mit Fehler?"

Wenn nicht, wird die Rückgabefunktion nicht ausgeführt. Ich habe gerade einen neuen Vorschlag zu dieser Idee geschrieben, der die Idee mit einem neuen speziellen eingebauten Schnittstellentyp ein bisschen weiterführt

Ich schlage vor, dass Go die Standardfehlerbehandlung in Go-Version 2 hinzufügt.

Wenn der Benutzer den Fehler nicht behandelt, gibt der Compiler err zurück, wenn er nicht nil ist, also wenn der Benutzer schreibt:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

kompilieren transformiere es in:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

    err = func2()
    if err != nil {
        return err
    }

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

Wenn der Benutzer den Fehler behandelt oder ihn mit _ ignoriert, wird der Compiler nichts tun:

_ = func1()

oder

err := func1()

für mehrere Rückgabewerte ist es ähnlich:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Compiler wird umgewandelt in:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Wenn die Signatur von Func() keinen Fehler zurückgibt, sondern Funktionen aufruft, die einen Fehler zurückgeben, meldet der Compiler Fehler: "Bitte behandeln Sie Ihren Fehler in Func()"
Dann kann der Benutzer einfach den Fehler in Func() protokollieren

Und wenn der Benutzer einige Informationen in den Fehler einschließen möchte:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

oder

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

Der Vorteil ist,

  1. das Programm scheitert einfach an dem Punkt, an dem ein Fehler auftritt, der Benutzer kann den Fehler nicht implizit ignorieren.
  2. kann Codezeilen deutlich reduzieren.

Es ist nicht so einfach, weil Go mehrere Rückgabewerte haben kann und die Sprache nicht das zuweisen sollte, was im Wesentlichen den Standardwerten für Rückgabeargumente entspricht, ohne dass der Entwickler ausdrücklich weiß, was vor sich geht.

Ich glaube, dass das Einfügen der Fehlerbehandlung in die Zuweisungssyntax nicht die Wurzel des Problems löst, nämlich "Fehlerbehandlung wiederholt sich".

Die Verwendung von if (err != nil) { return nil } (oder ähnlich) nach vielen Codezeilen (wo dies sinnvoll ist) verstößt gegen das DRY-Prinzip (wiederholen Sie sich nicht). Ich glaube, deshalb mögen wir das nicht.

Es gibt auch Probleme mit try ... catch . Sie müssen den Fehler nicht explizit in derselben Funktion behandeln, in der er auftritt. Ich glaube, das ist ein wichtiger Grund, warum wir try...catch nicht mögen.

Ich glaube nicht, dass sich diese gegenseitig ausschließen; wir können eine Art try...catch ohne throws .

Eine andere Sache, die ich persönlich an try...catch mag, ist die willkürliche Notwendigkeit des Schlüsselworts try . Es gibt keinen Grund, warum Sie nicht nach einem Umfangsbegrenzer catch können, was die Arbeitsgrammatik betrifft. (Jemand ruft es heraus, wenn ich falsch liege)

Das schlage ich vor:

  • Verwenden von ? als Platzhalter für einen zurückgegebenen Fehler, wobei _ würde, um ihn zu ignorieren
  • anstelle von catch wie in meinem Beispiel unten könnte stattdessen error? für volle Abwärtskompatibilität verwendet werden

^ Wenn meine Annahme, dass diese abwärtskompatibel sind, falsch ist, rufen Sie es bitte an.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

Mir fiel ein Argument dagegen ein: Wenn Sie es so schreiben, kann das Ändern dieses catch Blocks unbeabsichtigte Auswirkungen auf den Code in tieferen Bereichen haben. Dies ist das gleiche Problem wie bei try...catch .

Ich denke, wenn Sie dies nur im Rahmen einer einzigen Funktion tun können, ist das Risiko überschaubar - möglicherweise genauso wie das aktuelle Risiko, eine Zeile mit Fehlerbehandlungscode zu vergessen, wenn Sie viele davon ändern möchten. Ich sehe dies als den gleichen Unterschied zwischen den Folgen der Wiederverwendung von Code und den Folgen, wenn man DRY nicht befolgt (dh kein kostenloses Mittagessen, wie sie sagen).

Bearbeiten: Ich habe vergessen, ein wichtiges Verhalten für mein Beispiel anzugeben. Für den Fall, dass ? in einem Bereich ohne catch , denke ich, dass dies ein Compilerfehler sein sollte (anstatt eine Panik auszulösen, was zugegebenermaßen das erste war, an das ich gedacht habe).

Bearbeiten 2: Verrückte Idee: Vielleicht würde der catch Block einfach keinen Einfluss auf den Kontrollfluss haben ... es wäre buchstäblich so, als würde man den Code in catch { ... } kopieren und in die Zeile einfügen, nachdem der Fehler aufgetreten ist ? ed (naja, nicht ganz - es hätte immer noch seinen eigenen Geltungsbereich). Es scheint seltsam, da keiner von uns daran gewöhnt ist, also sollte catch definitiv nicht das Schlüsselwort sein, wenn es so gemacht wird, aber ansonsten ... warum nicht?

@mewmew , lassen Sie uns dieses Problem zu den Kontrollflussaspekten der Fehlerbehandlung

Ok, lassen Sie uns diesen Thread behalten, um den Fluss zu kontrollieren. Ich habe es einfach hinzugefügt, weil es ein Problem im Zusammenhang mit der konkreten Verwendung von Punkt 3 war. Geben Sie den Fehler mit zusätzlichen Kontextinformationen zurück .

@jba Kennen Sie ein Problem, das speziell dem

Ich kenne Ihre Codebasis nicht und mir ist bewusst, dass Sie sagten, dass es sich um eine automatisierte Umgestaltung handelte, aber warum mussten Sie Kontextinformationen in EOF einschließen? Obwohl es vom Typsystem als Fehler behandelt wird, ist EOF eher ein Signalwert, kein tatsächlicher Fehler. Insbesondere in einer io.Reader-Implementierung ist dies meistens ein erwarteter Wert. Wäre es nicht besser gewesen, den Fehler nur einzuschließen, wenn es nicht io.EOF wäre?

@DeedleFake Ich kann ein wenig

Je mehr ich alle Vorschläge (einschließlich meiner) lese, desto weniger denke ich, dass wir wirklich ein Problem mit der Fehlerbehandlung in go haben.

Was ich möchte, ist eine Durchsetzung, um einen Fehlerrückgabewert nicht versehentlich zu ignorieren, sondern zumindest durchzusetzen
_ := returnsError()

Ich weiß, dass es Tools gibt, um diese Probleme zu finden, aber ein First-Level-Support der Sprache könnte einige Fehler beheben. Einen Fehler überhaupt nicht zu behandeln ist für mich wie eine ungenutzte Variable zu haben - was bereits ein Fehler ist. Es würde auch beim Refactoring helfen, wenn Sie einen Fehlerrückgabetyp in eine Funktion einführen, da Sie gezwungen sind, ihn an allen Stellen zu behandeln.

Das Hauptproblem, das die meisten Leute hier zu lösen versuchen, scheint die "Anzahl der Eingaben" oder "Anzahl der Zeilen" zu sein. Ich würde jeder Syntax zustimmen, die die Anzahl der Zeilen reduziert, aber das ist meistens ein gofmt-Problem. Erlauben Sie einfach Inline-"Single-Line-Scopes" und wir sind gut.

Ein weiterer Vorschlag, um sich Tipparbeit zu sparen, ist die implizite nil Überprüfung wie bei booleschen Werten:

err := returnsError()
if err { return err }

oder auch

if err := returnsError(); err { return err }

Das würde mit allen Zeigertypen von Cause funktionieren.

Mein Gefühl ist, dass alles, was den Funktionsaufruf + die Fehlerbehandlung auf eine einzige Zeile reduziert, zu weniger lesbarem Code und komplexerer Syntax führt.

weniger lesbarer Code und komplexere Syntax.

Aufgrund der ausführlichen Fehlerbehandlung haben wir bereits weniger lesbaren Code. Das Hinzufügen des bereits erwähnten Scanner-API-Tricks, der diese Ausführlichkeit verbergen soll, macht es noch schlimmer. Das Hinzufügen einer komplexeren Syntax könnte die Lesbarkeit verbessern, wofür syntaktischer Zucker am Ende da ist. Sonst hat diese Diskussion keinen Sinn. Das Muster, einen Fehler zu sprudeln und für alles andere einen Nullwert zurückzugeben, ist meiner Meinung nach üblich genug, um eine Sprachänderung zu rechtfertigen.

Das Muster, einen Fehler zu sprudeln und für alles den Wert Null zurückzugeben

19642 würde dies erleichtern.


Danke auch @mewmew für den Erfahrungsbericht. Es hängt definitiv mit diesem Thread zusammen, insofern es sich auf Gefahren in bestimmten Arten von Fehlerbehandlungsdesigns bezieht. Ich würde gerne mehr davon sehen.

Ich habe das Gefühl, dass ich meine Idee nicht sehr gut erklärt habe, also habe ich eine Zusammenfassung erstellt (und viele der Mängel, die mir gerade aufgefallen sind, überarbeitet)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

Es gibt mehr als eine Idee in diesem Kern, daher hoffe ich, dass sie getrennt voneinander diskutiert werden können

Abgesehen von der Idee, dass der Vorschlag hier explizit auf Fehlerbehandlung abzielen muss, was wäre, wenn Go so etwas wie eine collect Anweisung einführen würde?

Eine collect Anweisung hätte die Form collect [IDENT] [BLOCK STMT] , wobei ident eine im Gültigkeitsbereich enthaltene Variable vom Typ nil -fähig sein muss. Innerhalb einer collect Anweisung steht eine spezielle Variable _! als Alias ​​für die Variable zur Verfügung, in die gesammelt wird. _! kann nur als Zuweisung verwendet werden, genau wie _ . Immer wenn _! zugewiesen wird, wird eine implizite nil Prüfung durchgeführt, und wenn _! nicht null ist, beendet der Block die Ausführung und fährt mit dem Rest des Codes fort.

Theoretisch würde das in etwa so aussehen:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

was äquivalent zu ist

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

Einige nette andere Dinge, die eine Syntaxfunktion wie diese ermöglichen würde:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

Neue Syntaxfunktionen erforderlich:

  1. Stichwort collect
  2. Special Ident _! (Ich habe damit im Parser gespielt, es ist nicht schwer, dieses Match als Ident zu erstellen, ohne etwas anderes zu beschädigen)

Der Grund, warum ich so etwas vorschlage, ist, dass das Argument "Fehlerbehandlung ist zu wiederholt" auf "Null-Prüfungen sind zu wiederholt" reduziert werden kann. Go verfügt bereits über zahlreiche Fehlerbehandlungsfunktionen, die unverändert funktionieren. Sie können einen Fehler mit _ ignorieren (oder einfach keine Rückgabewerte erfassen), Sie können einen Fehler unverändert mit if err != nil { return err } oder Kontext hinzufügen und mit if err != nil { return wrap(err) } . Keine dieser Methoden allein ist zu repetitiv. Die Wiederholung ( offensichtlich ) kommt daher, dass diese oder ähnliche Syntaxanweisungen im gesamten Code wiederholt werden müssen. Ich denke, die Einführung einer Möglichkeit zum Ausführen von Anweisungen, bis ein Wert ungleich Null gefunden wird, ist eine gute Möglichkeit, die Fehlerbehandlung gleich zu halten, aber die dafür erforderliche Menge an Boilerplate zu reduzieren.

  • Gute Unterstützung für 1) das Ignorieren eines Fehlers; 3) Umschließen eines Fehlers mit zusätzlichem Kontext.

check, da bleibt (meistens) gleich

  • Obwohl der Fehlerbehandlungscode klar sein sollte, sollte er die Funktion nicht dominieren.

überprüfen, da der Fehlerbehandlungscode jetzt bei Bedarf an einer Stelle abgelegt werden kann, während der Kern der Funktion linear lesbar erfolgen kann

  • Vorhandener Go 1-Code sollte weiterhin funktionieren, oder es muss zumindest möglich sein, Go 1 mit absoluter Zuverlässigkeit mechanisch auf den neuen Ansatz zu übersetzen.

Check, dies ist eine Ergänzung und keine Änderung

  • Der neue Ansatz soll Programmierer ermutigen, Fehler richtig zu behandeln.

check, denke ich, da sich die Mechanismen für die Fehlerbehandlung nicht unterscheiden - wir hätten nur eine Syntax zum "Sammeln" des ersten Nicht-Null-Wertes aus einer Reihe von Ausführungen und Zuweisungen, die verwendet werden können, um die Anzahl der zu begrenzen Stellen, an denen wir unseren Fehlerbehandlungscode in eine Funktion schreiben müssen

  • Jeder neue Ansatz sollte kürzer und/oder weniger repetitiv sein als der aktuelle Ansatz, dabei aber klar bleiben.

Ich bin mir nicht sicher, ob dies hier zutrifft, da die vorgeschlagene Funktion mehr als nur die Fehlerbehandlung betrifft. Ich denke, es kann Code, der Fehler

  • Die Sprache funktioniert heute und jede Änderung ist mit Kosten verbunden. Es sollte nicht nur eine Wäsche sein, es sollte deutlich besser sein.

Einverstanden, und so scheint es, dass eine Änderung, deren Umfang über die reine Fehlerbehandlung hinausgeht, angemessen sein könnte. Ich glaube, das zugrunde liegende Problem ist, dass nil Checks in go repetitiv und ausführlich werden, und zufälligerweise ist error ein nil -fähiger Typ.

@KernelDeimos Wir einfallen lassen . Ich bin jedoch noch einen Schritt weiter gegangen und habe erklärt, warum der Weg mit x, ? := doSomething() in der Praxis nicht so gut funktioniert. Obwohl es schön zu sehen ist, dass ich nicht die einzige Person bin, die darüber nachdenkt, das ? Operator auf interessante Weise in die Sprache ein.

https://github.com/golang/go/issues/25582

Ist das nicht im Grunde nur eine Falle ?

Hier ist ein Spießball:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 Zeilen → 44

trap bedeutet "diesen Code in Stapelreihenfolge ausführen, wenn eine mit ? markierte Variable des angegebenen Typs nicht der Nullwert ist." Es ist wie defer aber es kann den Kontrollfluss beeinflussen.

Ich mag die Idee von trap , aber die Syntax nervt mich ein wenig. Was wäre, wenn es sich um eine Art Deklaration handelt? Beispiel: trap err error {} deklariert ein trap namens err vom Typ error das, wenn es zugewiesen wird, den angegebenen Code ausführt. Der Code muss nicht einmal zurückkehren; es ist nur erlaubt. Dies bricht auch die Abhängigkeit davon, dass nil etwas Besonderes ist.

Bearbeiten: Erweitern und ein Beispiel geben, jetzt, wo ich nicht telefoniere.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

Im Wesentlichen funktioniert ein trap genauso wie ein var , außer dass immer dann, wenn ihm der Operator ? zugewiesen wird, der Codeblock ausgeführt wird. Der Operator ? verhindert auch, dass er gespiegelt wird, wenn er mit := . Das erneute Deklarieren eines trap im gleichen Bereich, im Gegensatz zu einem var , ist zulässig, muss jedoch vom gleichen Typ wie der vorhandene sein; Dadurch können Sie den zugehörigen Codeblock ändern. Da der laufende Block nicht unbedingt zurückkehrt, können Sie auch separate Pfade für bestimmte Dinge verwenden, z. B. um zu überprüfen, ob err == io.EOF .

Was mir an diesem Ansatz gefällt, ist, dass er dem errWriter Beispiel aus Errors are Values ähnlich zu sein scheint, jedoch in einem etwas allgemeineren Setup, das keine Deklaration eines neuen Typs erfordert.

@carlmjohnson wem hast du
Unabhängig davon scheint dieses trap Konzept nur eine andere Art zu sein, eine defer Anweisung zu schreiben, nicht wahr? Der Code, so wie er geschrieben wurde, wäre im Wesentlichen der gleiche, wenn Sie panic bei einem Fehler ungleich Null ausführen und dann eine verzögerte Schließung verwenden, um benannte Rückgabewerte festzulegen und eine Bereinigung durchzuführen. Ich denke, dies führt zu den gleichen Problemen wie mein früherer Vorschlag, _! zu verwenden, um automatisch in Panik zu geraten, da eine Fehlerbehandlungsmethode einer anderen übergeordnet wird. FWIW Ich hatte auch das Gefühl, dass der Code, so wie er geschrieben wurde, viel schwieriger zu verstehen war als das Original. Könnte dieses trap Konzept heute mit go nachgeahmt werden, auch wenn es weniger klar ist als die Syntax dafür? Ich habe das Gefühl, es könnte und es wäre if err != nil { panic (err) } und defer , um das zu erfassen und zu handhaben.

Es scheint dem Konzept des oben vorgeschlagenen collect Blocks ähnlich zu sein, der meiner Meinung nach eine sauberere Möglichkeit bietet, dieselbe Idee auszudrücken ("Wenn dieser Wert nicht Null ist, möchte ich ihn erfassen und etwas tun damit"). Go ist gerne linear und explizit. trap fühlt sich an wie eine neue Syntax für panic / defer aber mit einem weniger klaren Kontrollfluss.

@mccolljr , es schien mir eine recover - ähnliche Funktion zur Fehlerbehandlung.

Ich würde auch beobachten, dass die "Trap" -Umschreibung viele Funktionen meines Vorschlags verloren hat (sehr unterschiedliche Fehler kommen heraus), und außerdem ist mir unklar, wie ich die Fehlerbehandlung mit den Trap-Anweisungen ausklammern kann. Ein Großteil dieser Reduzierung meines Vorschlags besteht darin, dass die Fehlerbehandlungsfehler nicht mehr korrekt sind und ich glaube, dass es einfacher wird, Fehler einfach direkt zurückzugeben, als alles andere zu tun.

Die Möglichkeit, den Fluss fortzusetzen, wird durch das modifizierte trap Beispiel ermöglicht, das ich oben angegeben habe . Ich habe es später bearbeitet, daher weiß ich nicht, ob Sie es gesehen haben oder nicht. Es ist einem collect sehr ähnlich, aber ich denke, es gibt etwas mehr Kontrolle darüber. Je nachdem, wie die Scoping-Regeln dafür funktionieren, könnte es ein bisschen spaghettihaft sein, aber ich denke, es wäre möglich, eine gute Balance zu finden.

@thejerf Ah, das macht mehr Sinn. Ich wusste nicht, dass es eine Antwort auf Ihren Vorschlag war. Mir ist jedoch nicht klar, was der Unterschied zwischen erroring() und recover() wäre, abgesehen von der Tatsache, dass recover auf panic antwortet. Scheint, als würden wir nur implizit in Panik geraten, wenn ein Fehler zurückgegeben werden muss. Das Zurückstellen ist auch ein etwas kostspieliger Vorgang, daher bin ich mir nicht sicher, wie ich es in allen Funktionen verwenden soll, die Fehler verursachen könnten.

@DeedleFake Das gleiche gilt für trap , denn so wie ich es sehe ist trap entweder im Wesentlichen ein Makro, das Code einfügt, wenn der ? Operator verwendet wird, was seine eigenen Bedenken aufwirft und Überlegungen, oder es ist implementiert als goto ... was, wenn der Benutzer nicht in den trap Block zurückkehrt, oder es ist nur ein syntaktisch anderes defer . Und was passiert, wenn ich mehrere Trap-Blöcke in einer Funktion deklariere? Ist das erlaubt? Wenn ja, welche wird ausgeführt? Das erhöht die Komplexität der Implementierung. Go mag es, rechthaberisch zu sein, und das gefällt mir. Ich denke, dass collect oder ein ähnliches lineares Konstrukt eher mit der Ideologie von Go übereinstimmt als trap , das, wie mir nach meinem ersten Vorschlag aufgezeigt wurde, ein try-catch zu sein scheint im Kostüm aufbauen.

Was ist, wenn der Benutzer nicht in den Trap-Block zurückkehrt?

Wenn trap Kontrollfluss nicht zurückgibt oder anderweitig ändert ( goto , continue , break usw.), kehrt der Kontrollfluss dorthin zurück, wo der Code Block wurde 'aufgerufen'. Der Block selbst würde ähnlich wie der Aufruf einer Closure funktionieren, mit der Ausnahme, dass er Zugriff auf Kontrollflussmechanismen hat. Die Mechanismen würden an der Stelle funktionieren, an der der Block deklariert wird, nicht an der Stelle, von der aus er aufgerufen wird

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

würde funktionieren.

Und was passiert, wenn ich mehrere Trap-Blöcke in einer Funktion deklariere? Ist das erlaubt? Wenn ja, welche wird ausgeführt?

Ja, das ist erlaubt. Die Blöcke werden von der Falle benannt, daher ist es ziemlich einfach herauszufinden, welcher aufgerufen werden sollte. Zum Beispiel in

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

Block 2 wird aufgerufen. Die große Frage in diesem Fall wäre wahrscheinlich, was im Fall von n?, err? = 3, errors.New("Example") passiert, was wahrscheinlich die Angabe der Reihenfolge der Zuweisungen erfordern würde, wie in #25609 auftauchte.

Ich denke, Collect oder ein ähnliches lineares Konstrukt passt eher zu Gos Ideologie als Trap, das, wie mir nach meinem ersten Vorschlag aufgezeigt wurde, ein Try-Catch-Konstrukt im Kostüm zu sein scheint.

Ich denke, sowohl collect als auch trap sind im Wesentlichen try-catch s umgekehrt. Eine Standard- try-catch ist eine Fail-by-Default-Richtlinie, die eine Überprüfung erfordert, oder sie explodiert. Dies ist ein standardmäßig erfolgreiches System, mit dem Sie im Wesentlichen einen Fehlerpfad angeben können.

Eine Sache, die das Ganze noch komplizierter macht, ist die Tatsache, dass Fehler nicht von Natur aus als Fehler behandelt werden und einige Fehler wie io.EOF überhaupt keinen Fehler angeben. Ich denke, deshalb sind Systeme, die nicht speziell an Fehler gebunden sind, wie collect oder trap der richtige Weg.

"Ah, das macht mehr Sinn. Ich wusste nicht, dass es eine Antwort auf Ihren Vorschlag ist. Mir ist jedoch nicht klar, was der Unterschied zwischen erroring() und recovery() wäre, abgesehen von der Tatsache, dass recovery antwortet auf Panik."

Es geht darum, keinen großen Unterschied zu haben. Ich versuche, die Anzahl der neuen Konzepte zu minimieren und gleichzeitig so viel Leistung wie möglich aus ihnen zu ziehen. Ich betrachte das Aufbauen auf vorhandener Funktionalität als Feature und nicht als Fehler.

Einer der Punkte meines Vorschlags besteht darin, über "Was ist, wenn wir diesen wiederkehrenden Teil von drei Zeilen, in denen wir return err schreiben, in denen wir ? ersetzen" hinausgehen, zu untersuchen, "wie wirkt es sich auf die Rest der Sprache? Welche neuen Muster ermöglicht sie? Welche neuen „Best Practices“ werden dadurch geschaffen? Welche alten „Best Practices“ sind keine Best Practices mehr?" Ich sage nicht, dass ich diesen Job beendet habe. Und selbst wenn man der Meinung ist, dass die Idee für Gos Geschmack tatsächlich zu viel Kraft hat (da Go keine machtmaximierende Sprache ist und selbst mit der Designwahl, sie auf den Typ error sie wahrscheinlich immer noch die mächtigste Vorschlag in diesem Thread, den ich sowohl im guten als auch im schlechten Sinne von "mächtig" meine, denke ich, dass wir die Fragen untersuchen könnten, was die neuen Konstrukte mit Programmen als Ganzes machen werden, anstatt was sie tun reicht für sieben Zeilen Beispielfunktionen, weshalb ich versucht habe, die Beispiele zumindest auf den Bereich von ~50-100 Zeilen "echten Code" zu bringen. In 5 Zeilen sieht alles gleich aus, einschließlich der Go 1.0-Fehlerbehandlung, was vielleicht einer der Gründe ist, warum wir alle aus eigener Erfahrung wissen, dass es hier ein echtes Problem gibt, aber die Unterhaltung dreht sich irgendwie im Kreis, wenn wir darüber reden es in einem zu kleinen Maßstab, bis einige Leute anfangen, sich selbst davon zu überzeugen, dass es vielleicht doch kein Problem gibt. (Vertraue deinen echten Programmiererfahrungen, nicht den 5-Zeilen-Beispielen!)

"Es scheint, als würden wir nur implizit in Panik geraten, wenn ein Fehler zurückgegeben werden muss."

Es ist nicht implizit. Es ist explizit. Sie verwenden den Operator pop , wenn er das tut, was Sie wollen. Wenn es nicht tut, was Sie wollen, verwenden Sie es nicht. Was es tut, ist einfach genug, um es in einem einzigen einfachen Satz zu erfassen, obwohl die Spezifikation wahrscheinlich einen ganzen Absatz umfassen würde, da solche Dinge so funktionieren. Es gibt keine implizite. Außerdem ist es keine Panik, weil es nur eine Ebene des Stapels abwickelt, genau wie eine Rückkehr; es ist genauso Panik wie eine Rückkehr, die es überhaupt nicht gibt.

Es ist mir auch egal, ob Sie pop als buchstabieren? oder Wasauchimmer. Ich persönlich finde, dass ein Wort Go-ähnlicher aussieht, da Go derzeit keine symbolreiche Sprache ist, aber ich kann nicht leugnen, dass ein Symbol den Vorteil hat, dass es nicht mit bestehendem Quellcode kollidiert. Mich interessiert die Semantik und was wir darauf aufbauen können und welche Verhaltensweisen die neue Semantik sowohl neuen als auch erfahrenen Programmierern bietet, mehr noch als die Rechtschreibung.

"Das Aufschieben ist auch ein etwas kostspieliger Vorgang, daher bin ich mir nicht sicher, wie ich es in allen Funktionen verwenden soll, die Fehler verursachen könnten."

Das habe ich bereits anerkannt. Obwohl ich vorschlagen würde, dass es im Allgemeinen nicht so teuer ist, und ich habe kein schlechtes Gewissen, wenn Sie zu Optimierungszwecken eine heiße Funktion haben, schreiben Sie sie auf die aktuelle Weise. Es ist ausdrücklich nicht mein Ziel, 100% aller Fehlerbehandlungsfunktionen zu modifizieren, sondern 80% davon viel einfacher und korrekter zu machen und die 20%-Fälle (wahrscheinlich ehrlich gesagt eher 98/2, ehrlich) bleiben zu lassen sind. Der Großteil des Go-Codes reagiert nicht auf die Verwendung von Defer, weshalb defer existiert.

Tatsächlich können Sie den Vorschlag ganz einfach dahingehend ändern, dass er defer nicht verwendet, und ein Schlüsselwort wie trap als Deklaration verwenden, die nur einmal ausgeführt wird, unabhängig davon, wo sie erscheint, anstatt dass die Methode defer eigentlich eine Anweisung ist, die a pusht -Handler auf den Stapel der zurückgestellten Funktionen. Ich habe mich bewusst dafür entschieden, defer wiederzuverwenden, um zu vermeiden, der Sprache neue Konzepte hinzuzufügen... sogar die Fallen zu verstehen, die aus Verzögerungen in Schleifen resultieren könnten, die Leute unerwartet beißen. Aber es ist immer noch nur das eine defer Konzept, das es zu verstehen gilt.

Nur um deutlich zu machen, dass das Hinzufügen eines neuen Schlüsselworts zur Sprache eine bahnbrechende Änderung ist.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

ergibt sich

prog.go:7:6: syntax error: unexpected select, expecting name or (

Das heißt, wenn wir versuchen, ein try , trap , assert , ein beliebiges Schlüsselwort in die Sprache einzufügen, laufen wir Gefahr, eine Menge Code zu beschädigen. Code, der möglicherweise länger gepflegt werden kann.

Aus diesem Grund habe ich ursprünglich vorgeschlagen, einen speziellen ? go-Operator hinzuzufügen, der auf Variablen im Kontext von Anweisungen angewendet werden kann. Das Zeichen ? wird ab sofort als unzulässiges Zeichen für Variablennamen bezeichnet. Das bedeutet, dass es derzeit in keinem aktuellen Go-Code verwendet wird und wir es daher ohne grundlegende Änderungen einführen können.

Das Problem bei der Verwendung auf der linken Seite einer Zuweisung besteht nun darin, dass es nicht berücksichtigt, dass Go mehrere Rückgabeargumente zulässt.

Betrachten Sie zum Beispiel diese Funktion

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

wenn wir verwenden? oder versuchen Sie in der linken Seite der Zuweisung, die if err != nil-Blöcke loszuwerden. Gehen wir automatisch davon aus, dass Fehler bedeuten, dass alle anderen Werte jetzt Müll sind? Was wäre, wenn wir es so machen würden

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

Welche Annahmen machen wir hier? Dass es nicht schädlich sein sollte, einfach davon auszugehen, dass es in Ordnung ist, den Wert wegzuwerfen? Was ist, wenn der Fehler eher eine Warnung sein soll und der Wert x in Ordnung ist? Was ist, wenn die einzige Funktion, die den Fehler auslöst, der Aufruf von GetZ() ist und die x-, y-Werte tatsächlich gut sind? Vermuten wir, sie zurückzugeben. Was ist, wenn wir keine benannten Rückgabeargumente verwenden? Was ist, wenn die Rückgabeargumente Referenztypen wie eine Karte oder ein Kanal sind, sollten wir davon ausgehen, dass es sicher ist, dem Aufrufer nil zurückzugeben?

TLDR; hinzufügen? oder try auf Zuweisungen um zu eliminieren

if err != nil{
    return err
}

führt zu viel zu viel Verwirrung als Vorteile.

Und das Hinzufügen von etwas wie dem trap Vorschlag führt zu einem Bruch.

Deshalb in meinem Vorschlag , den ich in einer separaten Ausgabe gemacht habe. Ich habe die Möglichkeit zugelassen, func ?() bool für jeden Typ zu deklarieren, damit Sie, wenn Sie angerufen haben, sagen:

x, err := doSomething; return x, err?    

Sie können diesen Fallennebeneffekt auf eine Weise ausführen lassen, die für jeden Typ gilt.

Und die Anwendung ? Nur mit Anweisungen zu arbeiten, wie ich gezeigt habe, ermöglicht die Programmierbarkeit von Anweisungen. In meinem Vorschlag schlage ich vor, eine spezielle switch-Anweisung zuzulassen, die es jemandem ermöglicht, Fälle umzuschalten, die das Schlüsselwort + ?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

Wenn wir verwenden? auf einem Typ, der kein explizites ? deklarierte Funktion oder ein eingebauter Typ, dann das Standardverhalten der Überprüfung, ob var == nil || Der Nullwert {die Anweisung ausführen} ist die mutmaßliche Absicht.

Idk, ich bin kein Experte für Programmiersprachendesign, aber ist das nicht so?

Zum Beispiel ist die Funktion os.Chdir derzeit

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

Nach diesem Vorschlag könnte es geschrieben werden als

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

im Wesentlichen dasselbe wie die Pfeilfunktionen von Javascript oder wie Dart es als "Fettpfeil-Syntax" definiert

z.B

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

von der Darttour .

Für Funktionen, die nur einen Ausdruck enthalten, können Sie eine Kurzsyntax verwenden:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
Die Syntax => expr ist eine Abkürzung für { return expr; }. Die Notation => wird manchmal auch als Fat Arrow-Syntax bezeichnet.

@mortdeus , die linke Seite des Dart-Pfeils ist eine Funktionssignatur, während syscall.Chdir(dir) ein Ausdruck ist. Sie scheinen mehr oder weniger unabhängig zu sein.

@mortdeus Ich habe vergessen, dies früher zu klären, aber die Idee, die ich hier kommentiert habe, ähnelt kaum dem von Ihnen markierten Vorschlag. Ich mag die Idee von ? als Platzhalter, also habe ich das kopiert, aber meine Idee betonte die Wiederverwendung eines einzelnen Codeblocks, um die Fehler zu behandeln und gleichzeitig einige der bekannten Probleme mit try...catch vermeiden. Ich habe sehr darauf geachtet, etwas zu finden, über das vorher nicht gesprochen wurde, damit ich eine neue Idee einbringen konnte.

Wie wäre es mit einer neuen bedingten return (oder returnIf ) Anweisung?

return(bool expression) ...

dh.

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

Oder lassen Sie fmt die einzeiligen Return-Only-Funktionen in einer Zeile statt in drei formatieren:

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

Mir fällt ein, dass ich alles bekommen kann, was ich will, indem ich nur einen Operator hinzufüge, der früh zurückgibt, wenn der Fehler ganz rechts nicht null ist, wenn ich ihn mit benannten Parametern kombiniere:

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

Derzeit ist mein Verständnis des Go-Konsens gegen benannte Parameter, aber wenn sich die Angebote von Namensparametern ändern, kann sich auch der Konsens ändern. Wenn der Konsens dagegen stark genug ist, besteht natürlich die Möglichkeit, Accessoren einzubeziehen.

Dieser Ansatz bringt mir das, wonach ich suche (erleichtert die systematische Behandlung von Fehlern am Anfang einer Funktion, die Möglichkeit, solchen Code zu berücksichtigen, auch die Reduzierung der Zeilenanzahl) mit so ziemlich allen anderen Vorschlägen hier, einschließlich des Originals . Und selbst wenn die Go-Community beschließt, dass sie es nicht mag, muss ich mich nicht darum kümmern, da es in meinem Code funktionsbezogen vorkommt und in keiner Richtung eine Impedanzfehlanpassung aufweist.

Obwohl ich einen Vorschlag vorziehen würde, der es erlaubt, eine Funktion der Signatur func GetInt() (x int, err error) in Code mit OtherFunc(GetInt()?, "...") (oder was auch immer das Ergebnis ist) zu verwenden, gegenüber einem, der nicht zusammengesetzt werden kann Ein Ausdruck. Obwohl die sich ständig wiederholende einfache Fehlerbehandlungsklausel weniger ärgerlich ist, ist die Menge meines Codes, der eine Funktion von arity 2 entpackt, nur damit sie das erste Ergebnis haben kann, immer noch ärgerlich beträchtlich und trägt nicht wirklich zur Klarheit der resultierenden Code.

@thejerf , ich habe das Gefühl, dass es hier viele seltsame Verhaltensweisen gibt. Sie rufen net.Listen , was einen Fehler zurückgibt, aber nicht zugewiesen ist. Und dann schieben Sie auf und geben err . Überschreibt jedes neue defer das letzte, sodass annotateError nie aufgerufen wird? Oder stapeln sie sich, so dass, wenn ein Fehler von beispielsweise toServer.Send , closeOnErr zweimal aufgerufen wird und dann annotateError aufgerufen wird? Wird closeOnErr nur aufgerufen, wenn der vorhergehende Aufruf eine passende Signatur hat? Was ist mit diesem Fall?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

Beim Durchlesen des Codes verwirrt es auch Dinge, wie zum Beispiel warum kann ich nicht einfach sagen?

client.session = communicationProtocol.FinalProtocol(conn)?

Vermutlich, weil FinalProtocol einen Fehler zurückgibt? Aber das ist dem Leser verborgen.

Was passiert schließlich, wenn ich einen Fehler melden und ihn innerhalb einer Funktion wiederherstellen möchte? Es scheint, als würde Ihr Beispiel diesen Fall verhindern?

_Nachtrag_

OK, ich denke, wenn Sie einen Fehler beheben möchten, weisen Sie ihn in Ihrem Beispiel wie in dieser Zeile zu:

env, err := environment.GetRuntimeEnvironment()

Das ist in Ordnung, weil err überschattet ist, aber wenn ich mich dann geändert habe...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

nur

forwardPort = env.PortToForward()

Dann wird Ihr behandelter verzögerter Fehler ihn nicht abfangen, da Sie die err die im Geltungsbereich erstellt wurden. Oder übersehe ich etwas?

Ich glaube, eine Ergänzung der Syntax, die angibt, dass eine Funktion fehlschlagen kann, ist ein guter Anfang. Ich schlage etwas in dieser Richtung vor:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

Wenn eine Funktion fehlschlägt (indem Sie das Schlüsselwort fail anstelle von return , gibt sie den Nullwert für jeden Rückgabeparameter zurück.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

Dieser Ansatz hat den Vorteil, dass aktueller Code funktioniert und gleichzeitig ein neuer Mechanismus für eine bessere Fehlerbehandlung (zumindest in der Syntax) ermöglicht wird.

Kommentare zu den Stichworten:

  • fails vielleicht nicht die beste Option, aber es ist die beste, die mir derzeit einfällt. Ich dachte daran, err (oder errs ) zu verwenden, aber wie sie derzeit verwendet werden, könnte dies aufgrund der aktuellen Erwartungen zu einer schlechten Wahl machen ( err ist höchstwahrscheinlich ein Variablenname, und errs könnte als Slice oder Array oder Fehler angesehen werden).
  • handle könnte ein wenig irreführend sein. Ich wollte recover , aber es wird für panic s...

edit: r.Read-Aufruf geändert, um io.Reader.Read() .

Ein Grund für diesen Vorschlag ist, dass der aktuelle Ansatz in Go Tools nicht dabei hilft zu verstehen, ob ein zurückgegebener error Wert anzeigt, dass eine Funktion fehlschlägt oder als Teil ihrer Funktion einen Fehlerwert zurückgibt (z. B. github.com/pkg/errors ).

Ich glaube, dass die Aktivierung von Funktionen zum expliziten Ausdrücken von Fehlern der erste Schritt zur Verbesserung der Fehlerbehandlung ist.

@ibrasho , wie unterscheidet sich

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... wenn wir Compiler-Warnungen oder Lint für unbehandelte Instanzen von error ausgeben? Keine Sprachänderung erforderlich.

Zwei Dinge:

  • Ich denke, die vorgeschlagene Syntax liest und sieht besser aus. 😁
  • Meine Version erfordert, Funktionen die Möglichkeit zu geben, explizit anzugeben, dass sie fehlschlagen. Dies ist etwas, das derzeit in Go fehlt, was Tools ermöglichen könnte, mehr zu tun. Wir können eine Funktion, die einen error Wert zurückgibt, immer als fehlgeschlagen behandeln, aber das ist eine Annahme. Was ist, wenn die Funktion 2 error Werte zurückgibt?

Mein Vorschlag hatte etwas, das ich entfernt habe, nämlich die automatische Verbreitung:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

Ich habe das entfernt, da ich denke, dass die Fehlerbehandlung explizit sein sollte.

edit: r.Read-Aufruf so geändert, dass er mit io.Reader.Read() übereinstimmt.

Das wäre also eine gültige Signatur oder ein Prototyp?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Angesichts der Tatsache, dass eine io.Reader Implementierung io.EOF zuweist, wenn nichts mehr zu lesen ist, und einige andere Fehler für Fehlerbedingungen.)

Ja. Aber niemand sollte erwarten, dass err einen Fehler in der Funktion anzeigt, die ihre Aufgabe erfüllt. Der Fehler beim Lesen sollte an den Handle-Block übergeben werden. Fehler sind Werte in Go, und der von dieser Funktion zurückgegebene sollte keine besondere Bedeutung haben, abgesehen davon, dass er von dieser Funktion zurückgegeben wird (aus irgendeinem seltsamen Grund).

Ich habe vorgeschlagen, dass ein Fehler dazu führt, dass Rückgabewerte als Nullwerte zurückgegeben werden. Die aktuellen Reader.Read machen bereits einige Versprechungen, die mit diesem neuen Ansatz möglicherweise nicht möglich sind.

Wenn Read nach erfolgreichem Lesen von n > 0 Bytes auf einen Fehler oder eine Dateiendebedingung stößt, gibt es die Anzahl der gelesenen Bytes zurück. Es kann den Fehler (nicht Null) von demselben Aufruf oder den Fehler (und n == 0) von einem nachfolgenden Aufruf zurückgeben. Ein Beispiel für diesen allgemeinen Fall ist, dass ein Reader, der am Ende des Eingabestroms eine Anzahl von Bytes ungleich null zurückgibt, entweder err == EOF oder err == nil zurückgeben kann. Der nächste Read sollte 0, EOF, zurückgeben.

Aufrufer sollten immer die zurückgegebenen n > 0 Bytes verarbeiten, bevor sie den Fehler err berücksichtigen. Dadurch werden E/A-Fehler, die nach dem Lesen einiger Bytes auftreten, und auch beide zulässigen EOF-Verhalten korrekt behandelt.

Implementierungen von Read werden davon abgeraten, eine Null-Byte-Zählung mit einem Null-Fehler zurückzugeben, außer wenn len(p) == 0 ist. Aufrufer sollten eine Rückgabe von 0 und nil so behandeln, als ob nichts passiert wäre; insbesondere weist es nicht auf EOF hin.

Dieses Verhalten ist mit dem derzeit vorgeschlagenen Ansatz nicht möglich. Aus dem aktuellen Read-Schnittstellenvertrag sehe ich einige Mängel, wie z. B. die Handhabung von partiellen Lesevorgängen.

Wie sollte sich eine Funktion im Allgemeinen verhalten, wenn sie teilweise fertig ist, bis sie fehlschlägt? Darüber habe ich mir ehrlich gesagt noch keine Gedanken gemacht.

Der Fall von io.EOF ist einfach:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf , ich habe das Gefühl, dass es hier viele seltsame Verhaltensweisen gibt. Sie rufen net.Listen auf, was einen Fehler zurückgibt, aber nicht zugewiesen ist.

Ich verwende den gemeinsamen ? Operator, der von mehreren Personen vorgeschlagen wurde, um anzuzeigen, dass der Fehler mit Nullwerten für die anderen Werte zurückgegeben wird, wenn der Fehler nicht Null ist. Ich bevorzuge ein kurzes Wort gegenüber einem Operator, da ich glaube, dass Go keine Sprache mit viel Operator ist, aber wenn ? in die Sprache gehen würde, würde ich immer noch meine Gewinne zählen, anstatt mit den Füßen zu stampfen ein Hauch.

Überschreibt jede neue Verzögerung die letzte, sodass annotateError nie aufgerufen wird? Oder stapeln sie sich, so dass, wenn ein Fehler beispielsweise von toServer.Send zurückgegeben wird, closeOnErr zweimal aufgerufen wird und dann annotateError aufgerufen wird?

Es funktioniert jetzt wie Defer: https://play.golang.org/p/F0xgP4h5Vxf Ich habe ein paar Daumenabdrücke für diesen Beitrag erwartet, auf den meine geplante Antwort lauten würde, dass dies _bereits_ funktioniert und Sie würden einfach das aktuelle Go-Verhalten herunterdrücken, aber es hat keine bekommen. Ach. Wie dieser Ausschnitt auch zeigt, ist Shadowing kein Problem oder zumindest kein größeres Problem als es ohnehin schon ist. (Dies würde es weder beheben noch besonders verschlimmern.)

Ich denke, ein Aspekt, der verwirrend sein könnte, ist, dass es im aktuellen Go bereits der Fall ist, dass ein benannter Parameter am Ende "was auch immer zurückgegeben wird" ist. Sie können also tun, was ich getan habe, und einen Zeiger darauf nehmen und übergeben das zu einer zurückgestellten Funktion und manipulieren Sie sie, unabhängig davon, ob Sie direkt einen Wert wie return errors.New(...) , der intuitiv wie eine "neue Variable" erscheinen mag, die nicht die benannte Variable ist, aber tatsächlich wird Go enden mit ihm der benannten Variablen zugewiesen, wenn die Verzögerung ausgeführt wird. Es ist leicht, dieses besondere Detail des aktuellen Go im Moment zu ignorieren. Ich behaupte, dass, obwohl es jetzt verwirrend sein kann, wenn Sie sogar an einer Codebasis arbeiten, die dieses Idiom verwendet (dh ich sage nicht, dass dies überhaupt "Best Practice" werden muss), nur ein paar Belichtungen reichen aus). d finde es ziemlich schnell heraus. Denn, um es noch einmal ganz klar zu sagen: So funktioniert Go bereits, keine vorgeschlagene Änderung.

Hier ist ein Vorschlag, der meiner Meinung nach noch nie zuvor vorgeschlagen wurde. An einem Beispiel:

 r, !handleError := something()

Die Bedeutung davon ist die gleiche:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(wobei _xyzzy eine neue Variable ist, deren Gültigkeitsbereich sich nur auf diese beiden Codezeilen erstreckt, und R mehrere Werte sein können).

Vorteile dieses Vorschlags sind nicht spezifisch für Fehler, behandelt Nullwerte nicht speziell und es ist einfach, präzise anzugeben, wie Fehler in einen bestimmten Codeblock eingeschlossen werden. Die Syntaxänderungen sind gering. Angesichts der einfachen Übersetzung ist es leicht zu verstehen, wie diese Funktion funktioniert.

Nachteile sind, dass es eine implizite Rückgabe einführt, man kann keinen generischen Handler schreiben, der nur den Fehler zurückgibt (da seine Rückgabewerte auf der Funktion basieren müssen, von der er aufgerufen wird), und dass der Wert, der an den Handler übergeben wird, ist im Anrufcode nicht verfügbar.

So können Sie es verwenden:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

Oder Sie können es in einem Test verwenden, um t.Fatal aufzurufen, wenn Ihr Init-Code fehlschlägt:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

Ich würde vorschlagen, die Funktionssignatur in nur func(error) error ändern. Es vereinfacht den Mehrheitsfall, und wenn Sie den Fehler weiter analysieren müssen, verwenden Sie einfach den aktuellen Mechanismus.

Syntaxfrage: Können Sie die Funktion inline definieren?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

Ich bin mit "Tu das nicht" zufrieden, aber die Syntax sollte es wahrscheinlich zulassen, die Anzahl der Sonderfälle zu reduzieren. Dies würde auch erlauben:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

Was mir als einer der besseren Vorschläge für so etwas erscheint, zumindest im Hinblick darauf, das Zeug prägnant unterzubringen. Dies ist ein weiterer Fall, in dem Sie an dieser Stelle IMHO bessere Fehler ausgeben, als es bei ausnahmebasiertem Code der Fall ist, der oft nicht so detailliert über die Art des Fehlers ist, weil es so einfach ist, Ausnahmen einfach verbreiten zu lassen.

Ich würde auch aus Leistungsgründen vorschlagen, dass Bang-Error-Funktionen so definiert werden, dass sie nicht bei Nullwerten aufgerufen werden. Das hält ihre Auswirkungen auf die Leistung ziemlich gering; In dem Fall, den ich gerade gezeigt habe, ist es, wenn Read normal erfolgreich ist, nicht teurer als eine aktuelle Read-Implementierung, die bereits if jedem Fehler if Klausel fehlschlägt. Wenn wir die ganze Zeit eine Funktion für nil aufrufen, wird das sehr teuer, wenn sie nicht eingebunden werden kann, was in den seltensten Fällen zu einer nicht trivialen Zeit wird. (Wenn ein Fehler aktiv auftritt, können wir wahrscheinlich einen Funktionsaufruf unter fast allen Umständen rechtfertigen und leisten (wenn Sie nicht auf die aktuelle Methode zurückgreifen können), aber das wollen wir nicht wirklich für Nicht-Fehler.) bedeutet auch, dass Bang-Funktionen bei ihrer Implementierung einen Wert ungleich Null annehmen können, was sie ebenfalls vereinfacht.

@thejerf schöner, aber glücklicher Weg ist stark verändert.
Vor vielen Nachrichten gab es Vorschläge für Ruby wie "oder" sintax - f := OpenFile(filename) or failed("couldn't open file") .

Zusätzliche Bedenken - gilt das für alle Arten von Parametern oder nur für Fehler? wenn nur für Fehler - dann muss der Typ error eine besondere Bedeutung für den Compiler haben.

@thejerf schöner, aber glücklicher Weg ist stark verändert.

Ich würde empfehlen, zwischen dem wahrscheinlich gemeinsamen Pfad des ursprünglichen Vorschlags zu unterscheiden, der wie der ursprüngliche Vorschlag von Paulhankin aussieht:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

vielleicht sogar, wenn herr irgendwo herausgerechnet wurde und meine Erkundungen, was es braucht, um es vollständig zu spezifizieren, was für dieses Gespräch eine Notwendigkeit ist, und meine eigenen Überlegungen, wie ich es in meinem persönlichen Code verwenden könnte, was lediglich eine Erforschung dessen ist, was sonst noch durch einen Vorschlag ermöglicht und geboten wird. Ich habe bereits gesagt, dass das wörtliche Inlining einer Funktion wahrscheinlich eine schlechte Idee ist, aber die Grammatik sollte es wahrscheinlich zulassen, dass die Grammatik einfach bleibt. Ich kann bereits eine Go-Funktion schreiben, die drei Funktionen benötigt, und alle direkt in den Aufruf integrieren. Das bedeutet nicht, dass Go kaputt ist oder dass Go etwas tun muss, um dies zu verhindern; Das bedeutet, dass ich das nicht tun sollte, wenn ich Wert auf Code-Klarheit lege. Ich mag es, dass Go Code-Klarheit bietet, aber die Entwickler tragen immer noch ein gewisses Maß an Verantwortung, den Code klar zu halten.

Wenn Sie mir sagen, dass der "glückliche Weg" für

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

ist zerknittert und schwer zu lesen, aber der glückliche Weg ist leicht zu lesen

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

dann ist der einzig mögliche Weg, der zweite leichter zu lesen ist, dass Sie bereits daran gewöhnt sind, ihn zu lesen, und Ihre Augen darauf trainiert sind, genau den richtigen Weg zu überspringen, um den glücklichen Weg zu sehen. Das kann ich mir vorstellen, denn meine sind es auch. Aber sobald Sie sich an eine alternative Syntax gewöhnt haben, werden Sie auch dafür geschult. Sie sollten die Syntax basierend darauf analysieren, wie Sie sich fühlen werden, wenn Sie bereits daran gewöhnt sind, und nicht darauf, wie Sie sich jetzt fühlen.

Ich möchte auch beachten, dass die hinzugefügten Zeilenumbrüche im zweiten Beispiel darstellen, was mit meinem echten Code passiert. Es sind nicht nur "Zeilen" Code, die die aktuelle Go-Fehlerbehandlung dem Code hinzufügt, sondern auch viele "Absätze" zu einer ansonsten sehr einfachen Funktion. Ich möchte die Datei öffnen, einige Bytes lesen und verarbeiten. ich will nicht

Öffne einen Ordner.

Und dann einige Bytes lesen, wenn das in Ordnung ist.

Und dann verarbeiten, wenn das in Ordnung ist.

Ich habe das Gefühl, dass es viele Ablehnungen gibt, die auf "das ist nicht das, was ich gewohnt bin", hinauslaufen, anstatt eine tatsächliche Analyse, wie sich diese in echtem Code abspielen werden, sobald Sie sich daran gewöhnt haben und sie fließend verwenden .

Ich mag die Idee nicht, die Rückgabeanweisung zu verbergen, ich bevorzuge:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

In diesem Fall ist or ein bedingter Weiterleitungsoperator von Null ,
Weiterleiten der letzten Rückgabe, wenn sie ungleich Null ist.
Es gibt einen ähnlichen Vorschlag in C# mit dem Operator ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf "happy path" wird in Ihrem Fall ein Aufruf von failed(...) vorangestellt, der sehr lang sein kann. klingen auch wie yoda :rofl:

@rodcorsi- Zeichen, die mit der "Shift"-Taste eingegeben werden, sind IMHO schlecht - (wenn Sie mehrere Tastaturlayouts haben)

Bitte machen Sie diesen Weg nicht komplexer als er jetzt ist. Das Verschieben der gleichen Codes in eine Zeile (statt 3 oder mehr) ist keine wirkliche Lösung. Ich persönlich halte keinen dieser Vorschläge für so tragfähig. Leute, die Mathematik ist ganz einfach. Nehmen Sie entweder die "Try-Catch"-Idee an oder belassen Sie die Dinge so, wie sie jetzt sind, was viele "Wenn dann sonst"s und Codegeräusche bedeutet und nicht wirklich für die Verwendung in OO-Mustern wie Fluent Interface geeignet ist.

Vielen Dank für all Ihre Hand-Downs und vielleicht ein paar Hand-Ups ;-) (nur ein Scherz)

@KamyarM IMO, "die bekannteste Alternative verwenden oder überhaupt keine Änderungen vornehmen" ist keine sehr produktive Aussage. Es stoppt Innovationen und erleichtert zirkuläre Argumente.

@KernelDeimos Ich stimme dir zu, aber ich sehe viele Kommentare in diesem Thread, die im Wesentlichen das Alte befürworten, indem sie genau 4-5 Zeilen in eine einzige Zeile verschieben, was ich nicht als wirkliche Lösung sehe und auch viele in der Go-Community die Verwendung RELIGIÖS ablehnen Try-Catch, das die Türen für andere Meinungen schließt. Ich persönlich denke, dass diejenigen, die dieses Try-Catch-Konzept erfunden haben, wirklich darüber nachgedacht haben, und obwohl es vielleicht einige Fehler hat, aber diese Fehler werden nur durch schlechte Programmiergewohnheiten verursacht und es gibt keine Möglichkeit, Programmierer zu zwingen, guten Code zu schreiben, selbst wenn Sie ihn entfernen oder einschränken all die guten oder einige schlechte Eigenschaften, die eine Programmiersprache haben kann.
Ich habe so etwas schon einmal vorgeschlagen und dies ist nicht gerade Java- oder C#-Try-Catch und ich denke, es kann die aktuelle Fehlerbehandlung und Bibliotheken unterstützen, und ich verwende eines der Beispiele von oben. Im Grunde prüft der Compiler also nach jeder Zeile auf Fehler und springt zum Catch-Block, wenn der err-Wert auf nicht nil gesetzt ist:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
Woher weiß ich in Ihrem Beispiel (zum Zeitpunkt des Schreibens des Codes), welche Methode den Fehler zurückgegeben hat? Wie erfülle ich die dritte Art der Fehlerbehandlung ("Fehler mit zusätzlichen Kontextinformationen zurückgeben")?

@urandom
Eine Möglichkeit besteht darin, den Go-Schalter zu verwenden und den Ausnahmetyp im Catch zu finden. Nehmen wir an, Sie haben eine pathError-Ausnahme, von der Sie wissen, dass sie auf diese Weise von OpenFile() verursacht wird. Eine andere Möglichkeit, die sich nicht wesentlich von der aktuellen if err!=nil-Fehlerbehandlung in GoLang unterscheidet, ist diese:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

Auf diese Weise haben Sie also Optionen, sind jedoch nicht eingeschränkt. Wenn Sie wirklich genau wissen möchten, welche Zeile das Problem verursacht hat, setzen Sie jede einzelne Zeile in einen Versuchsfang, so wie Sie jetzt viele Wenn-dann-sonst schreiben. Wenn der Fehler für Sie nicht wichtig ist und Sie ihn an die Aufrufermethode übergeben möchten, die in den in diesem Thread besprochenen Beispielen tatsächlich darum geht, denke ich, dass mein vorgeschlagener Code die Aufgabe erfüllt.

@KamyarM Ich sehe, woher du jetzt kommst. Ich würde sagen, wenn es so viele Leute gegen try...catch gibt, sehe ich das als Beweis dafür, dass try...catch nicht perfekt ist und Fehler hat. Es ist eine einfache Lösung für ein Problem, aber wenn Go2 die Fehlerbehandlung besser machen kann als das, was wir in anderen Sprachen gesehen haben, wäre das wirklich cool.

Ich denke, es ist möglich, das Gute an Try...Catch zu nehmen, ohne das Schlechte an Try...Catch zu nehmen, was ich vorhin vorgeschlagen habe. Ich stimme zu, dass das Umwandeln von drei Zeilen in 1 oder 2 nichts löst.

Das grundlegende Problem, wie ich es sehe, besteht darin, dass der Fehlerbehandlungscode in einer Funktion wiederholt wird, wenn ein Teil der Logik "Zurück zum Aufrufer" ist. Wenn Sie die Logik zu irgendeinem Zeitpunkt ändern möchten, müssen Sie jede Instanz von if err != nil { return nil } ändern.

Das heißt, ich mag die Idee von try...catch wirklich, solange Funktionen nichts implizit throw können.

Eine andere Sache, die ich für nützlich halte, wäre, wenn die Logik in catch {} ein break Schlüsselwort erfordert, um den Kontrollfluss zu unterbrechen. Manchmal möchten Sie einen Fehler behandeln, ohne den Kontrollfluss zu unterbrechen. (Beispiel: "Für jedes Element dieser Daten tun Sie etwas, fügen Sie einen Fehler ungleich Null zu einer Liste hinzu und fahren Sie fort")

@KernelDeimos Dem stimme ich voll und ganz zu. Ich habe die genaue Situation so gesehen. Sie müssen Fehler so gut wie möglich erfassen, bevor Sie die Codes knacken. Wenn in GoLang in solchen Situationen so etwas wie Channels verwendet werden könnte, war das gut. Dann könnten Sie alle Fehler an den Channel senden, den Catch erwartet, und dann konnte catch sie einzeln verarbeiten.

Ich würde lieber "or return" mit #19642, #21498 mischen, als try..catch zu verwenden (defer/panic/recover existiert bereits; das Werfen innerhalb derselben Funktion ist wie mehrere goto-Anweisungen und wurde mit einem zusätzlichen Typwechsel in catch unordentlich; ermöglicht das Vergessen der Fehlerbehandlung, indem try..catch weit oben im Stack platziert wird (oder den Compiler erheblich komplizieren, wenn der Geltungsbereich try..catch innerhalb einer einzelnen Funktion liegt)

@egorse
Es scheint, dass die Try-Catch-Syntax, die @KamyarM vorschlägt, ein

Abgesehen davon , try einen Variablendefinitionsteil? Sie definieren eine err Variable, die jedoch von den anderen err Variablen innerhalb des Blocks selbst überschattet wird. Was ist seine Aufgabe?

Ich denke, es sagt ihm, welche Variable im Auge behalten werden soll, damit es vom Typ error entkoppelt werden kann. Es würde wahrscheinlich eine Änderung der Shadowing-Regeln erfordern, es sei denn, Sie brauchen nur die Leute, die wirklich vorsichtig damit sind. Bei der Deklaration im catch Block bin ich mir jedoch nicht sicher.

@egorse Genau das, was @DeedleFake dort erwähnt hat, ist der Zweck. Dies bedeutet, dass der try-Block dieses Objekt im Auge hat. Außerdem schränkt es seine Reichweite ein. Es ähnelt der using-Anweisung in C#. In C# werden die Objekte, die durch das Schlüsselwort using definiert werden, automatisch verworfen, sobald dieser Block ausgeführt wird, und der Geltungsbereich dieser Objekte ist auf den Block "Using" beschränkt.
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Die Verwendung des Catch ist notwendig, weil wir den Programmierer zwingen wollen, zu entscheiden, wie er den Fehler richtig handhaben soll. In C# und Java ist Catch ebenfalls obligatorisch. Wenn Sie in C# keine Ausnahme behandeln möchten, verwenden Sie in dieser Funktion überhaupt kein try-catch. Wenn eine Ausnahme auftritt, kann jede Methode in der Aufrufhierarchie die Ausnahme behandeln oder sogar erneut auslösen (oder sie in eine andere Ausnahme einschließen). Ich glaube nicht, dass Sie das gleiche in Java tun können. In Java muss eine Methode, die eine Ausnahme auslösen kann, diese in der Funktionssignatur deklarieren.

Ich möchte betonen, dass dieser Try-Catch-Block nicht der genaue ist. Ich habe diese Schlüsselwörter verwendet, weil dies in dem, was es erreichen soll, ähnlich ist und auch das ist, was viele Programmierer kennen und in den meisten Programmierkonzeptkursen gelehrt werden.

Es könnte eine _Return-on-Error_-Zuweisung geben, die nur funktioniert, wenn es einen _benannten Fehler-Rückgabeparameter_ gibt, wie in:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

Wenn err nicht nil dann kehren Sie zurück.

Ich denke, diese Diskussion über das Hinzufügen eines try Zuckers zu Rust wäre für die Teilnehmer dieser Diskussion aufschlussreich.

FWIW, ein alter Gedanke zur Vereinfachung der Fehlerbehandlung (Entschuldigung, wenn das Unsinn ist):

Die Erhöhungskennung , gekennzeichnet durch das Caret-Symbol ^ , kann als einer der Operanden auf der linken Seite einer Zuweisung verwendet werden. Für die Zwecke der Zuweisung ist der Raise-Bezeichner ein Alias ​​für den letzten Rückgabewert der enthaltenden Funktion, unabhängig davon, ob der Wert einen Namen hat oder nicht. Nachdem die Zuweisung abgeschlossen ist, testet die Funktion den letzten Rückgabewert gegen den Nullwert seines Typs (nil, 0, false, ""). Wenn er als Null angesehen wird, wird die Funktion weiter ausgeführt, andernfalls kehrt sie zurück.

Der Hauptzweck des Raise-Bezeichners besteht darin, Fehler von aufgerufenen Funktionen in einem bestimmten Kontext an den Aufrufer zurückzugeben, ohne die Tatsache zu verbergen, dass dies geschieht.

Betrachten Sie als Beispiel den folgenden Code:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

Dies entspricht ungefähr:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

Das Programm ist fehlerhaft, wenn:

  • die Erhöhungskennung wird in einer Zuweisung mehr als einmal verwendet
  • die Funktion gibt keinen Wert zurück
  • der Typ des letzten Rückgabewerts hat keinen aussagekräftigen und effizienten Test auf Null

Dieser Vorschlag ähnelt anderen darin, dass er nicht das Problem anspricht, mehr Kontextinformationen bereitzustellen, was das wert ist.

@gboyle Deshalb sollte der letzte Rückgabewert von IMO benannt und vom Typ error . Dies hat zwei wichtige Implikationen:

1 - andere Rückgabewerte werden auch benannt, daher
2 - sie haben bereits sinnvolle Nullwerte.

@object88 Wie context Pakets zeigt, erfordert dies einige Maßnahmen des Kernteams, wie das Definieren eines integrierten error Typs (nur ein normaler Go error ) mit einigen gemeinsamen Attributen (Nachricht? Aufrufliste? usw. usw.).

AFAIK gibt es in Go nicht viele kontextbezogene Sprachkonstrukte. Außer go und defer gibt es keine anderen und selbst diese beiden sind sehr explizit und klar (bot in Syntax - und für das Auge - und Semantik).

Was ist mit so etwas?

(einen echten Code kopiert, an dem ich arbeite):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

Wenn err ungleich Null ist, wird bis zum Ende der "catch"-Anweisung unterbrochen. Sie können "catch" verwenden, um ähnliche Aufrufe zu gruppieren, die normalerweise denselben Fehlertyp zurückgeben. Auch wenn die Aufrufe nicht zusammenhängen, können Sie die Fehlertypen im Nachhinein überprüfen und entsprechend umschließen.

@lukescott habe diesen Blogbeitrag von @robpike gelesen https://blog.golang.org/errors-are-values

@davecheney Die

@lukescott Sie können Robs Technik heute verwenden, Sie müssen die Sprache nicht ändern.

Es gibt einen ziemlich großen Unterschied zwischen Ausnahmen und Fehlern:

  • Fehler werden erwartet (wir können einen Test dafür schreiben),
  • Ausnahmen werden nicht erwartet (daher die "Ausnahme"),

Viele Sprachen behandeln beide als Ausnahmen.

Zwischen Generika und besserer Fehlerbehandlung würde ich eine bessere Fehlerbehandlung wählen, da die meisten Code-Unordnung in Go von der Fehlerbehandlung stammt. Obwohl man sagen kann, dass diese Art von Ausführlichkeit gut ist und der Einfachheit zugute kommt, verschleiert sie IMO auch den _glücklichen Weg_ eines Workflows bis zur Mehrdeutigkeit.

Ich möchte ein wenig auf dem Vorschlag von @thejerf aufbauen.

Zuerst wird anstelle eines ! ein Operator or eingeführt, der das letzte zurückgegebene Argument vom Funktionsaufruf auf der linken Seite ist, und ruft rechts eine return-Anweisung auf, deren Ausdruck . ist eine Funktion, die aufgerufen wird, wenn das verschobene Argument nicht null ist (nicht null für Fehlertypen), und ihr dieses Argument übergibt. Es ist in Ordnung, wenn die Leute denken, dass es auch nur für Fehlertypen gelten sollte, obwohl ich denke, dass dieses Konstrukt für Funktionen nützlich sein wird, die auch einen booleschen Wert als letztes Argument zurückgeben (ist etwas in Ordnung/nicht in Ordnung).

Die Read-Methode sieht wie folgt aus:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

Ich gehe davon aus, dass das Fehlerpaket Komfortfunktionen wie die folgenden bietet:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

Dies sieht gut lesbar aus, deckt dabei alle notwendigen Punkte ab und wirkt aufgrund der Vertrautheit der return-Anweisung auch nicht zu fremd.

@urandom In dieser Aussage f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) wie kann der Grund ermittelt werden? Liegt es beispielsweise an der fehlenden Leseberechtigung oder existiert die Datei überhaupt nicht?

@dc0d
Nun, auch im obigen Beispiel ist der ursprüngliche Fehler enthalten, da die vom Benutzer angegebene Nachricht nur Kontext hinzugefügt wird. Wie bereits erwähnt und aus dem ursprünglichen Vorschlag abgeleitet, erwartet or return eine Funktion, die einen einzelnen Parameter des verschobenen Typs empfängt. Dies ist der Schlüssel und ermöglicht nicht nur Hilfsfunktionen, die für eine ziemlich große Gruppe geeignet sind, sondern Sie können auch Ihre eigenen schreiben, wenn Sie eine wirklich benutzerdefinierte Handhabung bestimmter Werte benötigen.

@urandom IMO verbirgt es zu viel.

Meine 2 Cent hier, ich möchte eine einfache Regel vorschlagen:

"impliziter Ergebnisfehlerparameter für Funktionen"

Für jede Funktion wird ein Fehlerparameter am Ende der Ergebnisparameterliste impliziert
wenn nicht explizit definiert.

Angenommen, wir haben zur Diskussion eine Funktion, die wie folgt definiert ist:

func f() (int) {}
was identisch ist mit: func f() (int, error) {}
gemäß unserer impliziten Ergebnisfehlerregel.

für die Zuweisung können Sie den Fehler wie folgt aufblasen, ignorieren oder abfangen:

1) blase auf

x := f()

Wenn f einen Fehler zurückgibt, kehrt die aktuelle Funktion sofort mit dem Fehler zurück
(oder einen neuen Fehler-Stack erstellen?)
Wenn die aktuelle Funktion main ist, wird das Programm angehalten.

Es entspricht dem folgenden Code-Snippet:

x, err := f()
wenn err != nil {
zurück ..., ähm
}

2) ignorieren

x, _ := f()

ein leerer Bezeichner am Ende der Zuweisungsausdrucksliste, um explizit zu signalisieren, dass der Fehler verworfen wird.

3) fangen

x, err := f()

err muss wie gewohnt gehandhabt werden.

Ich glaube, diese Änderung der idiomatischen Codekonvention sollte nur minimale Änderungen am Compiler erfordern
oder ein Präprozessor sollte die Arbeit erledigen.

@dc0d Können Sie ein Beispiel dafür geben, was es verbirgt und wie?

@urandom Dies ist der Grund für die Frage "Wo ist der ursprüngliche Fehler?", wie ich in einem früheren Kommentar gefragt habe. Es übergibt den Fehler implizit und es ist nicht klar (zum Beispiel), wo der ursprüngliche Fehler in dieser Zeile steht: f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . Der ursprüngliche Fehler, der von OpenFile() - das kann etwa ein Mangel an Leseberechtigung oder eine fehlende Datei sein und nicht nur "mit dem Dateinamen stimmt etwas nicht".

@dc0d
Ich bin nicht einverstanden. Es ist ungefähr so ​​klar wie der Umgang mit http.Handlers, bei denen Sie sie mit dem späteren an irgendeinen Mux übergeben, und plötzlich erhalten Sie einen Request- und einen Response-Writer. Und die Leute sind an diese Art von Verhalten gewöhnt. Woher wissen die Leute, was die Anweisung go bedeutet? Es ist offensichtlich bei der ersten Begegnung nicht klar, aber es ist ziemlich durchdringend und in der Sprache.

Ich glaube nicht, dass wir gegen einen Vorschlag sein sollten, weil er neu ist und niemand weiß, wie er funktioniert, denn das trifft auf die meisten zu.

@urandom Das macht jetzt etwas mehr Sinn (einschließlich http.Handler Beispiel).

Und wir diskutieren Dinge. Ich spreche nicht gegen oder für eine bestimmte Idee. Aber ich unterstütze Einfachheit und Offenheit und vermittle gleichzeitig ein bisschen Verstand in Bezug auf die Entwicklererfahrung.

@dc0d

Dies kann beispielsweise ein Mangel an Leseberechtigung oder eine fehlende Datei sein

In diesem Fall würden Sie den Fehler nicht einfach erneut ausgeben, sondern den tatsächlichen Inhalt überprüfen. Für mich geht es in dieser Ausgabe darum, den beliebtesten Fall abzudecken. Das heißt, Fehler beim erneuten Auslösen mit hinzugefügtem Kontext. Nur in selteneren Fällen wandeln Sie einen Fehler in einen konkreten Typ um und überprüfen, was er tatsächlich sagt. Und dafür ist die aktuelle Fehlerbehandlungssyntax völlig in Ordnung und wird nirgendwohin führen, selbst wenn einer der Vorschläge hier angenommen würde.

@creker Fehler sind keine Ausnahmen (einige frühere Kommentare von mir). Fehler sind Werte, daher ist es nicht möglich, sie zu werfen oder erneut zu werfen. Für Try/Catch-ähnliche Szenarien hat Go Panic/Recover.

@dc0d Ich spreche nicht von Ausnahmen. Mit erneutem Werfen meine ich, Fehler an den Anrufer zurückzugeben. Das vorgeschlagene or return errors.Contextf("opening file %s", filename) umschließt im Grunde einen Fehler und wirft ihn erneut aus.

@creker Danke für die Erklärung. Es fügt auch einige zusätzliche Funktionsaufrufe hinzu, die sich auf den Scheduler auswirken, der wiederum in einigen Situationen möglicherweise nicht das gewünschte Verhalten erzeugt.

@dc0d das ist ein Implementierungsdetail und kann sich in Zukunft ändern. Und es könnte sich tatsächlich ändern, nicht-kooperative Vorbeugung ist gerade in Arbeit.

@creker
Ich denke, Sie können noch mehr Fälle abdecken, als nur einen geänderten Fehler zurückzugeben:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@dc0d
Die zusätzlichen Funktionsaufrufe werden wahrscheinlich vom Compiler inline eingebunden

@urandom das sieht sehr interessant aus. Ein bisschen magisch mit impliziten Argumenten, aber dieser könnte tatsächlich allgemein und prägnant genug sein, um alles abzudecken. Nur in sehr seltenen Fällen müssen Sie auf die regulären if err != nil zurückgreifen

@urandom , dein Beispiel verwirrt mich. Warum gibt retryReadErrHandler eine Funktion zurück?

@object88
Das ist die Idee hinter dem Operator or return . Es erwartet eine Funktion, die es im Fall eines letzten zurückgegebenen Arguments ungleich Null von der linken Seite aufruft. In dieser Hinsicht verhält es sich genauso wie ein http.Handler und überlässt die eigentliche Logik der Behandlung des Arguments und seiner Rückgabe (oder der Anfrage und seiner Antwort im Fall eines Handlers) dem Callback. Und um Ihre eigenen benutzerdefinierten Daten im Callback zu verwenden, erstellen Sie eine Wrapper-Funktion, die diese Daten als Parameter empfängt und das Erwartete zurückgibt.

Oder in vertrauteren Worten, es ist ähnlich wie bei Handlern:
```gehen
func nodeHandler(repo Repo) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Daten, _ := json.Marshal(repo.GetNodes())
w.Schreiben (Daten)
})
}

@urandom , Sie könnten etwas Magie vermeiden, indem Sie den LHS wie heute or ... return in returnif (cond) ändern:

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

Dies verbessert die Allgemeinheit und Transparenz der Fehlerwerte links und der Auslösebedingung rechts.

Je mehr ich diese unterschiedlichen Vorschläge sehe, desto eher neige ich zu einer reinen Gofmt-Änderung. Die Sprache hat bereits die Macht, machen wir sie nur noch scanbarer. @billyh , eingehen , aber returnif(cond) ... ist nur eine Möglichkeit, if cond { return ...} umzuschreiben . Warum können wir nicht einfach letzteres schreiben? Wir wissen bereits, was es bedeutet.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

oder auch

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

oder

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

Keine neuen magischen Schlüsselwörter oder Syntax oder Operatoren.

(Es könnte helfen, wenn wir auch #377 korrigieren, um die Verwendung von := etwas flexibler zu machen.)

@randall77 Ich neige auch immer mehr dazu.

@randall77 An welchem ​​Punkt wird dieser Block umbrochen?

Die obige Lösung ist im Vergleich zu den hier vorgeschlagenen Alternativen angenehmer, aber ich bin nicht überzeugt, dass es besser ist, als keine Maßnahmen zu ergreifen. Gofmt sollte so deterministisch wie möglich sein.

@as , ich habe es nicht ganz durchdacht, aber vielleicht "wenn der Hauptteil einer if Anweisung eine einzelne return Anweisung enthält, dann ist die if Anweisung formatiert als eine einzige Zeile."

Möglicherweise muss die Bedingung if zusätzlich eingeschränkt werden, beispielsweise muss es sich um eine boolesche Variable oder einen binären Operator von zwei Variablen oder Konstanten handeln.

@billyh
Ich sehe keine Notwendigkeit, es ausführlicher zu machen, da ich nichts Verwirrendes von diesem kleinen bisschen Magie in or sehe. Ich gehe davon aus, dass im Gegensatz zu

@randall77
Was Sie vorschlagen, ist eher ein Vorschlag für einen Codestil, und hier ist go sehr eigenwillig. Es könnte in der Community als Ganzes nicht gut funktionieren, wenn es plötzlich zwei Formatierungsstile für if-Anweisungen gibt.

Ganz zu schweigen davon, dass solche Einzeiler viel schwieriger zu lesen sind. if != ; { } ist selbst in mehreren Zeilen zu viel, daher dieser Vorschlag. Das Muster ist für fast alle Fälle festgelegt und kann in leicht lesbaren und verständlichen syntaktischen Zucker umgewandelt werden.

Das Problem, das ich bei den meisten dieser Vorschläge habe, ist, dass nicht klar ist, was los ist. Im Eröffnungspost wird vorgeschlagen, || um einen Fehler zurückzugeben. Mir ist nicht klar, dass dort eine Rückkehr stattfindet. Ich denke, wenn eine neue Syntax erfunden werden soll, muss sie den Erwartungen der meisten Leute entsprechen. Wenn ich || sehe, erwarte ich keine Rückkehr oder sogar eine Unterbrechung der Ausführung. Es nervt mich.

Ich mag Gos "Fehler sind Werte"-Sentiment, aber ich stimme auch zu, dass if err := expression; err != nil { return err } zu ausführlich ist, hauptsächlich weil fast jeder Aufruf einen Fehler zurückgibt. Dies bedeutet, dass Sie viele davon haben werden, und es ist leicht, es zu vermasseln, je nachdem, wo err deklariert (oder überschattet) wird. Es ist mit unserem Code passiert.

Da Go kein try/catch und panic/defer für "außergewöhnliche" Umstände verwendet, haben wir möglicherweise die Möglichkeit, die Schlüsselwörter try und/oder catch wiederzuverwenden, um die Fehlerbehandlung zu verkürzen, ohne das Programm abzustürzen.

Hier ist ein Gedanke, den ich hatte:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

Der Gedanke ist, dass Sie err auf der linken Seite das Schlüsselwort try voranstellen. Wenn err nicht null ist, erfolgt eine sofortige Rückgabe. Sie müssen hier keinen Haken setzen, es sei denn, die Rückgabe ist nicht vollständig zufrieden. Dies entspricht eher den Erwartungen der Leute, dass "Versuch unterbricht die Ausführung", aber anstatt das Programm zum Absturz zu bringen, kehrt es einfach zurück.

Wenn die Rückgabe nicht vollständig erfüllt ist (Zeitprüfung kompilieren) oder wir den Fehler einschließen möchten, können wir catch als spezielles Forward-only-Label wie folgt verwenden:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

Auf diese Weise können Sie auch bestimmte Fehler überprüfen und ignorieren:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

Vielleicht könnten Sie sogar try das Label angeben lassen:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

Mir ist klar, dass meine Codebeispiele irgendwie scheiße sind (foo/bar, ack). Aber hoffentlich habe ich illustriert, was ich damit meine, mit/gegen bestehende Erwartungen zu gehen. Ich wäre auch völlig in Ordnung, Fehler so zu belassen, wie sie in Go 1 sind. Aber wenn eine neue Syntax erfunden wird, muss sorgfältig darüber nachgedacht werden, wie diese Syntax bereits wahrgenommen wird, nicht nur in Go. Es ist schwer, eine neue Syntax zu erfinden, ohne dass sie bereits etwas bedeutet, daher ist es oft besser, sich an bestehende Erwartungen zu halten, anstatt sie zu widerlegen.

Vielleicht eine Art Verkettung, wie Sie Methoden verketten können, aber auf Fehler? Ich bin mir nicht sicher, wie das aussehen würde oder ob es überhaupt funktionieren würde, nur eine wilde Idee.
Sie können jetzt eine Kette sortieren, um die Anzahl der Fehlerprüfungen zu reduzieren, indem Sie eine Art Fehlerwert innerhalb einer Struktur beibehalten und ihn am Ende der Kette extrahieren.

Es ist eine sehr merkwürdige Situation, denn obwohl es ein bisschen Boilerplate gibt, bin ich mir nicht ganz sicher, wie ich es weiter vereinfachen soll, während es dennoch Sinn macht.

Der Beispielcode von @thejerf sieht mit dem Vorschlag von @lukescott so aus :

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

Es geht von 59 Zeilen auf 47 herunter.

Dies ist die gleiche Länge, aber ich denke, es ist etwas klarer als die Verwendung von defer :

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

Dieses Beispiel wäre wahrscheinlich einfacher zu folgen mit einem deferifnotnil oder so.
Aber das geht irgendwie auf die ganze eine Zeile zurück, um die es bei vielen dieser Vorschläge geht.

Nachdem ich ein wenig mit dem Beispielcode gespielt habe, bin ich jetzt gegen die try(label) name Variante. Ich denke, wenn Sie mehrere ausgefallene Dinge zu tun haben, verwenden Sie einfach das aktuelle System von if err != nil { ... } . Wenn Sie im Wesentlichen dasselbe tun, z. B. eine benutzerdefinierte Fehlermeldung festlegen, können Sie Folgendes tun:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

Wenn jemand Ruby verwendet hat, sieht dies sehr nach ihrer rescue Syntax aus, die sich meiner Meinung nach ziemlich gut liest.

Eine Möglichkeit besteht darin, nil einem falschen Wert zu machen und andere Werte zu wahr auszuwerten, sodass Sie am Ende Folgendes erhalten:

err := doSomething()
if err { return err }

Aber ich bin mir nicht sicher, ob das wirklich funktionieren würde und es rasiert nur ein paar Charaktere.
Ich habe viele Dinge getippt, aber ich glaube nicht, dass ich jemals != nil getippt habe.

Es wurde bereits erwähnt, Schnittstellen wahrheitsgetreu/falsch zu machen, und ich sagte, dass Fehler mit Flags häufiger auftreten würden:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , in dem oben angegebenen Beispiel sind Fehlermeldungen mit Happy-Path-Code durchsetzt, was für mich etwas seltsam ist. Wenn Sie diese Zeichenfolgen formatieren müssen, müssen Sie viel zusätzliche Arbeit leisten, unabhängig davon, ob etwas schief geht oder nicht:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@object88 , ich denke, dass die SSA-Analyse in der Lage sein sollte, herauszufinden, ob bestimmte Zuweisungen nicht verwendet werden, und sie so

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

Wäre das legal?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

Ich denke, das sollte eine Schleife machen, bis doThing() Null zurückgibt, aber ich könnte vom Gegenteil überzeugt werden.

@carlmjohnson

Nachdem ich ein wenig mit dem Beispielcode gespielt habe, bin ich jetzt gegen die Namensvariante try(label).

Ja, ich war mir bei der Syntax nicht sicher. Ich mag es nicht, weil es try wie ein Funktionsaufruf aussehen lässt. Aber ich konnte den Wert der Angabe eines anderen Labels erkennen.

Wäre das legal?

Ich würde ja sagen, weil Versuch nur vorwärts sein sollte. Wenn Sie das tun wollten, würde ich sagen, dass Sie es so machen müssen:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

Oder so:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

Eine Möglichkeit besteht darin, nil zu einem falschen Wert zu machen und andere Werte zu wahr auszuwerten, sodass Sie am Ende erhalten: err := doSomething() if err { return err }

Ich denke, es lohnt sich, es zu kürzen. Ich denke jedoch, dass es nicht in allen Situationen auf Null zutreffen sollte. Vielleicht könnte es eine neue Schnittstelle wie diese geben:

interface Truthy {
  True() bool
}

Dann kann jeder Wert, der diese Schnittstelle implementiert, wie von Ihnen vorgeschlagen verwendet werden.

Dies würde funktionieren, solange der Fehler die Schnittstelle implementiert:

err := doSomething()
if err { return err }

Aber das würde nicht funktionieren:

err := doSomething()
if err == true { return err } // err is not true

Ich bin wirklich neu in Golang, aber wie denkst du über die Einführung eines bedingten Delegators wie unten?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

Delegator ist eine Funktion wie Sachen, aber

  • Sein return bedeutet return from its caller context . (Rückgabeart muss mit Anrufer identisch sein)
  • Es dauert optional if vor { , im obigen Beispiel ist es if err != nil .
  • Es muss vom Anrufer mit dem Schlüsselwort delegate delegiert werden

Es kann sein, dass delegate zum Delegieren weggelassen werden kann, aber ich denke, es macht den Fluss der Funktion schwer zu lesen.

Und vielleicht ist es nicht nur für die Fehlerbehandlung nützlich, bin mir jetzt aber nicht sicher.

Es ist schön, check hinzuzufügen, aber Sie können noch mehr tun, bevor Sie zurückkehren:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

wird

result, err := openFile(f);
check err

```Los
Ergebnis, err := openFile(f);
prüfe Fehler {
log.Println(..., err)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

```Los
Ergebnis, err := check openFile(f) {
log.Println(..., err)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

Es versucht auch, die (von manchen als mühsam empfundene) Fehlerbehandlung zu vereinfachen (#21161). Es würde werden:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Natürlich können Sie try und andere Schlüsselwörter anstelle von check , wenn es klarer ist.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

```Los
Ergebnis, err := openFile(f);
versuch es mal {
// Fehler behandeln und zurückgeben
log.Println(..., err)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

wird

check UpdateDB()

Für eine Funktion mit mehreren Rückgabewerten müssen Sie wie jetzt zuweisen.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

wird

a, b, err := Foo()
check err

// use a and b

2. Form (Check A, B)
check A, B wertet A aus. Wenn null, tut es nichts. Wenn nicht null, wirkt check wie eine Rückgabe {}*, B.

Dies ist für Fehler-Dekorationsbedürfnisse. Wir prüfen immer noch auf A, aber B wird in der impliziten Rückgabe verwendet.

Beispiel

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

wird

a, err := Foo()
check err, &BarError{"Bar", err}

Anmerkungen
Es ist ein Kompilierungsfehler zu

Verwenden Sie die Check-Anweisung für Dinge, die nicht als Fehler ausgewertet werden
Verwenden Sie check in einer Funktion mit Rückgabewerten nicht in der Form { type }*, error
Die Zwei-Ausdrücke-Formprüfung A, B ist kurzgeschlossen. B wird nicht ausgewertet, wenn A Null ist.

Hinweise zur Praktikabilität
Es gibt Unterstützung für das Dekorieren von Fehlern, aber Sie zahlen nur dann für die klobigere Syntax von Check A, B, wenn Sie tatsächlich Fehler dekorieren müssen.

Für die if err != nil { return nil, nil, err } Boilerplate (die sehr häufig vorkommt) ist die Prüfung err so kurz wie möglich, ohne die Klarheit zu beeinträchtigen (siehe Hinweis zur Syntax unten).

Hinweise zur Syntax
Ich würde argumentieren, dass diese Art von Syntax (check .., am Anfang der Zeile, ähnlich einer Rückgabe) eine gute Möglichkeit ist, Fehler bei der Überprüfung von Boilerplate zu beseitigen, ohne die Unterbrechung des Kontrollflusses zu verbergen, die implizite Rückgaben einführen.

Eine Kehrseite von Ideen wie dem||undFangoben, oder die a, b = foo()? in einem anderen Thread vorgeschlagen, ist, dass sie die Kontrollflussmodifikation auf eine Weise verbergen, die es schwieriger macht, dem Fluss zu folgen; ersteres mit ||Maschinen, die am Ende einer ansonsten schlicht aussehenden Zeile angehängt werden, letztere mit einem kleinen Symbol, das überall erscheinen kann, auch in der Mitte und am Ende einer schlicht aussehenden Codezeile, möglicherweise mehrmals.

Eine check-Anweisung wird im aktuellen Block immer auf der obersten Ebene sein und hat dieselbe Bedeutung wie andere Anweisungen, die den Kontrollfluss ändern (z. B. eine vorzeitige Rückkehr).

Hier ist noch ein Gedanke.

Stellen Sie sich eine again Anweisung vor, die ein Makro mit einem Label definiert. Die Anweisung, die es beschriftet, kann später in der Funktion durch textuelle Ersetzung wieder erweitert werden (erinnert an const/iota, mit Schattierungen von goto :-] ).

Beispielsweise:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

wäre genau äquivalent zu:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

Beachten Sie, dass die Makroerweiterung keine Argumente hat - dies bedeutet, dass es weniger Verwirrung darüber geben sollte, dass es sich um ein Makro handelt, da der Compiler keine eigenen Symbole mag .

Wie bei der goto-Anweisung liegt der Gültigkeitsbereich des Labels innerhalb der aktuellen Funktion.

Interessante Idee. Ich mochte die Idee mit dem Catch-Label, aber ich glaube nicht, dass es gut zu Go-Bereichen passte (mit dem aktuellen Go können Sie kein Label mit neuen Variablen in seinem Bereich goto ). Die Idee von again behebt dieses Problem, da das Label kommt, bevor neue Bereiche eingeführt werden.

Hier nochmal das Mega-Beispiel:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Hier ist eine Version, die Rogs Vorschlag näher kommt (ich mag es nicht so sehr):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson Fürs Protokoll, das ist nicht ganz das, was ich vorschlage. Der Bezeichner "check" ist nicht speziell - er muss deklariert werden, indem er nach dem Schlüsselwort "again" eingefügt wird.

Außerdem würde ich vorschlagen, dass das obige Beispiel seine Verwendung nicht sehr gut veranschaulicht - es gibt nichts in der oben wiederbeschrifteten Anweisung, das nicht genauso gut in einer defer-Anweisung ausgeführt werden könnte. Im try/catch-Beispiel kann dieser Code (beispielsweise) den Fehler nicht mit Informationen über die Quellposition der Fehlerrückgabe umschließen. Es funktioniert auch nicht AFAICS, wenn Sie ein "try" in eine dieser if-Anweisungen einfügen (z innerhalb des Blocks deklariert.

Ich denke, mein einziges Problem mit einem check Schlüsselwort ist, dass alle Exits zu einer Funktion ein return (oder zumindest eine _irgendwie_ Art von "Ich werde die Funktion verlassen"-Konnotation haben). Wir _könnten_ become (für TCO) bekommen, mindestens become hat eine Art "Wir werden eine andere Funktion"... aber das Wort "Check" klingt wirklich nicht nach es wird ein Ausgang für die Funktion sein.

Der Austrittspunkt einer Funktion ist extrem wichtig, und ich bin mir nicht sicher, ob check wirklich dieses "Austrittspunkt"-Gefühl hat. Abgesehen davon gefällt mir die Idee von check , es ermöglicht eine viel kompaktere Fehlerbehandlung, ermöglicht aber dennoch, jeden Fehler anders zu behandeln oder den Fehler nach Ihren Wünschen einzuschließen.

Kann ich auch einen Vorschlag hinzufügen?
Was ist mit so etwas:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

Die Fehlerzuordnung kann jede Form haben, auch so etwas wie Dummes sein

f := Open(name) =: e

Oder geben Sie im Fehlerfall einen anderen Satz von Werten zurück, nicht nur Fehler, und auch ein try-catch-Block wäre schön.

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers Ich persönlich glaube, dass es sehr schön ist, wenn Fehler in Go nicht besonders behandelt werden. Es sind einfach Werte, und ich denke, das sollte auch so bleiben.

Außerdem ist Try-Catch (und ähnliche Konstrukte) einfach ein schlechtes Konstrukt, das schlechte Praktiken fördert. Jeder Fehler sollte separat behandelt werden, nicht von einem "catch all"-Fehlerhandler.

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
das ist zu kompliziert.

meine Idee: |err| bedeutet Prüffehler: if err!=nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@chen56 und alle zukünftigen Kommentatoren: Siehe https://go.googlesource.com/proposal/+/master/design/go2draft.md .

Ich vermute, dass dies diesen Thread jetzt überholt und es wenig Sinn macht, hier weiterzumachen. Auf der Wiki-Feedback-Seite sollten die Dinge wahrscheinlich in Zukunft hingehen.

Das Mega-Beispiel mit dem Go 2-Vorschlag:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Ich denke, das ist ungefähr so ​​sauber, wie wir hoffen können. Der Block handle hat die guten Eigenschaften des Labels again oder Rubys Schlüsselwort rescue . Die einzigen Fragen, die mir im Kopf bleiben, sind, ob ich Satzzeichen oder ein Schlüsselwort verwenden soll (ich denke, Schlüsselwort) und ob man den Fehler erhalten kann, ohne ihn zurückzugeben.

Ich versuche, den Vorschlag zu verstehen - es scheint, dass es nur einen Handle-Block pro Funktion gibt, anstatt die Möglichkeit, während der Funktionsausführungsprozesse unterschiedliche Reaktionen auf unterschiedliche Fehler zu erstellen. Das scheint eine echte Schwäche zu sein.

Ich frage mich auch, ob wir den kritischen Bedarf an der Entwicklung von Testkabelbäumen auch in unseren Systemen übersehen. Wenn man bedenkt, wie wir Fehlerpfade bei Tests ausüben werden, sollte dies ein Teil der Diskussion sein, aber ich sehe das auch nicht,

@sdwarwick Ich glaube nicht, dass dies der beste Ort ist, um den unter https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md beschriebenen Designentwurf zu diskutieren. Ein besserer Ansatz besteht darin, einen Link zu einem Bericht auf der Wiki-Seite unter https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback hinzuzufügen.

Allerdings erlaubt dieser Designentwurf mehrere Handle-Blöcke in einer Funktion.

Dieses Thema begann als ein konkreter Vorschlag. Wir werden diesen Vorschlag nicht annehmen. Es hat viele großartige Diskussionen zu diesem Thema gegeben, und ich hoffe, dass die Leute die guten Ideen in separate Vorschläge und in die Diskussion über den jüngsten Designentwurf einfließen lassen. Ich werde dieses Thema abschließen. Danke für die ganze Diskussion.

Wenn in der Menge dieser Beispiele zu sprechen:

r, err := os.Open(src)
    if err != nil {
        return err
    }

Das möchte ich in einer Zeile ungefähr so ​​schreiben:

r, err := os.Open(src) try ("blah-blah: %v", err)

Anstelle von "versuchen" kannst du ein beliebiges schönes und passendes Wort eingeben.

Bei einer solchen Syntax würde der Fehler zurückgegeben und der Rest wären je nach Typ einige Standardwerte. Wenn ich zusammen mit einem Fehler und etwas anderem Spezifischen als Standard zurückgeben muss, bricht niemand die klassische mehrzeilige Option ab.

Noch kürzer (ohne irgendeine Art von Fehlerbehandlung hinzuzufügen):

r, err := os.Open(src) try

)
PS Entschuldigung für mein Englisch))

Meine Variante:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

Hallo,

Ich habe eine einfache Idee, die lose darauf basiert, wie die Fehlerbehandlung in der Shell funktioniert, genau wie der ursprüngliche Vorschlag. In der Shell werden Fehler durch Rückgabewerte ungleich Null kommuniziert. Der Rückgabewert des letzten Befehls/Aufrufs wird in $? in der Schale. Zusätzlich zu dem vom Benutzer vergebenen Variablennamen könnten wir den Fehlerwert des letzten Aufrufs automatisch in einer vordefinierten Variablen speichern und durch eine vordefinierte Syntax überprüfen lassen. Ich habe gewählt ? als Syntax zum Verweisen auf den neuesten Fehlerwert, der von einem Funktionsaufruf im aktuellen Gültigkeitsbereich zurückgegeben wurde. Ich habe gewählt ! als Abkürzung für wenn ? != null {}. Die Wahl für ? durch die Hülle beeinflusst wird, sondern auch, weil es sinnvoll erscheint. Wenn ein Fehler auftritt, interessiert Sie natürlich, was passiert ist. Dies wirft eine Frage auf. ? ist das häufigste Zeichen für eine gestellte Frage und wird daher verwendet, um auf den letzten Fehlerwert zu verweisen, der im selben Bereich generiert wurde.
! wird als Abkürzung für if ? != nil, weil es bedeutet, dass man aufpassen muss, falls etwas schief gelaufen ist. ! bedeutet: Wenn etwas schief gelaufen ist, tun Sie dies. ? verweist auf den letzten Fehlerwert. Wie üblich ist der Wert von ? gleich Null, wenn kein Fehler aufgetreten ist.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

Um die Syntax ansprechender zu gestalten, würde ich die Platzierung des ! Linie direkt hinter dem Anruf sowie das Weglassen der geschweiften Klammern.
Mit diesem Vorschlag können Sie auch Fehler behandeln, ohne einen vom Programmierer definierten Bezeichner zu verwenden.

Dies wäre erlaubt:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

Das wäre erlaubt

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

Bei diesem Vorschlag müssen Sie die Funktion nicht verlassen, wenn ein Fehler auftritt
und Sie können stattdessen versuchen, den Fehler zu beheben.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

Unter diesem Vorschlag könnten Sie ! in einer for-Schleife für mehrere Wiederholungen wie diese.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

Das ist mir bewusst! wird hauptsächlich für die Negation von Ausdrücken verwendet, daher könnte die vorgeschlagene Syntax bei Uneingeweihten Verwirrung stiften. Die Idee ist das! von selbst erweitert auf ? != nil, wenn es in einem bedingten Ausdruck in einem Fall verwendet wird, wie das obere Beispiel zeigt, wo es keinem bestimmten Ausdruck angehängt ist. Die obere for-Zeile kann mit dem aktuellen go nicht kompiliert werden, da sie ohne Kontext keinen Sinn macht. Unter diesem Vorschlag! allein ist wahr, wenn beim letzten Funktionsaufruf ein Fehler aufgetreten ist, der einen Fehler zurückgeben kann.

Die return-Anweisung zum Zurückgeben des Fehlers wird beibehalten, da es, wie andere hier kommentiert haben, wünschenswert ist, auf einen Blick zu sehen, wo Ihre Funktion zurückkehrt. Sie können diese Syntax in einem Szenario verwenden, in dem Sie die Funktion aufgrund eines Fehlers nicht verlassen müssen.

Dieser Vorschlag ist einfacher als einige andere Vorschläge, da es keinen Aufwand gibt, eine aus anderen Sprachen bekannte Variante der try-and-catch-Block-ähnlichen Syntax zu erstellen. Es behält die aktuelle Philosophie von go bei, Fehler direkt dort zu behandeln, wo sie auftreten, und macht dies prägnanter.

@tobimensch bitte Go 2 Error Handling Feedback Wiki . Beiträge zu diesem geschlossenen Thema können übersehen werden.

Wenn Sie es noch nicht gesehen haben, möchten Sie vielleicht den Go 2 Error Handling Draft Design lesen.

Vielleicht interessieren Sie sich auch für Anforderungen, die für die Fehlerbehandlung von Go 2 zu berücksichtigen sind .

Es mag ein bisschen zu spät sein, um darauf hinzuweisen, aber alles, was sich wie Javascript-Magie anfühlt, stört mich. Ich spreche von dem || Operator, der auf magische Weise mit einer error Schnittstelle funktionieren sollte. Ich mag es nicht.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen