Go: предложение: Перейти 2: упростите обработку ошибок с помощью || суффикс err

Созданный на 25 июл. 2017  ·  519Комментарии  ·  Источник: golang/go

Было много предложений по упрощению обработки ошибок в Go, и все они основывались на общей жалобе на то, что слишком много кода Go содержит строки

if err != nil {
    return err
}

Я не уверен, что здесь есть проблема, которую нужно решить, но, поскольку она постоянно возникает, я собираюсь выдвинуть эту идею.

Одна из основных проблем большинства предложений по упрощению обработки ошибок заключается в том, что они упрощают только два способа обработки ошибок, но на самом деле их три:

  1. игнорировать ошибку
  2. вернуть ошибку без изменений
  3. вернуть ошибку с дополнительной контекстной информацией

Игнорировать ошибку уже легко (возможно, слишком просто) (см. №20803). Многие существующие предложения по обработке ошибок упрощают возврат ошибки без изменений (например, # 16225, # 18721, # 21146, # 21155). Немногие упрощают возврат ошибки с дополнительной информацией.

Это предложение частично основано на языках оболочки Perl и Bourne, плодотворных источниках языковых идей. Мы вводим новый тип оператора, похожего на оператор выражения: выражение вызова, за которым следует || . Грамматика:

PrimaryExpr Arguments "||" Expression

Точно так же мы вводим новый тип оператора присваивания:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Хотя грамматика принимает любой тип после || в случае отсутствия присваивания, единственным разрешенным типом является предварительно объявленный тип error . Выражение, следующее за || должно иметь тип, присваиваемый error . Это может быть не логический тип, даже не именованный логический тип, присваиваемый error . (Это последнее ограничение требуется для обеспечения обратной совместимости данного предложения с существующим языком.)

Эти новые типы операторов разрешены только в теле функции, которая имеет хотя бы один параметр результата, а тип последнего параметра результата должен быть предопределенным типом error . Вызываемая функция также должна иметь по крайней мере один параметр результата, а тип последнего параметра результата должен быть предопределенным типом error .

При выполнении этих операторов выражение вызова оценивается как обычно. Если это оператор присваивания, результаты вызова, как обычно, присваиваются левым операндам. Затем результат последнего вызова, который, как описано выше, должен иметь тип error , сравнивается с nil . Если результат последнего вызова не равен nil , неявно выполняется инструкция возврата. Если вызывающая функция имеет несколько результатов, для всех результатов, кроме последнего, возвращается нулевое значение. Выражение, следующее за || , возвращается как последний результат. Как описано выше, последний результат вызывающей функции должен иметь тип error , а выражение должно быть присвоено типу error .

В случае без присваивания выражение оценивается в области видимости, в которой вводится новая переменная err и устанавливается значение последнего результата вызова функции. Это позволяет выражению легко ссылаться на ошибку, возвращаемую вызовом. В случае присваивания выражение оценивается в рамках результатов вызова и, таким образом, может напрямую относиться к ошибке.

Это полное предложение.

Например, функция os.Chdir в настоящее время

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

В соответствии с этим предложением это можно было бы записать как

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

Я пишу это предложение в основном для того, чтобы побудить людей, которые хотят упростить обработку ошибок Go, подумать о способах упростить перенос контекста вокруг ошибок, а не просто вернуть ошибку без изменений.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Самый полезный комментарий

Простая идея с поддержкой оформления ошибок, но требующая более радикального изменения языка (очевидно, не для go1.10) - это введение нового ключевого слова check .

У него будет две формы: check A и check A, B .

И A и B должны быть error . Вторая форма будет использоваться только при исправлении ошибок; люди, которые не нуждаются или не хотят приукрашивать свои ошибки, будут использовать более простую форму.

1-я форма (отметка А)

check A оценивает A . Если nil , ничего не делает. Если не nil , check действует как return {<zero>}*, A .

Примеры

  • Если функция просто возвращает ошибку, ее можно использовать в строке с check , поэтому
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

становится

check UpdateDB()
  • Для функции с несколькими возвращаемыми значениями вам нужно будет назначить, как мы это делаем сейчас.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

становится

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

// use a and b

2-я форма (отметьте А, Б)

check A, B оценивает A . Если nil , ничего не происходит. Если не nil , check действует как return {<zero>}*, B .

Это для устранения ошибок. Мы все еще проверить на A , но B , который используется в неявном return .

Пример

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

становится

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

Примечания

Ошибка компиляции

  • используйте оператор check для вещей, которые не оцениваются как error
  • используйте check в функции с возвращаемыми значениями не в форме { type }*, error

Форма с двумя выражениями check A, B замкнута накоротко. B не оценивается, если A равно nil .

Примечания по практичности

Есть поддержка декорирования ошибок, но вы платите за неуклюжий синтаксис check A, B только тогда, когда вам действительно нужно украсить ошибки.

Для шаблона if err != nil { return nil, nil, err } (который очень распространен) check err настолько краток, насколько это возможно, без ущерба для ясности (см. Примечание к синтаксису ниже).

Примечания по синтаксису

Я бы сказал, что такой синтаксис ( check .. в начале строки, аналог return ) - хороший способ устранить шаблон проверки ошибок, не скрывая нарушения потока управления, которое неявные возвраты вводят.

Обратной стороной таких идей, как <do-stuff> || <handle-err> и <do-stuff> catch <handle-err> выше или a, b = foo()? предложенных в другом потоке, является то, что они скрывают модификацию потока управления таким образом, что это затрудняет поток. следовать; первый с механизмом || <handle-err> добавленным в конец простой на вид строки, второй с небольшим символом, который может появляться повсюду, в том числе в середине и в конце простой на вид строки кода, возможно несколько раз.

Оператор check всегда будет на верхнем уровне в текущем блоке, имея такое же значение, как и другие операторы, изменяющие поток управления (например, ранний return ).

Все 519 Комментарий

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

Откуда оттуда взяли e ? Опечатка?

Или вы имели в виду:

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

(То есть, неявная проверка err! = Nil сначала назначает ошибку параметру результата, которому можно присвоить имя, чтобы изменить его снова перед неявным возвратом?)

Вздох, испортил мой собственный пример. Теперь исправлено: e должно быть err . Предложение помещает err в область видимости для хранения значения ошибки вызова функции, когда она не находится в операторе присваивания.

Хотя я не уверен, согласен ли я с идеей или синтаксисом, я должен отдать вам должное за то, что вы уделили внимание добавлению контекста к ошибкам, прежде чем возвращать их.

Это может быть интересно для @davecheney , написавшего https://github.com/pkg/errors.

Что происходит в этом коде:

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

(Приношу свои извинения, если это невозможно даже без части || &PathError{"chdir", dir, e} ; я пытаюсь выразить, что это похоже на сбивающее с толку переопределение существующего поведения, а неявные возвраты ... скрытны?)

@ object88 Я был бы в порядке, если бы не разрешил этот новый случай в SimpleStmt, который используется в операторах if и for и switch . Вероятно, это было бы лучше, хотя это немного усложнило бы грамматику.

Но если мы этого не сделаем, то произойдет следующее: если thing.Nope() вернет ошибку, отличную от нуля, тогда вызывающая функция вернется с &PathError{"chdir", dir, err} (где err - это переменная, установленная вызовом thing.Nope() ). Если thing.Nope() возвращает ошибку nil , то мы точно знаем, что err == nil истинно в условии оператора if , и поэтому тело если инструкция выполняется. Переменная ignoreError никогда не читается. Здесь нет двусмысленности или отмены существующего поведения; обработка || представленная здесь, принимается только тогда, когда выражение после || не является логическим значением, что означает, что в настоящее время оно не будет компилироваться.

Я согласен с тем, что неявные возвраты ненадежны.

Да, мой пример довольно плохой. Но запрещение операции внутри if , for или switch решит множество потенциальных проблем.

Поскольку панель для рассмотрения - это то, что трудно сделать на языке как есть, я решил посмотреть, насколько сложно этот вариант кодировать на языке. Не намного сложнее остальных: https://play.golang.org/p/9B3Sr7kj39

Мне действительно не нравятся все эти предложения о том, чтобы сделать один тип значения и одну позицию в возвращаемых аргументах особенными. Это в некотором смысле даже хуже, потому что оно также делает err специальным именем в этом конкретном контексте.

Хотя я, конечно, согласен с тем, что люди (включая меня!) Должны больше уставать возвращать ошибки без дополнительного контекста.

Когда есть другие возвращаемые значения, например

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

это определенно может утомить читать. Мне несколько понравилось предложение @nigeltao для return ..., err в https://github.com/golang/go/issues/19642#issuecomment -288559297

Если я правильно понимаю, для построения синтаксического дерева синтаксическому анализатору необходимо знать типы переменных, чтобы различать

boolean := BoolFunc() || BoolExpr

и

err := FuncReturningError() || Expr

Это не выглядит хорошо.

меньше - больше...

Когда возвращаемый ExpressionList содержит два или более элементов, как это работает?

Кстати, вместо этого я хочу panicIf.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor В err все еще не объявлен явно, а указан как «волшебный» (язык предопределен), верно?

Или это будет что-то вроде

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

?

С другой стороны (поскольку он уже отмечен как "смена языка" ...)
Введите новый оператор (!! или ??), который выполняет ярлык при ошибке! = Nil (или любой другой, допускающий значение NULL?)

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

Извините, если это слишком далеко :)

Я согласен с тем, что обработка ошибок в Go может повторяться. Я не против повторения, но их слишком много влияет на читабельность. Есть причина, по которой «Цикломатическая сложность» (верите вы в это или нет) использует потоки управления в качестве меры сложности. Оператор «if» добавляет лишнего шума.

Однако предлагаемый синтаксис «||» не очень интуитивно понятен для чтения, тем более что этот символ широко известен как оператор ИЛИ. Кроме того, как поступать с функциями, возвращающими несколько значений и ошибок?

Я просто подбрасываю сюда несколько идей. Как насчет того, чтобы вместо использования ошибки в качестве вывода мы использовали ошибку в качестве входных данных? Пример: https://play.golang.org/p/rtfoCIMGAb

Спасибо за все комментарии.

@opennota Хорошее замечание. Это все еще может работать, но я согласен, что это неудобно.

@mattn Я не думаю, что есть возвращаемый ExpressionList, поэтому я не уверен, о чем вы спрашиваете. Если вызывающая функция имеет несколько результатов, все, кроме последнего, возвращаются как нулевое значение типа.

@mattn panicif не затрагивает один из ключевых элементов этого предложения, что является простым способом вернуть ошибку с дополнительным контекстом. И, конечно, сегодня достаточно легко написать panicif .

@tandr Да, err определяется волшебным образом, что довольно ужасно. Другая возможность - разрешить выражению ошибки использовать error для ссылки на ошибку, что по-другому ужасно.

@tandr Мы могли бы использовать другой оператор, но я не вижу большого преимущества. Похоже, это не делает результат более читаемым.

@henryas Я думаю, это предложение объясняет, как оно обрабатывает несколько результатов.

@henryas Спасибо за пример. Что мне не нравится в таком подходе, так это то, что он делает обработку ошибок наиболее важным аспектом кода. Я хочу, чтобы обработка ошибок присутствовала и была видна, но я не хочу, чтобы это было первым делом. Это верно сегодня, с идиомой if err != nil и отступом кода обработки ошибок, и это должно оставаться верным, если какие-либо новые функции добавлены для обработки ошибок.

Еще раз спасибо.

@ianlancetaylor Я не знаю, проверяли ли вы мою ссылку на игровую площадку, но там была опция panicIf, позволяющая добавить дополнительный контекст.

Я воспроизведу здесь несколько упрощенную версию:

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

По совпадению, я только что произнес молниеносный доклад на GopherCon, где я использовал (но серьезно не предлагал) немного синтаксиса, чтобы помочь в визуализации кода обработки ошибок. Идея состоит в том, чтобы отложить этот код в сторону, чтобы убрать его с пути основного потока, не прибегая к каким-либо фокусам для сокращения кода. Результат выглядит как

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

где =: - это новый бит синтаксиса, зеркало := которое присваивается в другом направлении. Очевидно, что нам также понадобится что-то для = , что, по общему признанию, проблематично. Но общая идея состоит в том, чтобы облегчить читателю понимание счастливого пути без потери информации.

С другой стороны, текущий способ обработки ошибок имеет некоторые достоинства в том, что он служит ярким напоминанием о том, что вы, возможно, делаете слишком много вещей в одной функции, и некоторый рефакторинг может быть запоздалым.

Мне очень нравится синтаксис, предложенный здесь @billyh

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

или более сложный пример с использованием 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
}

Простая идея с поддержкой оформления ошибок, но требующая более радикального изменения языка (очевидно, не для go1.10) - это введение нового ключевого слова check .

У него будет две формы: check A и check A, B .

И A и B должны быть error . Вторая форма будет использоваться только при исправлении ошибок; люди, которые не нуждаются или не хотят приукрашивать свои ошибки, будут использовать более простую форму.

1-я форма (отметка А)

check A оценивает A . Если nil , ничего не делает. Если не nil , check действует как return {<zero>}*, A .

Примеры

  • Если функция просто возвращает ошибку, ее можно использовать в строке с check , поэтому
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

становится

check UpdateDB()
  • Для функции с несколькими возвращаемыми значениями вам нужно будет назначить, как мы это делаем сейчас.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

становится

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

// use a and b

2-я форма (отметьте А, Б)

check A, B оценивает A . Если nil , ничего не происходит. Если не nil , check действует как return {<zero>}*, B .

Это для устранения ошибок. Мы все еще проверить на A , но B , который используется в неявном return .

Пример

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

становится

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

Примечания

Ошибка компиляции

  • используйте оператор check для вещей, которые не оцениваются как error
  • используйте check в функции с возвращаемыми значениями не в форме { type }*, error

Форма с двумя выражениями check A, B замкнута накоротко. B не оценивается, если A равно nil .

Примечания по практичности

Есть поддержка декорирования ошибок, но вы платите за неуклюжий синтаксис check A, B только тогда, когда вам действительно нужно украсить ошибки.

Для шаблона if err != nil { return nil, nil, err } (который очень распространен) check err настолько краток, насколько это возможно, без ущерба для ясности (см. Примечание к синтаксису ниже).

Примечания по синтаксису

Я бы сказал, что такой синтаксис ( check .. в начале строки, аналог return ) - хороший способ устранить шаблон проверки ошибок, не скрывая нарушения потока управления, которое неявные возвраты вводят.

Обратной стороной таких идей, как <do-stuff> || <handle-err> и <do-stuff> catch <handle-err> выше или a, b = foo()? предложенных в другом потоке, является то, что они скрывают модификацию потока управления таким образом, что это затрудняет поток. следовать; первый с механизмом || <handle-err> добавленным в конец простой на вид строки, второй с небольшим символом, который может появляться повсюду, в том числе в середине и в конце простой на вид строки кода, возможно несколько раз.

Оператор check всегда будет на верхнем уровне в текущем блоке, имея такое же значение, как и другие операторы, изменяющие поток управления (например, ранний return ).

@ALTree , я не понял, как твой пример:

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

Достигает трехзначной отдачи от оригинала:

return "", "", err

Он просто возвращает нулевые значения для всех заявленных возвратов, кроме последней ошибки? Как насчет случаев, когда вы хотите вернуть действительное значение вместе с ошибкой, например количество байтов, записанных при сбое Write ()?

Какое бы решение мы ни выбрали, оно должно минимально ограничивать универсальность обработки ошибок.

Что касается значения наличия check в начале строки, я лично предпочитаю видеть основной поток управления в начале каждой строки и чтобы обработка ошибок как можно меньше влияла на читаемость этого основного потока управления. насколько возможно. Кроме того, если обработка ошибок отделена зарезервированным словом, например check или catch , то практически любой современный редактор каким-то образом

@billyh это объяснено выше, в строке, которая гласит:

Если не nil, проверка действует как return {<zero>}*, A

check вернет нулевое значение любого возвращаемого значения, кроме ошибки (в последней позиции).

А как насчет случаев, когда вы хотите вернуть действительное значение вместе с ошибкой?

Затем вы воспользуетесь идиомой if err != nil { .

Во многих случаях вам понадобится более сложная процедура исправления ошибок. Например, после обнаружения ошибки вам может потребоваться откатить что-то или записать что-то в файл журнала. Во всех этих случаях у вас по-прежнему будет обычная идиома if err в вашем наборе инструментов, и вы можете использовать ее для запуска нового блока, где любые операции, связанные с обработкой ошибок, независимо от того, насколько они сформулированы, могут осуществляться.

Какое бы решение мы ни выбрали, оно должно минимально ограничивать универсальность обработки ошибок.

Смотрите мой ответ выше. У вас все еще будет if и все остальное, что дает вам язык.

практически любой современный редактор выделяет зарезервированное слово

Может быть. Но введение непрозрачного синтаксиса, требующего выделения синтаксиса для чтения, не идеально.

эту конкретную ошибку можно исправить, добавив в язык возможность двойного возврата.
в этом случае функция a () возвращает 123:

func a () int {
б ()
возврат 456
}
func b () {
возврат return int (123)
}

Это средство можно использовать для упрощения обработки ошибок следующим образом:

дескриптор func (var * foo, err error) (var * foo, err error) {
if err! = nil {
return return nil, err
}
return var, nil
}

func client_code () (* client_object, error) {
var obj, err = handle (something_that_can_fail ())
// это достигается, только если что-то не вышло из строя
// иначе функция client_code распространит ошибку вверх по стеку
assert (err == ноль)
}

Это позволяет людям писать функции обработчика ошибок, которые могут распространять ошибки вверх по стеку.
такие функции обработки ошибок могут быть отделены от основного кода

Извините, если я ошибся, но я хочу прояснить момент, функция ниже выдаст ошибку, предупреждение vet или она будет принята?

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

@rodcorsi В соответствии с этим предложением ваш пример будет принят без предупреждения ветеринара. Это было бы эквивалентно

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

Как насчет расширения использования контекста для обработки ошибок? Например, учитывая следующее определение:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Теперь о функции, подверженной ошибкам ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
В промежуточной функции ...
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 }
И в функции верхнего уровня
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
Такой подход дает несколько преимуществ. Во-первых, это не отвлекает читателя от основного пути выполнения. Существует минимальное количество операторов «если», указывающих на отклонение от основного пути выполнения.

Во-вторых, он не скрывает ошибки. Из сигнатуры метода ясно, что если он принимает ErrorContext, функция может иметь ошибки. Внутри функции используются обычные операторы ветвления (например, «если»), которые показывают, как обрабатываются ошибки с использованием обычного кода Go.

В-третьих, ошибка автоматически передается заинтересованной стороне, которая в данном случае является владельцем контекста. Если будет дополнительная обработка ошибок, это будет четко показано. Например, давайте внесем некоторые изменения в промежуточную функцию, чтобы обернуть любую существующую ошибку:
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 }
По сути, вы просто пишете код обработки ошибок по мере необходимости. Вам не нужно вручную надувать их.

Наконец, вы, как автор функций, можете сказать, следует ли обрабатывать ошибку. Используя текущий подход Go, это легко сделать ...
`` ''
// учитывая следующее определение
func MyFunction (number int) ошибка

// затем сделаем это
MyFunction (8) // без проверки ошибки
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 () {
возвращение
}
// ...
}
Or make it compulsory with this:
func MyFunction (ctx ErrorContext) {
if ctx.HasError () {// паника, если ctx равен нулю
возвращение
}
// ...
}
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) {
игнорируется: = context.New ()
MyFunction (ignored) // игнорируется

 MyFunction(ctx) //this one is handled

}
`` ''
Такой подход ничего не меняет в существующем языке.

@ALTree Альберто, а как насчет того, чтобы смешать ваш check и то, что предложил @ianlancetaylor ?

так

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
}

становится

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
}

Кроме того, мы можем ограничить check только обработкой типов ошибок, поэтому, если вам нужно несколько возвращаемых значений, их нужно назвать и назначить, чтобы он каким-то образом присваивал «на месте» и вел себя как простой «возврат»

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
}

Если в один прекрасный день return станет приемлемым в выражении, тогда check не понадобится или станет стандартной функцией.

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
}

последнее решение похоже на Perl

Я не могу вспомнить, кто это изначально предложил, но вот еще одна синтаксическая идея (всеми любимый велосипедный навес :-). Я не говорю, что он хороший, но если мы бросаем идеи в горшок ...

x, y := try foo()

будет эквивалентно:

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

и

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

будет эквивалентно:

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

Форма try определенно предлагалась и раньше, несколько раз, по модулю поверхностных различий в синтаксисе. Форма try ... catch предлагается реже, но она явно похожа на конструкцию check A, B @ALTree и последующее предложение

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

У вас может быть несколько попыток / уловов в одном операторе:

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

хотя я не думаю, что мы хотим поощрять это. Здесь также нужно быть осторожным с порядком оценки. Например, проверяется ли h() наличие побочных эффектов, если e() возвращает ненулевую ошибку.

Очевидно, что новые ключевые слова, такие как try и catch , нарушат совместимость с Go 1.x.

Я предлагаю снизить цель этой пропорции. Какую проблему будет исправлять этот пропорсал? Сократить следующие три строчки на две или одну? Это может быть изменение языка возврата / если.

if err != nil {
    return err
}

Или уменьшить количество проверок ошибок? Это может быть попытка решить эту проблему.

Я хотел бы предположить, что любой разумный сокращенный синтаксис для обработки ошибок имеет три свойства:

  1. Он не должен появляться перед кодом, который он проверяет, чтобы путь, не связанный с ошибками, был заметен.
  2. Он не должен вводить неявные переменные в область видимости, чтобы читатели не запутались, когда есть явная переменная с тем же именем.
  3. Это не должно делать одно действие восстановления (например, return err ) проще другого. Иногда может быть предпочтительнее совершенно другое действие (например, вызов t.Fatal ). Мы также не хотим отговаривать людей добавлять дополнительный контекст.

Учитывая эти ограничения, кажется, что один почти минимальный синтаксис будет чем-то вроде

STMT SEPARATOR_TOKEN VAR BLOCK

Например,

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

что эквивалентно

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 (каталог) :: {return @err }
`` ''

Интересно, как это ведет себя при возврате ненулевого значения и ошибки. Например, bufio.Peek может возвращать ненулевое значение и ErrBufferFull одновременно.

@mattn вы все еще можете использовать старый синтаксис.

@nigeltao Да, я понимаю. Я подозреваю, что такое поведение может вызвать ошибку в коде пользователя, поскольку bufio.Peek также возвращает ненулевые и нулевые значения. В коде не должно быть неявных значений и ошибок одновременно. Таким образом, значение и ошибка должны быть возвращены вызывающей стороне (в этом случае).

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

@jba То, что вы описываете, немного похоже на транспонированный оператор композиции функций:

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

Но тот факт, что мы пишем в основном императивный код, требует, чтобы мы не использовали функцию во второй позиции, потому что отчасти суть в том, чтобы иметь возможность вернуться раньше.

Итак, теперь я думаю о трех наблюдениях, которые все как бы связаны:

  1. Обработка ошибок похожа на композицию функций, но способ, которым мы делаем что-то в Go, является своего рода противоположностью монады ошибок Haskell: поскольку мы в основном пишем императивный, а не последовательный код, мы хотим преобразовать ошибку (чтобы добавить контекст), а не значение, не связанное с ошибкой (которое мы бы предпочли просто привязать к переменной).

  2. Функции Go, возвращающие (x, y, error) обычно означают нечто большее, чем объединение (# 19412) (x, y) | error .

  3. В языках, которые распаковывают или объединяют сопоставление с образцом, случаи представляют собой отдельные области действия, и многие проблемы, которые возникают с ошибками в Go, возникают из-за неожиданного затенения повторно объявленных переменных, которое можно улучшить, разделив эти области (# 21114).

Так что, возможно, то, что мы действительно хотим, похоже на оператор =: , но с своего рода условным соответствием объединения:

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

`` идти
n: = io.WriteString (w, s) =? err {return err}

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

За исключением области видимости, это не сильно отличается от простого изменения gofmt чтобы сделать его более доступным для однострочных:

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

`` идти
n, ошибка: = io.WriteString (w, s); if err! = nil {return err}

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

Но обзор - это большое дело! Проблемы с областью видимости заключаются в том, что такого рода код пересекает черту от «несколько уродливых» до «незначительных ошибок».

@ianlancetaylor
Хотя я фанат общей идеи, я не сторонник загадочного синтаксиса, подобного Perl. Возможно, более многословный синтаксис будет менее запутанным, например:

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

syscall.Chdir(dir) or dump

Кроме того, я не понял, появляется ли последний аргумент в случае присваивания, например:

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

Не будем забывать, что ошибки - это значения в go, а не какой-то особый тип.
Мы ничего не можем сделать с другими структурами, что мы не можем сделать с ошибками, и наоборот. Это означает, что если вы понимаете структуры в целом, вы понимаете ошибки и то, как они обрабатываются (даже если вы думаете, что это многословно).

Этот синтаксис потребует от новых и старых разработчиков изучить новый бит информации, прежде чем они смогут начать понимать код, который ее использует.

Уже одно это делает это предложение не стоящим ИМХО.

Лично я предпочитаю этот синтаксис

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

над

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

Это на одну строку больше, но она отделяет предполагаемое действие от обработки ошибок. Эта форма для меня самая читаемая.

@bcmills :

За исключением области видимости, это не сильно отличается от простого изменения gofmt, чтобы он был более податливым для однострочных.

Не только обзор; есть еще и левый край. Думаю, это действительно влияет на читабельность. я думаю

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

намного яснее, чем

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

особенно, когда это происходит на нескольких последовательных строках, потому что ваш глаз может сканировать левый край, чтобы игнорировать обработку ошибок.

Смешивая идею @bcmills, мы можем ввести оператор условной переадресации конвейера.

Функция F2 будет выполнена, если последнее значение не равно

func F1() (foo, bar){}

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

Частный случай переадресации конвейера с оператором возврата

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

Реальный пример, приведенный @urandom в другом выпуске
Для меня гораздо удобнее читать с фокусом в основном потоке

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
}

Я согласен, что обработка ошибок неэргономична. А именно, когда вы читаете код ниже, вы должны озвучивать его как if error not nil then что переводится как if there is an error then .

if err != nil {
    // handle error
}

Я хотел бы иметь возможность выражать приведенный выше код таким образом, который, на мой взгляд, более читабелен.

if err {
    // handle error
}

Просто мое скромное предложение :)

Он действительно похож на perl, в нем даже есть волшебная переменная
Для справки, в perl вы бы сделали

open (FILE, $ file) или die ("невозможно открыть $ file: $!");

ИМХО, это того не стоит, мне нравится в go то, что обработка ошибок
явно и "прямо в лицо"

Если мы будем его придерживаться, я бы не хотел, чтобы магические переменные были, мы должны
возможность назвать переменную ошибки

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

И мы могли бы также использовать символ, отличный от || специально для этой задачи,
Я думаю, что текстовые символы, такие как 'или', невозможны из-за обратного
совместимость

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

1 августа 2017 г., в 14:47, Родриго [email protected] написал:

Смешивая идею @bcmills https://github.com/bcmills, мы можем представить
оператор условного трубного перенаправления.

Функция F2 будет выполнена, если последнее значение не равно

func F1 () (foo, bar) {}
первый: = F1 ()?> последний: F2 (первый, последний)

Частный случай переадресации конвейера с оператором возврата

func Chdir (строка каталога) error {
syscall.Chdir (каталог)?> err: return & PathError {"chdir", dir, err}
вернуть ноль
}

Реальный пример
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
принесено @urandom https://github.com/urandom в другом выпуске
Для меня гораздо удобнее читать с фокусом в основном потоке

func configureCloudinit (icfg * instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
// При начальной загрузке мы хотим только apt-get update / upgrade
// и настраиваем ключи SSH. Остальное оставляем cloudinit / sshinit.
udata: = cloudconfig.NewUserdataConfig (icfg, cloudcfg)?> err: return nil, err
if icfg.Bootstrap! = nil {
udata.ConfigureBasic ()?> ошибка: вернуть ноль, ошибка
вернуть удата, ноль
}
udata.Configure ()?> err: return nil, err
вернуть удата, ноль
}
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)
операционная система: = series.GetOSFromSeries (icfg.Series)?> err: return nil, errors.Trace (err)
udata: = renderer.Render (cloudcfg, operatingSystem)?> err: return nil, errors.Trace (err)
logger.Tracef ("Сгенерированная облачная инициализация: \ n% s", строка (udata))
вернуть удата, ноль
}

-
Вы получаете это, потому что подписаны на эту беседу.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/21161#issuecomment-319359614 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

Я единственный, кто считает, что все предлагаемые изменения будут более сложными, чем текущая форма.

Я считаю, что простота и краткость не равны и не взаимозаменяемы. Да, все эти изменения будут на одну или несколько строк короче, но будут вводить операторы или ключевые слова, которые пользователь языка должен будет выучить.

@rodcorsi Я знаю, что это кажется незначительным, но я думаю, что важно, чтобы вторая часть была блоком : существующие операторы if и for используют блоки, а select и switch оба используют синтаксис, разделенный фигурными скобками, поэтому кажется неудобным опускать фигурные скобки для этой одной конкретной операции потока управления.

Также намного проще обеспечить однозначность дерева синтаксического анализа, если вам не нужно беспокоиться о произвольных выражениях, следующих за новыми символами.

Синтаксис и семантика, которые я имел в виду для своего наброска, следующие:


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

NonZeroGuardStmt выполняет Block если последнее значение Expression не равно нулевому значению его типа. Если присутствует identifier , оно привязано к этому значению в Block . ZeroGuardStmt выполняет Block если последнее значение Expression равно нулевому значению его типа.

Для формы := другие (ведущие) значения Expression привязаны к IdentifierList как в ShortVarDecl . Идентификаторы объявлены в содержащей его области, что означает, что они также видны в Block .

Для формы assign_op каждый левый операнд должен быть адресуемым, выражением индекса карты или (только для назначений = ) пустым идентификатором. Операнды могут быть заключены в скобки. Другие (ведущие) значения правой части Expression оцениваются как Assignment . Присваивание происходит до выполнения Block и независимо от того, выполняется ли затем Block .


Я считаю, что предложенная здесь грамматика совместима с Go 1: ? не является допустимым идентификатором, и нет существующих операторов Go, использующих этот символ, и хотя ! является допустимым оператором, нет существующее производство, в котором за ним может следовать { .

@bcmills LGTM с соответствующими изменениями в gofmt.

Я бы подумал, что вы сделаете =? и =! каждый как отдельный токен, что сделает грамматику тривиально совместимой.

Я бы подумал, вы сделаете =? и =! каждый токен сам по себе, что сделало бы грамматику тривиально совместимой.

Мы можем сделать это в грамматике, но не в лексере: последовательность "=!" может появиться в действительном коде Go 1 (https://play.golang.org/p/pMTtUWgBN9).

Фигурная скобка - это то, что делает синтаксический анализ в моем предложении однозначным: =! настоящее время может появляться только в объявлении или присвоении логической переменной, а объявления и назначения в настоящее время не могут появляться непосредственно перед фигурной скобкой (https : //play.golang.org/p/ncJyg-GMuL), если они не разделены неявной точкой с запятой (https://play.golang.org/p/lhcqBhr7Te).

@romainmenke Нет. Ты не единственный. Я не вижу ценности однострочной обработки ошибок. Вы можете сэкономить одну строчку, но добавить гораздо больше сложности. Проблема в том, что во многих из этих предложений часть обработки ошибок скрыта. Идея состоит не в том, чтобы сделать их менее заметными, поскольку обработка ошибок важна, а в том, чтобы упростить чтение кода. Краткость - это не просто удобочитаемость. Если вам нужно внести изменения в существующую систему обработки ошибок, я считаю, что обычная попытка-поймать-наконец-то гораздо более привлекательна, чем многие идеи, приведенные здесь.

Мне нравится предложение check потому что вы также можете расширить его для обработки

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

Другие предложения не кажутся совместимыми с defer . check также очень удобочитаем и прост для Google, если вы этого не знаете. Я не думаю, что нужно ограничиваться типом error . Все, что является возвращаемым параметром в последней позиции, может его использовать. Итак, итератор может иметь check вместо Next() bool .

Однажды я написал сканер, который выглядит как

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

Вместо этого последний бит может быть check s.read(&rt) .

@carlmjohnson

Другие предложения не кажутся совместимыми с defer .

Если вы предполагаете, что мы расширим defer чтобы разрешить возврат из внешней функции с использованием нового синтаксиса, вы можете одинаково хорошо применить это предположение к другим предложениям.

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

Поскольку предложение @ALTree check вводит отдельный оператор, я не понимаю, как вы могли бы смешать его с defer который делает что-либо, кроме простого возврата ошибки.

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

Контрастность:

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

Обоснование многих из этих предложений - лучшая «эргономика», но я действительно не понимаю, чем какие-либо из них лучше, чем сделать так, чтобы печатать было немного меньше. Как это повысить ремонтопригодность кода? Возможность компоновки? Читаемость? Легкость понимания потока управления?

@jimmyfrasche

Как это повысить ремонтопригодность кода? Возможность компоновки? Читаемость? Легкость понимания потока управления?

Как я отмечал ранее, главное преимущество любого из этих предложений, вероятно, должно исходить от более четкого определения объема назначений и переменных err : см. # 19727, # 20148, # 5634, # 21114 и, возможно, другие способы, которыми люди сталкиваются с проблемами области видимости в связи с обработкой ошибок.

@bcmills благодарит за мотивацию и извините, что пропустил ее в вашем предыдущем посте.

Однако, учитывая эту предпосылку, не лучше ли было бы предоставить более общее средство для «более четкого определения объема назначений», которое могло бы использоваться всеми переменными? Я, конечно же, непреднамеренно скрыл свою долю переменных, не связанных с ошибками.

Я помню, когда было представлено текущее поведение := - большая часть этого треда на ходу † требовала способа явно аннотировать, какие имена следует использовать повторно вместо неявного "повторного использования, только если эта переменная существует в именно в нынешнем масштабе ", в котором, по моему опыту, проявляются все тонкие, трудно заметные проблемы.

† Я не могу найти эту ветку, есть ли у кого-нибудь ссылка?

Есть много вещей, которые, я думаю, можно улучшить в Go, но поведение := всегда казалось мне одной серьезной ошибкой. Может быть, пересмотр поведения := - это способ решить основную проблему или, по крайней мере, уменьшить потребность в других, более серьезных изменениях?

@jimmyfrasche

Однако, учитывая эту предпосылку, не лучше ли было бы предоставить более общее средство для «более четкого определения объема назначений», которое могло бы использоваться всеми переменными?

да. Это одна из вещей, которые мне нравятся в операторе =? или :: который мы с @jba предложили: он также хорошо распространяется на (по общему признанию ограниченное подмножество) не-ошибок.

Лично я подозреваю, что в долгосрочной перспективе я был бы более счастлив с более явной функцией tagged-union / varint / algebraic datatype (см. Также # 19412), но это гораздо большее изменение языка: трудно понять, как мы будем модернизировать это на существующие API в смешанной среде Go 1 / Go 2.

Легкость понимания потока управления?

В предложениях my и @bcmills ваш глаз может сканировать левую часть и легко воспринимать поток, не связанный с контролем ошибок.

@bcmills Я думаю, что я отвечаю как минимум за половину слов в # 19412, поэтому вам не нужно продавать меня по типам сумм;)

Когда дело доходит до возврата материала с ошибкой, бывает четыре случая.

  1. просто ошибка (ничего делать не нужно, просто верните ошибку)
  2. прочее И ошибка (вы бы справились с этим точно так же, как и сейчас)
  3. одна вещь ИЛИ ошибка (вы можете использовать типы сумм!: tada :)
  4. две или более вещи ИЛИ ошибка

Если вы нажмете 4, все станет сложнее. Без введения типов кортежей (немаркированные типы продуктов для использования с помеченными типами продуктов struct) вам пришлось бы уменьшить проблему до случая 3, объединив все в структуру, если вы хотите использовать типы сумм для моделирования «это или ошибка».

Введение типов кортежей вызовет всевозможные проблемы, проблемы совместимости и странные совпадения (является ли func() (int, string, error) неявно определенным кортежем или несколько возвращаемых значений являются отдельной концепцией? Если это неявно определенный кортеж, то означает ли это func() (n int, msg string, err error) - это неявно определенная структура !? Если это структура, как мне получить доступ к полям, если я не в одном пакете!)

Я по-прежнему считаю, что типы сумм дают много преимуществ, но, конечно, они ничего не делают для решения проблем с областью видимости. Во всяком случае, они могли усугубить ситуацию, потому что вы могли затенять всю сумму «результат или ошибку» вместо того, чтобы просто затенять случай ошибки, когда у вас что-то было в случае результата.

@jba Я не понимаю, почему это свойство желательно. Если не считать общей нехватки легкости с концепцией создания, так сказать, двумерного потока управления, я тоже не могу понять, почему это не так. Вы можете мне объяснить преимущества?

Без введения типов кортежей […] вам пришлось бы [связать] все в структуру, если вы хотите использовать типы сумм для моделирования «этого или ошибки».

Я согласен с этим: я думаю, что таким образом у нас было бы гораздо больше читаемых сайтов вызовов (больше никаких случайно переносимых позиционных привязок!), А # 12854 уменьшил бы много накладных расходов, связанных в настоящее время с возвратами структур.

Большой проблемой является миграция: как нам перейти от модели «значений и ошибок» в Go 1 к потенциальной модели «значений или ошибок» в Go 2, особенно с учетом таких API, как io.Writer которые действительно возвращают »значения и ошибка »?

Я по-прежнему считаю, что типы сумм дают много преимуществ, но, конечно, они ничего не делают для решения проблем с областью видимости.

Это зависит от того, как вы их распаковываете, что, я полагаю, возвращает нас туда, где мы находимся сегодня. Если вы предпочитаете союзы, возможно, вы можете представить себе версию =? как API «асимметричного сопоставления с образцом»:

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

Где match будет традиционной операцией сопоставления с образцом в стиле ML, но в случае неисчерпывающего сопоставления будет возвращено значение (как interface{} если объединение имеет более одной несовпадающей альтернативы) вместо того, чтобы паниковать из-за неисчерпывающего провала матча.

Я только что зарегистрировал пакет на https://github.com/mpvl/errd, который программно решает обсуждаемые здесь проблемы (без изменений языка). Наиболее важным аспектом этого пакета является то, что он не только сокращает время обработки ошибок, но и упрощает ее правильное выполнение. В документации я привожу примеры того, как традиционная идиоматическая обработка ошибок сложнее, чем кажется, особенно при взаимодействии с defer.

Однако я считаю этот пакет «горелочным»; цель состоит в том, чтобы получить хороший опыт и понимание того, как лучше всего расширить язык. Он довольно хорошо взаимодействует с дженериками, кстати, если бы это стало чем-то вроде.

Еще работаем над некоторыми примерами, но этот пакет готов к экспериментам.

@bcmills a миллион: +1: для # 12854

Как вы заметили, есть «возврат X и ошибка» и «возврат X или ошибка», поэтому вы не смогли бы обойти это без какого-либо макроса, который переводит старый способ на новый по запросу (и, конечно же, будут ошибки или, по крайней мере, паника во время выполнения, когда она неизбежно использовалась для функции «X и ошибка»).

Мне действительно не нравится идея введения в язык специальных макросов, особенно если они предназначены только для обработки ошибок, что является моей основной проблемой со многими из этих предложений.

Го не особо разбирается в сахаре или магии, и это плюс.

В текущей практике слишком много инерции и слишком мало информации, чтобы справиться с массовым скачком к более функциональной парадигме обработки ошибок.

Если в Go 2 появятся типы сумм - что, честно говоря, шокировало бы меня (в хорошем смысле!) - это, во всяком случае, должен был бы быть очень медленным постепенным процессом, чтобы перейти к «новому стилю», а тем временем был бы даже больше фрагментации и путаницы в том, как обрабатывать ошибки, поэтому я не вижу в этом положительного результата. (Однако я бы сразу начал использовать его для таких вещей, как chan union { Msg1 T; Msg2 S; Err error } вместо трех каналов).

Если бы это было до Go1, и команда Go могла бы сказать: «Мы просто собираемся потратить следующие шесть месяцев, перемещая все, а когда что-то ломается, не отставайте», это было бы одно, но сейчас мы в основном застряли. даже если мы получим типы сумм.

Как вы заметили, есть «вернуть X и ошибку» и «вернуть X или ошибку», поэтому вы не смогли бы обойти это без какого-либо макроса, который переводит старый способ на новый по запросу.

Как я пытался сказать выше, я не думаю, что для нового способа, каким бы он ни был, необходимо включать «возврат X и ошибку». Если подавляющее большинство случаев - это «возврат X или ошибка», а новый способ улучшает только это, то это прекрасно, и вы все равно можете использовать старый, совместимый с Go 1 способ для более редкого «возврата X и ошибки».

@nigeltao Верно, но нам все равно понадобится способ отличить их друг от друга во время перехода, если только вы не предлагаете сохранить всю стандартную библиотеку в существующем стиле.

@jimmyfrasche Я не думаю, что могу придумать аргумент в его пользу. Вы можете посмотреть мой доклад или посмотреть пример в репозитории README . Но если визуальные доказательства вас не убедят, то я ничего не могу сказать.

@jba смотрел выступление и читал README. Теперь я понимаю, откуда вы пришли с этой штукой в ​​скобках / сносках / концевых сносках / боковых заметках (и я поклонник боковых сносок (и скобок)).

Если цель состоит в том, чтобы из-за отсутствия лучшего термина, неудачный путь в сторону, тогда плагин $ EDITOR будет работать без изменения языка, и он будет работать со всем существующим кодом, независимо от предпочтений автора кода.

Смена языка делает синтаксис несколько более компактным. @bcmills упоминает, что это улучшает область видимости, но я действительно не понимаю, как это могло быть, если у него нет других правил масштабирования, чем := но это просто кажется, что это вызовет больше путаницы.

@bcmills Я не понимаю вашего комментария. Очевидно, вы можете отличить их друг от друга. Старый способ выглядит так:

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

Новый способ выглядит как

check foo()

или

foo() || &FooError{err}

или любого другого цвета велосипедного навеса. Я предполагаю, что большая часть стандартной библиотеки может переходить, но не вся она должна.

В дополнение к требованиям @ianlancetaylor : упрощение сообщений об ошибках должно не только делать их короче, но и упростить правильную обработку ошибок. Передавать паники и ошибки нижестоящего уровня в функции отложенного выполнения сложно.

Рассмотрим, например, запись в файл Google Cloud Storage, где мы хотим прервать запись файла при любой ошибке:

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
}

Тонкости этого кода включают:

  • Ошибка из Copy незаметно передается через именованный возвращаемый аргумент в функцию defer.
  • Для полной безопасности мы ловим панику от r и гарантируем, что прервем запись, прежде чем возобновить панику.
  • Игнорирование ошибки первого Close намеренно, но вроде как артефакт ленивого программиста.

При использовании пакета errd этот код выглядит так:

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 - обработчик ошибок. Обработчики ошибок также могут использоваться для переноса, регистрации любых ошибок.

e.Must эквивалентно foo() || wrapError

e.Defer является дополнительным и имеет дело с передачей ошибок в defers.

При использовании дженериков этот фрагмент кода мог бы выглядеть примерно так:

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

Если мы стандартизируем методы, используемые для Defer, это может даже выглядеть так:

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

Где DeferClose выбирает Close или CloseWithError. Не говорить, что это лучше, а просто показывать возможности.

Во всяком случае, на прошлой неделе я выступал на встрече в Амстердаме по этой теме, и мне показалось, что возможность упростить правильную обработку ошибок считается более полезной, чем ее сокращение.

Решение, улучшающее ошибки, должно быть сосредоточено, по крайней мере, в такой же степени на том, чтобы упростить получение правильных решений, чем на сокращении времени.

@ALTree errd выполняет "сложное восстановление после ошибок" из коробки.

@jimmyfrasche : errd делает примерно то же, что и ваш пример игровой площадки, но также вплетает ошибки и панику в задержку.

@jimmyfrasche : Я согласен с тем, что большинство предложений мало что добавляют к тому, что уже может быть достигнуто в коде.

@romainmenke : согласен, слишком много внимания уделяется лаконичности. Чтобы было легче делать вещи правильно, следует уделять больше внимания.

@jba : подход errd позволяет довольно легко сканировать поток ошибок по сравнению с потоком без ошибок, просто глядя на левую сторону (все, что начинается с e., является обработкой ошибок или отложений). Это также позволяет очень легко сканировать, какие из возвращаемых значений обрабатываются для ошибки или задержки, а какие нет.

@bcmills : хотя errd сам по себе не устраняет проблемы с областью видимости, он устраняет необходимость передавать нижестоящие ошибки в ранее объявленные переменные ошибок и все остальное, тем самым значительно уменьшая проблему для обработки ошибок, AFAICT.

errd, похоже, полностью полагается на панику и восстановление. это звучит так, как будто это приводит к значительному снижению производительности. Я не уверен, что это общее решение из-за этого.

@urandom : Под капотом он реализован как более дорогостоящая, но единственная отсрочка.
Если исходный код:

  • не использует defer: штраф за использование errd велик, около 100 нс *.
  • использует идиоматическую задержку: время выполнения или errd того же порядка, хотя и несколько медленнее
  • использует правильную обработку ошибок для defer: время выполнения примерно равно; errd может быть быстрее, если количество задержек> 1

Прочие накладные расходы:

  • Передача замыканий (w.Close) в Defer в настоящее время также добавляет около 25 нс * накладных расходов по сравнению с использованием DeferClose или DeferFunc API (см. Выпуск v0.1.0). После обсуждения с @rsc я удалил его, чтобы упростить API и позаботиться об оптимизации позже.
  • Обертывание строк встроенных ошибок в качестве обработчиков ( e.Must(err, msg("oh noes!") ) стоит около

(*) все числа на моем MacBook Pro 2016 года выпуска.

В общем, стоимость кажется приемлемой, если в исходном коде используется defer. Если нет, Остин работает над тем, чтобы значительно снизить стоимость отсрочки, поэтому со временем стоимость может даже снизиться.

В любом случае, цель этого пакета - получить опыт того, как использование альтернативной обработки ошибок будет ощущаться и быть полезным сейчас, чтобы мы могли создать лучшее языковое дополнение для Go 2. Речь идет о текущем обсуждении, в нем слишком много внимания уделяется сокращению несколько строк для простых случаев, тогда как можно получить гораздо больше и, возможно, другие моменты более важны.

@jimmyfrasche :

тогда плагин $ EDITOR будет работать без изменения языка

Да, именно об этом я и утверждаю в своем выступлении. Здесь я утверждаю, что если мы вносим изменение в язык, это должно соответствовать концепции "второстепенного текста".

@nigeltao

Очевидно, вы можете отличить их друг от друга. Старый способ выглядит так:

Я говорю о смысле объявления, а не о смысле использования.

Некоторые из обсуждаемых здесь предложений не делают различий между этими двумя предложениями на месте звонка, но некоторые делают это. Если мы выберем один из вариантов, который предполагает «значение или ошибку» - например, || , try … catch или match - тогда следует использовать ошибку времени компиляции. этот синтаксис с функцией «значение и ошибка», и разработчик функции должен определить, какой именно.

В момент объявления в настоящее время нет способа различить «значение и ошибка» и «значение или ошибка»:

func Atoi(string) (int, error)

и

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

имеют одинаковые возвращаемые типы, но разную семантику ошибок.

@mpvl Я смотрю документы и src для errd. Думаю, я начинаю понимать, как это работает, но похоже, что у него много API, которые мешают пониманию, которое, кажется, может быть реализовано в отдельном пакете. Я уверен, что все это делает его более полезным на практике, но в качестве иллюстрации добавляет много шума.

Если мы проигнорируем общие помощники, такие как функции верхнего уровня для работы с результатом WithDefault (), и предположим, для простоты, что мы всегда использовали контекст, и проигнорируем любые решения, принимаемые для повышения производительности, абсолютный минимальный barebone API уменьшится до ниже операций?

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)

Глядя на код, я вижу несколько веских причин, по которым он не определен так, как указано выше, но я пытаюсь понять основную семантику, чтобы лучше понять концепцию. Например, я не уверен, находится ли IsSentinel в ядре или нет.

@jimmyfrasche

@bcmills упоминает, что это улучшает область видимости, но я действительно не понимаю, как это могло

Основное улучшение заключается в том, что переменная err попадает в область видимости. Это позволит избежать ошибок, подобных тем, на которые есть ссылки на https://github.com/golang/go/issues/19727. Чтобы проиллюстрировать отрывком из одного из них:

    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
    }

Ошибка возникает в последнем операторе if: ошибка из Decode отбрасывается, но это не очевидно, потому что err из предыдущей проверки все еще находилась в области видимости. Напротив, при использовании оператора :: или =? это будет записано:

    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 }

Здесь помогают два улучшения области видимости:

  1. Первый err (из предыдущего вызова Get ) входит только в область действия блока return , поэтому его нельзя случайно использовать в последующих проверках.
  2. Поскольку err из Decode объявляется в том же операторе, в котором проверяется на нуль, не может быть перекоса между объявлением и проверкой.

Одного (1) было бы достаточно, чтобы выявить ошибку во время компиляции, но (2) позволяет легко избежать при очевидном использовании оператора защиты.

@bcmills спасибо за разъяснения

Итак, в res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } =? макрос

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

Если это верно, это то, что я имел в виду, когда сказал, что у него должна быть другая семантика от := .

Похоже, это вызовет собственные недоразумения, такие как:

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
}

Если только я чего-то не понял.

Ага, это правильное расширение. Вы правы, что он отличается от := , и это сделано намеренно.

Похоже, это вызовет собственное замешательство

Это правда. Для меня не очевидно, будет ли это сбивать с толку на практике. Если это так, мы могли бы предоставить ":" варианты оператора защиты для объявления (и назначить только варианты "=").

(И теперь это наводит меня на мысль, что операторы должны быть написаны ? и ! вместо =? и =! .)

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

но

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

@mpvl Меня больше всего беспокоит errd интерфейс Handler: кажется, он поощряет конвейеры обратного вызова в функциональном стиле, но мой опыт работы с кодом стиля обратного вызова / продолжения (как в императивных языках, таких как Go и C ++, так и в функциональных языков, таких как ML и Haskell) состоит в том, что ему зачастую гораздо труднее следовать, чем эквивалентному последовательному / императивному стилю, который также совпадает с остальными идиомами Go.

Вы представляете себе цепочки в стиле Handler как часть API, или ваш Handler заменяет какой-то другой синтаксис, который вы рассматриваете (например, что-то, работающее на Block с?)

@bcmills Я все еще не использую волшебные функции, которые вводят десятки концепций в язык в одну строку и работают только с одним элементом, но я наконец понимаю, почему они больше, чем просто немного более короткий способ написать x, err := f(); if err != nil { return err } . Спасибо, что помогли мне понять, и извините, что потребовалось так много времени.

@bcmills Я переписал мотивирующий пример @mpvl , в котором есть некоторая грубая обработка ошибок, используя последнее предложение =? которое не всегда объявляет новую переменную err:

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
}

Большая часть обработки ошибок осталась неизменной. Я мог использовать =? в двух местах. Во-первых, это не принесло мне никакой пользы. Во втором он сделал код длиннее и скрыл тот факт, что io.Copy возвращает две вещи, поэтому, вероятно, было бы лучше просто не использовать его там.

@jimmyfrasche Этот код является исключением, а не правилом. Мы не должны разрабатывать функции, чтобы было легче писать.

Кроме того, я сомневаюсь, что recover вообще должно быть там. Если w.Write или r.Read (или io.Copy !) Паникуют, вероятно, лучше прекратить.

Без recover нет реальной необходимости в defer , и нижняя часть функции может стать

_ = 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

Обратите внимание, что моя конкретная формулировка (в https://github.com/golang/go/issues/21161#issuecomment-319434101) касается нулевых значений, а не конкретно ошибок.

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

Это не так, хотя я мог бы сказать об этом более четко.

Мне не особенно нравится использование @mpvl recover в этом примере: оно поощряет использование паники вместо идиоматического потока управления, тогда как я думаю, что во всяком случае мы должны исключить посторонние вызовы recover ( например, в fmt ) из стандартной библиотеки в Go 2.

При таком подходе я бы написал этот код как:

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

С другой стороны, вы правы в том, что при унидиоматическом восстановлении мало возможностей для применения функций, предназначенных для поддержки идиоматической обработки ошибок. Однако отделение восстановления от операции Close действительно приводит к несколько более чистому коду IMO.

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 обработчик отсрочки повторно паникует: он пытается уведомить процесс на другом компьютере, чтобы случайно не зафиксировать плохую транзакцию (при условии, что это все еще возможно в состоянии потенциальной ошибки). Если это не очень распространено, вероятно, так и должно быть. Я согласен с тем, что чтение / запись / копирование не должно вызывать паники, но если бы там был другой код, который мог бы вызвать разумную панику по какой-либо причине, мы бы вернулись к тому, с чего начали.

@bcmills , последняя версия выглядит лучше (даже если вы =? , правда)

@jba :

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

это все еще не покрывает случай паники среди читателя. По общему признанию, это редкий случай, но довольно важный: вызывать здесь Close в случае паники очень плохо.

Этот код является исключением, а не правилом. Мы не должны разрабатывать функции, чтобы было легче писать.

@jba : Я полностью не согласен с этим. Важно правильно обрабатывать ошибки. Упрощение простого случая побудит людей еще меньше думать о правильной обработке ошибок. Я хотел бы увидеть какой-то подход, который, например errd , упрощает консервативную обработку ошибок, требуя при этом некоторых усилий для ослабления правил, а не всего, что хоть немного движется в другом направлении.

@jimmyfrasche : относительно вашего упрощения: вы примерно правы.

  • IsSentinel не обязателен, а просто удобен и распространен. Я уронил его, по крайней мере, на данный момент.
  • Err in State отличается от err, поэтому ваш API отбрасывает это. Однако для понимания это не критично.
  • Обработчики могут быть функциями, но являются интерфейсами в основном по соображениям производительности. Я просто знаю, что многие люди не будут использовать этот пакет, если он не оптимизирован. (см. некоторые из первых комментариев к errd в этом выпуске)
  • Контекст неудачный. AppEngine это нужно, но я не думаю, что больше ничего другого. Я бы не отказался от поддержки, пока люди не откажутся.

@mpvl Я просто пытался сократить это до нескольких вещей, чтобы было легче понять, как это работает, как его использовать, и представить, как это впишется в код, который я написал.

@jimmyfrasche : понял, хотя хорошо, если API не требует этого. :)

@bcmills : Обработчики служат нескольким целям, например, в порядке важности:

  • обернуть ошибку
  • определить, чтобы игнорировать ошибки (чтобы сделать это явным. См. пример)
  • журнал ошибок
  • метрики ошибок

Опять же, в порядке важности, они должны быть оценены по:

  • блокировать
  • линия
  • упаковка

Ошибки по умолчанию нужны только для того, чтобы было легче гарантировать, что ошибка где-то обрабатывается,
но я мог жить только с блочным уровнем. Изначально у меня был API с параметрами вместо обработчиков. Однако это привело к тому, что API стал более крупным и неуклюжим, а также стал медленнее.

Я не вижу здесь такой проблемы с обратным вызовом. Пользователи определяют Runner, передавая ему Handler, который вызывается в случае ошибки. Конкретный бегун явно указывается в блоке, в котором обрабатываются ошибки. Во многих случаях обработчиком будет просто строковый литерал в оболочке, переданный в строку. Я немного поиграюсь, чтобы увидеть, что полезно, а что нет.

Кстати, если мы не должны поощрять логирование ошибок в обработчиках, то поддержка контекста, вероятно, может быть прекращена.

@jba :

Кроме того, я сомневаюсь, что восстановление вообще должно быть там. Если w.Write или r.Read (или io.Copy!) Вызывает панику, вероятно, лучше завершить работу.

writeToGS по-прежнему завершается, если возникает паника, как и должно (!!!), он просто гарантирует, что вызывает CloseWithError с ошибкой, отличной от nil. Если паника не обработана, defer все равно вызывается, но с ошибкой err == nil, что приводит к материализации потенциально поврежденного файла в облачном хранилище. Правильнее всего здесь вызвать CloseWithError с некоторой временной ошибкой, а затем продолжить панику.

Я нашел кучу подобных примеров в коде Go. Работа с io.Pipes также часто приводит к слишком тонкому коду. Обработка ошибок часто не так проста, как кажется, как вы сами видели.

@bcmills

Мне не особенно нравится использование функции восстановления в этом примере в @mpvl : она поощряет использование паники вместо идиоматического потока управления,

Ни в коей мере не пытаясь поощрять панику. Обратите внимание, что паника возникает повторно сразу после CloseWithError и, таким образом, иначе не меняет поток управления. Паника остается паникой.
Однако не использовать восстановление здесь неправильно, поскольку паника вызовет вызов defer с ошибкой nil, сигнализирующей о том, что то, что было написано до сих пор, может быть зафиксировано.

Единственный в некоторой степени веский аргумент не использовать здесь recovery - это то, что очень маловероятно возникновение паники даже для произвольного Reader (Reader неизвестного типа в этом примере по какой-то причине :)).
Однако для производственного кода это неприемлемая позиция. Это обязательно когда-нибудь случится при программировании в достаточно большом масштабе (паника может быть вызвана другими вещами, а не ошибками в коде).

Кстати, обратите внимание, что пакет errd избавляет пользователя от необходимости думать об этом. Тем не менее, любой другой механизм, который сигнализирует об ошибке в случае паники, подойдет. Не вызывать отсрочки из-за паники тоже можно, но тут есть свои проблемы.

В момент объявления в настоящее время нет способа различить «значение и ошибка» и «значение или ошибка»:

@bcmills О, понятно. Чтобы открыть еще одну банку велосипедных навесов, я полагаю, вы могли бы сказать

func Atoi(string) ?int

вместо

func Atoi(string) (int, error)

но WriteString останется без изменений:

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

Мне нравится предложение =? / =! / :=? / :=! от @bcmills / @jba больше, чем аналогичные предложения. У него есть несколько хороших свойств:

  • составной (вы можете использовать =? внутри блока =? )
  • общий (заботится только о нулевом значении, а не о типе ошибки)
  • улучшенная область видимости
  • может работать с задержкой (в одном из вариантов выше)

У него также есть некоторые свойства, которые я не нахожу такими хорошими.

Составные гнезда. При повторном использовании отступы будут увеличиваться все дальше и дальше вправо. Это не обязательно плохо само по себе, но я могу представить, что в ситуациях с очень сложной обработкой ошибок, требующей обработки ошибок, вызывающих ошибки, вызывающие ошибки, код для их устранения быстро станет гораздо менее ясным, чем текущее статус-кво. В такой ситуации можно использовать =? для самой внешней ошибки и if err != nil для внутренних ошибок, но тогда действительно ли это улучшило обработку ошибок в целом или только в общем случае? Возможно, все, что нужно, - это улучшить общий случай, но лично я не считаю это убедительным.

Он привносит в язык фальшь, чтобы добиться его общности. Ложность, определяемая как «является (не) нулевым значением», вполне разумна, но if err != nil { лучше, чем if err { поскольку, на мой взгляд, это явно. Я ожидал увидеть в дикой природе искажения, которые пытаются использовать =? / etc. через более естественный поток управления, чтобы попытаться получить доступ к его лживости. Это, конечно, было бы неидиоматично и осуждалось бы, но это произойдет. Хотя потенциальное злоупотребление функцией само по себе не является аргументом против функции, ее следует учитывать.

Улучшенная область видимости (для вариантов, объявляющих свой параметр) удобна в некоторых случаях, но если область видимости необходимо исправить, исправьте область видимости в целом.

Семантика «единственный правый результат» имеет смысл, но мне кажется немного странной. Это больше чувство, чем аргумент.

Это предложение добавляет краткости формулировке, но не дает дополнительной мощности. Он может быть полностью реализован как препроцессор, выполняющий расширение макроса. Это, конечно, было бы нежелательно: это усложнило бы сборку и разработку фрагментов, и любой такой препроцессор был бы чрезвычайно сложным, поскольку он должен учитывать типы и гигиеничность. Я не пытаюсь сбить с толку, говоря «просто сделайте препроцессор». Я поднимаю это исключительно для того, чтобы указать, что это предложение полностью сахарное. Он не позволяет вам делать то, что вы не могли бы сделать в Go сейчас; это просто позволяет вам писать более компактно. Я не категорически против сахара. В тщательно подобранной лингвистической абстракции заключена сила, но тот факт, что это сахар, означает, что его следует рассматривать, так сказать, пока не будет доказана невиновность.

Левая часть операторов - это оператор, но это очень ограниченное подмножество операторов. Какие элементы включить в это подмножество, довольно очевидно, но, по крайней мере, потребуется рефакторинг грамматики в спецификации языка, чтобы учесть это изменение.

Хотел бы что-нибудь

func F() (S, T, error)

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

позволено, разрешено?

Если

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

допускается, что должно быть (каким-то образом) эквивалентно

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

что кажется глубоко проблематичным и может вызвать очень запутанные ситуации, даже если игнорировать это очень непохожим образом, он скрывает последствия для производительности неявного распределения замыкания. Альтернативой является возврат return из неявного закрытия, а затем сообщение об ошибке, что вы не можете вернуть значение типа error из func() которое является немного тупой.

На самом деле, кроме немного улучшенного исправления области видимости, это не решает ни одной из проблем, с которыми я сталкиваюсь при работе с ошибками в Go. В большинстве случаев ввод if err != nil { return err } вызывает неудобства по модулю небольших проблем с удобочитаемостью, которые я выразил в # 21182. Две самые большие проблемы:

  1. думать о том, как справиться с ошибкой - и язык ничего не может с этим поделать
  2. анализ ошибки, чтобы определить, что делать в некоторых ситуациях - некоторые дополнительные соглашения с поддержкой пакета errors будут иметь большое значение здесь, хотя они не могут решить все проблемы.

Я понимаю, что это не единственные проблемы, и что многие находят другие аспекты более важными, но именно с ними я провожу больше всего времени и считаю их более неприятными и неприятными, чем что-либо еще.

Конечно, всегда будет приветствоваться лучший статический анализ для определения того, когда я что-то напортачил (и в целом, не только в этом сценарии). Изменения языка и соглашения, упрощающие анализ источника, поэтому они более полезны, также будут представлять интерес.

Я просто много писал (МНОГО! Извините!) Об этом, но я не отклоняю это предложение. Я действительно думаю, что у него есть достоинства, но я не уверен, что он убирает планку или поднимает свой вес.

@jimmyfrasche

Я помню, когда было введено текущее поведение: = - большая часть этого потока сходила с ума †, требовала способа явно аннотировать, какие имена следует использовать повторно, вместо неявного «повторного использования, только если эта переменная существует точно в текущей области видимости». "где, по моему опыту, проявляются все тонкие, трудно заметные проблемы.

† Я не могу найти эту ветку, есть ли у кого-нибудь ссылка?

Я думаю, вы, должно быть, помните другую ветку, если только вы не участвовали в Go, когда он был выпущен. В спецификации от 2009/11/9, незадолго до его выпуска, есть:

В отличие от обычных объявлений переменных, краткое объявление переменных может повторно объявлять переменные при условии, что они были изначально объявлены в том же блоке с тем же типом, и по крайней мере одна из непустых переменных является новой.

Я помню, как увидел это при первом чтении спецификации и подумал, что это отличное правило, поскольку раньше я использовал язык с: =, но без этого правила повторного использования, и придумывать новые имена для того же самого было утомительно.

@mpvl
Я думаю, что хитрость вашего исходного примера больше связана с тем, что
API, который вы там используете, чем самой обработки ошибок Go.

Однако это интересный пример, особенно из-за того, что
вы не хотите закрывать файл в обычном режиме, если у вас возникнет паника, поэтому
обычная идиома "defer w.Close ()" не работает.

Если вам не нужно было избегать вызова Close, когда есть
паника, тогда вы могли бы сделать:

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
}

предполагая, что семантика была изменена таким образом, что вызов Close
после вызова CloseWithError не выполняется.

Я не думаю, что это уже так плохо.

Даже с требованием, чтобы файл не записывался без ошибок, когда возникает паника, не должно быть слишком сложно уладить; например, добавив функцию Finalize, которая должна вызываться явно перед Close.

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

Это не может присоединить сообщение об ошибке паники, но приличное ведение журнала могло бы прояснить это.
(метод Close может даже иметь внутри себя вызов восстановления, хотя я не уверен, что это
на самом деле очень плохая идея ...)

Однако я думаю, что аспект панического восстановления в этом примере - отвлекающий маневр в этом контексте, поскольку 99 +% случаев обработки ошибок не приводят к паническому восстановлению.

@rogpeppe :

Это не может присоединить сообщение об ошибке паники, но приличное ведение журнала могло бы прояснить это.

Я не думаю, что это проблема.

Предлагаемое вами изменение API смягчает, но еще не решает проблему полностью. Требуемая семантика требует, чтобы другой код тоже вел себя должным образом. Рассмотрим пример ниже:

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

Сам по себе этот код показывает, что обработка ошибок может быть сложной или, по крайней мере, беспорядочной (и мне было бы любопытно, как это можно улучшить с помощью других предложений): он незаметно передает нижестоящие ошибки через переменную и имеет немного неуклюжий оператор if, чтобы убедиться, что передана правильная ошибка. Оба слишком отвлекают от «бизнес-логики». Обработка ошибок преобладает в коде. И этот пример еще даже не справляется с паникой.

Для полноты, в errd это _would_ правильно обрабатывать панику и будет выглядеть так:

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

Если указанный выше ридер (не использующий errd ) передается в качестве читателя в writeToGS, а io.Reader, возвращаемый паникой newReader, все равно приведет к неправильной семантике с предложенным вами исправлением API (может привести к успешному закрытию файл GS после того, как канал был закрыт в панике с нулевой ошибкой.)

Это тоже подтверждает точку зрения. Рассуждать о правильной обработке ошибок в Go нетривиально. Когда я посмотрел, как будет выглядеть код, если его переписать с помощью errd , я обнаружил кучу ошибочного кода. На самом деле я только узнал, насколько сложно и тонко было написать правильную идиоматическую обработку ошибок Go, однако, при написании модульных тестов для пакета errd . :)

Альтернативой предлагаемому вами изменению API было бы вообще не обрабатывать какие-либо задержки при панике. У этого есть свои проблемы, и это не решит проблему полностью и, вероятно, не исправимо, но будет иметь некоторые приятные качества.

В любом случае, лучше всего было бы какое-то изменение языка, которое смягчает тонкости обработки ошибок, а не то, которое фокусируется на краткости.

@mpvl
С кодом обработки ошибок в Go я часто обнаруживаю, что создание другой функции может исправить ситуацию. Я бы написал ваш код примерно так:

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

Я предполагаю, что r.Close не возвращает полезную ошибку - если вы полностью прочитали ридер и только что встретили только io.EOF, то почти наверняка не имеет значения, возвращает ли он ошибку при закрытии.

Мне не нравится errd API - он слишком чувствителен к запускаемым горутинам. Например: https://play.golang.org/p/iT441gO5us Независимо от того, запускает ли doSomething горутину для запуска
Функция в не должна влиять на корректность программы, но при использовании errd она влияет. Вы ожидаете, что паника безопасно преодолеет границы абстракции, а в Go этого не происходит.

@mpvl

отложить w.CloseWithError (ошибка)

Кстати, эта строка всегда вызывает CloseWithError с нулевым значением ошибки. Я думаю ты хотел
написать:

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

@mpvl

Обратите внимание, что ошибка, возвращаемая методом Close для io.Reader , почти никогда не бывает полезной (см. Список в https://github.com/golang/go/issues/20803#issuecomment-312318808 ).

Это говорит о том, что мы должны написать ваш пример сегодня как:

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

... что мне кажется вполне нормальным, не считая того, что он немного многословен.

Верно, что в случае паники w.CloseWithError он передает ошибку nil, но вся программа в любом случае завершается на этом этапе. Если важно никогда не закрываться с ошибкой nil, это простое переименование плюс одна дополнительная строка:

-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 : действительно, спасибо. :)

Да, я осведомлен о проблеме с горутинами. Это неприятно, но, вероятно, это не так сложно выявить с помощью ветеринарного осмотра. В любом случае, я не рассматриваю errd как окончательное решение, а скорее как способ получить опыт того, как лучше всего решать проблему обработки ошибок. В идеале было бы изменение языка, которое решало бы те же проблемы, но с соответствующими ограничениями.

Вы ожидаете, что паника безопасно преодолеет границы абстракции, а в Go этого не происходит.

Это не то, чего я ожидал. В этом случае я ожидаю, что API не сообщат об успехе, когда его не было. Ваш последний фрагмент кода обрабатывает это правильно, потому что он не использует defer для писателя. Но это очень тонко. Многие пользователи использовали бы в этом случае defer, потому что это считается идиоматическим.

Возможно, набор ветеринарных проверок поможет выявить проблемные случаи использования отсрочки. Тем не менее, как в исходном «идиоматическом» коде, так и в вашем последнем отреставрированном фрагменте есть много попыток обойти тонкости обработки ошибок для чего-то, что в остальном является довольно простым фрагментом кода. Код обходного пути не предназначен для выяснения того, как обрабатывать определенные случаи ошибок, это просто пустая трата мозговых циклов, которую можно использовать для продуктивного использования.

В частности, что я пытаюсь узнать из errd , так это то, упрощает ли он обработку ошибок при прямом использовании. Насколько я могу судить, многие сложности и тонкости исчезают. Было бы хорошо посмотреть, сможем ли мы кодифицировать аспекты его семантики в новых языковых функциях.

@jimmyfrasche

Он привносит в язык фальшь, чтобы добиться его общности.

Это очень хороший момент. Обычные проблемы с ложностью возникают из-за того, что забыли вызвать логическую функцию или забыли разыменовать указатель на nil.

Мы могли бы решить последнее, определив оператор для работы только с пустыми типами (и, вероятно, в результате отбросим =! , поскольку в большинстве случаев это было бы бесполезно).

Мы могли бы обратиться к первому, дополнительно ограничив его, чтобы он не работал с типами функций или работал только с типами указателей или интерфейсов: тогда было бы ясно, что переменная не является логической, и попытки использовать ее для логических сравнений будут больше очевидно неправильно.

Разрешено ли что-то вроде [ MustF ]?

да.

Если [ defer f.Close() :=? err { ] разрешено, это должно быть (каким-то образом) эквивалентно
[ defer func() { … }() ].

Не обязательно, нет. Он может иметь собственную семантику (больше похоже на call/cc чем на анонимную функцию). Я не предлагал изменения спецификации для использования =? в defer (для этого потребуется как минимум изменение грамматики), поэтому я не уверен, насколько сложным будет такое определение. .

Две самые большие проблемы - это […] 2. анализ ошибки, чтобы определить, что делать в некоторых ситуациях.

Я согласен с тем, что на практике это более серьезная проблема, но она кажется более или менее ортогональной по отношению к этой проблеме (которая больше связана с уменьшением шаблонного кода и связанной с ним вероятности ошибок).

( @rogpeppe , @davecheney , @dsnet , @crawshaw , я и еще несколько человек, о которых я наверняка забыл, хорошо обсудили на GopherCon API для проверки ошибок, и я надеюсь, что мы увидим несколько хороших предложений и в этом отношении , но я действительно думаю, что это уже другой вопрос.)

@bcmills : у этого кода две проблемы: 1) то же, что упомянуто в @rogpeppe :

В противном случае я согласен с тем, что ошибку, возвращаемую Close, часто можно игнорировать. Но не всегда (см. Первый пример).

Я нахожу несколько удивительным, что на моих довольно простых примерах было сделано около 4 или 5 ошибочных предложений (включая одно от меня самого), и я все еще чувствую, что должен утверждать, что обработка ошибок в Go нетривиальна. :)

@bcmills :

Верно, что в случае паники он передает ошибку nil в w.CloseWithError, но вся программа в любом случае завершается на этом этапе.

Имеет ли это? Те, кто откладывает эту горутину, все еще вызывают. Насколько я понимаю, они дойдут до доработки. В этом случае Close сигнализирует io.EOF.

См., Например, https://play.golang.org/p/5CFbsAe8zF. После паники горутина она по-прежнему успешно передает "foo" другой горутине, которая затем все еще может записать ее в Stdout.

Точно так же другой код может получить неверный io.EOF от паникующей горутины (например, тот, что в вашем примере), завершить работу и успешно зафиксировать файл в GS до того, как паникующая горутина возобновит панику.

Ваш следующий аргумент может быть таким: хорошо, не пишите код с ошибками, но:

  • затем упростите предотвращение этих ошибок, и
  • паника может быть вызвана внешними факторами, такими как OOM.

Если важно никогда не закрываться с ошибкой nil, это простое переименование плюс одна дополнительная строка:

Он должен по-прежнему закрываться с nil, чтобы сигнализировать io.EOF о завершении, так что это не сработает.

Если важно никогда не закрываться с ошибкой nil, это простое переименование плюс одна дополнительная строка:

Он должен по-прежнему закрываться с nil, чтобы сигнализировать io.EOF о завершении, так что это не сработает.

Почему нет? return err в конце установит rerr в nil .

@bcmills : а, теперь я понимаю, о чем вы. Да, это должно сработать. Впрочем, меня волнует не количество строк, а тонкость кода.

Я считаю, что это относится к той же категории проблем, что и затенение переменных, только с меньшей вероятностью столкнуться с ним (возможно, еще хуже). Большинство ошибок затенения переменных, с которыми вы, возможно, столкнетесь с помощью хороших модульных тестов. Произвольную панику проверить сложнее.

При работе в большом масштабе практически гарантировано, что вы обнаружите подобные ошибки. Я могу быть параноиком, но я видел гораздо менее вероятные сценарии, приводящие к потере и повреждению данных. Обычно это нормально, но не для обработки транзакций (например, для записи файлов gs).

Надеюсь, вы не возражаете, что я украду ваше предложение с помощью альтернативного синтаксиса - как люди относятся к чему-то вроде этого:

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

@SirCmpwn Это похоронит лэдэ. Самым простым для чтения в функции должен быть нормальный поток управления, а не обработка ошибок.

Это справедливо, но ваше предложение также вызывает у меня дискомфорт - оно вводит непрозрачный синтаксис (||), который ведет себя не так, как пользователи привыкли ожидать || вести себя. Не уверен, какое решение будет правильным, подумаю еще немного.

@SirCmpwn Да, как я сказал в исходном посте: «Я пишу это предложение в основном для того, чтобы побудить людей, которые хотят упростить обработку ошибок Go, подумать о том, как упростить перенос контекста вокруг ошибок, а не просто возвращать ошибку без изменений. . " Я написал свое предложение как мог, но не ожидаю, что оно будет принято.

Понял.

Это немного более радикально, но, возможно, подход, основанный на макросах, сработает лучше.

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

try! съест последнее значение в кортеже и вернет его, если не nil, а в противном случае вернет остальную часть кортежа.

Я хотел бы предположить, что наша проблема заключается в следующем:

Обработка ошибок в Go является многословной и повторяющейся. Идиоматический формат обработки ошибок в Go затрудняет просмотр потока управления, не связанного с ошибками, а многословие непривлекательно, особенно для новичков. На сегодняшний день решения, предлагаемые для этой проблемы, обычно требуют кустарных одноразовых функций обработки ошибок, уменьшают локальность обработки ошибок и увеличивают сложность. Поскольку одна из целей Go - заставить автора задуматься об обработке ошибок и восстановлении, любое улучшение обработки ошибок также должно основываться на этой цели.

Чтобы решить эту проблему, я предлагаю следующие цели улучшения обработки ошибок в Go 2.x:

  1. Уменьшает количество повторяющихся шаблонов обработки ошибок и максимально концентрирует внимание на основной цели пути кода.
  2. Поощряет правильную обработку ошибок, включая перенос ошибок при их распространении.
  3. Придерживается принципов ясности и простоты дизайна Go.
  4. Применимо в самом широком диапазоне ситуаций обработки ошибок.

Оценивая это предложение:

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

в соответствии с этими целями, я бы пришел к выводу, что он хорошо преуспел в голе №1. Я не уверен, как это помогает с №2, но это также не снижает вероятность добавления контекста (мое собственное предложение разделяет эту слабость по №2). Хотя на # 3 и # 4 он не очень успешен:
1) Как говорили другие, проверка и присвоение значения ошибки непрозрачны и необычны; и
2) Синтаксис =? также необычен. Это особенно сбивает с толку в сочетании с похожим, но другим синтаксисом =! . Людям потребуется время, чтобы привыкнуть к их значениям; и
3) Возврат действительного значения вместе с ошибкой является достаточно распространенным явлением, поэтому любое новое решение также должно обрабатывать этот случай.

Хотя обработка ошибок в блоке может быть хорошей идеей, если, как предлагали другие, она сочетается с изменениями в gofmt . По сравнению с моим предложением, это улучшает общность, которая должна помочь с целью № 4, и знакомство, которое помогает цели № 3 ценой жертвы в краткости для общего случая простого возврата ошибки с добавленным контекстом.

Если бы вы спросили меня в аннотации, я мог бы согласиться с тем, что более общее решение будет предпочтительнее, чем конкретное решение для обработки ошибок, если оно соответствует целям улучшения обработки ошибок, указанным выше. Однако теперь, прочитав это обсуждение и подумав над ним, я склонен полагать, что конкретное решение для обработки ошибок приведет к большей ясности и простоте. Хотя ошибки в Go - это просто значения, обработка ошибок составляет такую ​​значительную часть любого программирования, что наличие определенного синтаксиса для того, чтобы сделать код обработки ошибок ясным и кратким, кажется целесообразным. Боюсь, что мы сделаем и без того сложную проблему (создание чистого решения для обработки ошибок) еще более сложной и сложной, если объединим ее с другими целями, такими как определение объема и возможность компоновки.

Тем не менее, как отмечает @rsc в своей статье « На пути к Go 2» , ни формулировка проблемы, ни цели, ни какое-либо предложение по синтаксису вряд ли будут продвигаться без отчетов об опыте, которые демонстрируют, что проблема значительна. Может, вместо того, чтобы обсуждать различные предложения по синтаксису, нам стоит начать копаться в поисках подтверждающих данных?

Тем не менее, как отмечает @rsc в своей статье «На пути к Go 2», ни формулировка проблемы, ни цели, ни какое-либо предложение по синтаксису вряд ли будут продвигаться без отчетов об опыте, которые демонстрируют, что проблема значительна. Может, вместо того, чтобы обсуждать различные предложения по синтаксису, нам стоит начать копаться в поисках подтверждающих данных?

Я думаю, что это очевидно, если предположить, что эргономика важна. Откройте любую кодовую базу Go и поищите места, где есть возможности ОСУШИТЬ вещи и / или улучшить эргономику, на которые язык может обратить внимание - прямо сейчас обработка ошибок является явным исключением. Я думаю, что подход Toward Go 2 может ошибочно отстаивать игнорирование проблем, у которых есть обходные пути - в этом случае люди просто улыбаются и терпят это.

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

Когда шаблонов больше, чем кода, проблема очевидна, imho.

@billyh

Мне кажется, что формат: f.Close() =? err { return fmt.Errorf(…, err) } слишком многословен и сбивает с толку. Я лично не считаю, что часть ошибки должна быть в блоке. Неизбежно это приведет к тому, что он будет разбит на 3 строки вместо 1. Кроме того, при изменении выключения, которое вам нужно сделать больше, чем просто изменить ошибку перед ее возвратом, можно просто использовать текущий if err != nil { ... } синтаксис.

Оператор =? также немного сбивает с толку. Не сразу понятно, что там происходит.

Примерно так:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
или сокращение:
file := os.Open("/some/file") or raise
и отложенные:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
является немного более многословным, и выбор слова может уменьшить начальную путаницу (то есть люди могут сразу связать raise с аналогичным ключевым словом из других языков, таких как python, или просто вывести, что повышение вызывает ошибку / last-non- значение по умолчанию вверх по стеку для вызывающей стороны).

Это также хорошее императивное решение, которое не пытается решить все возможные скрытые ошибки обработки под солнцем. Безусловно, самая большая часть обработки ошибок в дикой природе имеет вышеупомянутую природу. В последнем случае также может помочь текущий синтаксис.

Редактировать:
Если мы хотим немного уменьшить «магию», предыдущие примеры также могут выглядеть так:
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)
Я лично считаю, что предыдущие примеры лучше, поскольку они перемещают полную обработку ошибок вправо, а не разделяют ее, как здесь. Хотя это могло бы быть яснее.

Я хотел бы предположить, что наша постановка проблемы ...

Я не согласен с постановкой проблемы. Хочу предложить альтернативу:


С языковой точки зрения обработки ошибок не существует. Единственное, что предоставляет Go, - это заранее объявленный тип ошибки, и даже это просто для удобства, потому что он не включает ничего действительно нового. Ошибки - это просто ценности . Обработка ошибок - это просто обычный пользовательский код. В этом нет ничего особенного с точки зрения языка и не должно быть ничего особенного. Единственная проблема с обработкой ошибок состоит в том, что некоторые люди считают, что эту ценную и красивую простоту необходимо устранить любой ценой.

В соответствии с тем, что говорит Чник, было бы неплохо иметь решение, которое было бы полезно не только для обработки ошибок.

Один из способов сделать обработку ошибок более общей - рассматривать ее в терминах типов-объединений / типов-сумм и разворачивания. У Swift и Rust есть решения? ! синтаксис, хотя я думаю, что Rust был немного нестабильным.

Если мы не хотим превращать типы сумм в концепцию высокого уровня, мы могли бы сделать их просто частью множественного возврата, так как кортежи на самом деле не являются частью Go, но вы все равно можете делать множественный возврат.

Укол синтаксиса, вдохновленный Swift:

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

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

Вы можете использовать это и для других целей, например:

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

Решение =? предложенное @bcmills и @jba , не только для ошибки, концепция предназначена для ненулевого значения. этот пример будет работать нормально.

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

Основная идея этого предложения - примечания, которые отделяют основную цель кода и отбрасывают второстепенные случаи, чтобы облегчить чтение.
Для меня чтение кода Go в некоторых случаях не является непрерывным, много раз вы останавливаете идею с помощью if err!= nil {return err} , поэтому идея боковых примечаний кажется мне интересной, как в книге, которую мы читаем основную идею постоянно, а затем читайте примечания. ( @jba talk )
В очень редких случаях ошибка является основной целью функции, возможно, при восстановлении. Обычно, когда у нас есть ошибка, мы добавляем некоторый контекст, журнал и возвращаем, в этих случаях боковые примечания могут сделать ваш код более читабельным.
Я не знаю, лучший ли это синтаксис, особенно мне не нравится блок во второй части, примечание должно быть маленьким, строки должно хватить

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

@billyh

  1. Как говорили другие, проверка и присвоение значения ошибки непрозрачны и необычны; и

Пожалуйста, будьте более конкретны: «непрозрачное и необычное» ужасно субъективно. Можете ли вы привести несколько примеров кода, где, по вашему мнению, предложение может сбивать с толку?

  1. Знак =? синтаксис тоже необычен. […]

ИМО, это особенность. Если кто-то видит необычного оператора, я подозреваю, что он более склонен искать, что он делает, вместо того, чтобы просто предполагать что-то, что может быть, а может и нет.

  1. Возврат действительного значения вместе с ошибкой является достаточно распространенным явлением, поэтому любое новое решение также должно обрабатывать этот случай.

Оно делает?

Внимательно прочтите предложение: =? выполняет назначения перед оценкой Block , поэтому его можно использовать и в этом случае:

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

И, как заметил @nigeltao , вы всегда можете использовать существующий шаблон 'n, err: = r.Read (buf) `. Добавление функции, помогающей с определением области видимости и шаблоном для общего случая, не означает, что мы должны использовать ее и для необычных случаев.

Может, вместо того, чтобы обсуждать различные предложения по синтаксису, нам стоит начать копаться в поисках подтверждающих данных?

См. Многочисленные проблемы (и их примеры), на которые Иэн указал в исходном сообщении.
См. Также https://github.com/golang/go/wiki/ExperienceReports#error -handling.

Если вы получили конкретную информацию из этих отчетов, поделитесь ею.

@urandom

Я лично не считаю, что часть ошибки должна быть в блоке. Это неизбежно приведет к тому, что он будет разбит на 3 строки вместо 1.

Блок имеет двоякое назначение:

  1. чтобы обеспечить четкий визуальный и грамматический разрыв между выражением, вызывающим ошибку, и его обработчиком, и
  2. чтобы обеспечить более широкий диапазон обработки ошибок (согласно заявленной цели @ianlancetaylor в исходном сообщении).

3 строки против 1 - это даже не изменение языка: если количество строк является вашей самой большой проблемой, мы могли бы решить эту проблему, просто изменив gofmt .

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

У нас уже есть return и panic ; добавление raise поверх них выглядит так, как будто добавляет слишком много способов выхода из функции при слишком небольшом выигрыше.

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

errors.ReplaceIfNil(err, err2) потребует очень необычной семантики передачи по ссылке.
Вместо этого вы могли бы передать err по указателю, я полагаю:

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

но мне это все еще кажется очень странным. Конструирует ли токен or выражение, оператор или что-то еще? (Более конкретное предложение могло бы помочь.)

@carlmjohnson

Каким будет конкретный синтаксис и семантика вашего оператора guard … else ? Мне это кажется очень похожим на =? или :: с обменом токенов и переменных позиций. (Опять же, поможет более конкретное предложение: какой фактический синтаксис и семантика вы имеете в виду?)

@bcmills
Гипотетический ReplaceIfNil будет простым:

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

Ничего необычного в этом нет. Может имя ...

or будет бинарным оператором, где левый операнд будет либо IdentifierList, либо PrimaryExpr. В первом случае он сокращается до крайнего правого идентификатора. Затем он позволяет выполнить правый операнд, если левый не является значением по умолчанию.

Вот почему мне впоследствии понадобился еще один токен, чтобы совершить волшебство по возврату значений по умолчанию для всех, кроме последнего параметра в функции Result, который впоследствии примет значение выражения.
IIRC, не так давно было другое предложение, в котором язык должен был бы добавлять '...' или что-то в этом роде, которое заменило бы утомительную инициализацию значения по умолчанию. По этой причине все может выглядеть так:

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

Что касается блока, я понимаю, что он допускает более широкое обращение. Я лично не уверен, следует ли в рамках этого предложения попытаться удовлетворить все возможные сценарии, а не охватывать гипотетические 80%. И я лично считаю, что имеет значение, сколько строк займет результат (хотя я никогда не говорил, что меня больше всего беспокоит читаемость или ее отсутствие при использовании непонятных токенов, таких как =?). Если это новое предложение в общем случае охватывает несколько строк, я лично не вижу его преимуществ перед чем-то вроде:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • если бы указанные выше переменные были доступны вне области if .
    И это по-прежнему затрудняет чтение функции с парой таких операторов из-за визуального шума этих блоков обработки ошибок. И это одна из жалоб, которые люди испытывают при обсуждении обработки ошибок на ходу.

@urandom

or будет бинарным оператором, где левый операнд будет либо IdentifierList, либо PrimaryExpr. […] Затем он позволяет выполнить правый операнд, если левый не является значением по умолчанию.

Бинарные операторы Go являются выражениями, а не операторами, поэтому создание бинарного оператора or вызовет много вопросов. (Какова семантика or как части более крупного выражения и как это согласуется с примерами, которые вы опубликовали с помощью := ?)

Если предположить, что это на самом деле оператор, какой правый операнд? Если это выражение, каков его тип и можно ли использовать raise в качестве выражения в других контекстах? Если это оператор, какова его семантика, кроме raise ? Или вы предлагаете, чтобы or raise было по существу одним оператором (например, or raise в качестве синтаксической альтернативы :: или =? )?

Могу я написать

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

?

Могу я написать

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

?

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

Нет, это будет недействительно из-за второго raise . Если этого не было, тогда должна пройти вся цепочка преобразования, а конечный результат должен быть возвращен вызывающей стороне. Хотя такая семантика в целом, вероятно, не нужна, так как вы можете просто написать:

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


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

Если мы предположим, что мой исходный комментарий - где или взял бы последнее значение в левой части, так что, если бы это было значение по умолчанию, окончательное выражение оценивало бы остальную часть списка результатов; тогда да, это должно быть верно. В этом случае, если r.Read возвращает ошибку, эта ошибка возвращается вызывающей стороне. В противном случае n будет передано f

Редактировать:

Если меня не смущают термины, я думаю о or как о бинарном операторе, операнды которого должны быть одного типа (но немного волшебным, если левый операнд представляет собой список вещей, и в этом случае он принимает последний элемент указанного списка вещей). raise будет унарным оператором, который принимает свой операнд и возвращается из функции, используя значение этого операнда в качестве значения последнего возвращаемого аргумента, причем предыдущие имеют значения по умолчанию. Затем вы можете технически использовать raise в отдельном операторе для возврата из функции, также известной как return ..., err

Это будет идеальный случай, но меня также устраивает, что or raise является синтаксической альтернативой =? , если он также принимает простой оператор вместо блока, чтобы охватить большинство вариантов использования менее подробным образом. Или мы можем пойти с грамматикой типа defer, где она принимает выражение. Это охватывало бы большинство таких случаев, как:

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

и сложные случаи:

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

Поразмыслив немного над своим предложением, я опускаю немного о типах объединений / сумм. Предлагаемый мной синтаксис:

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

В случае выражения, выражение оценивается, и если результат не равен true для логических выражений или пустому значению для других выражений, выполняется BLOCK. В присвоении последнее присвоенное значение оценивается для != true / != nil . Следуя оператору защиты, любые сделанные назначения будут в области видимости (она не создает новую область видимости блока [кроме, может быть, последней переменной?]).

В Swift БЛОК для операторов guard должен содержать одно из следующих значений: return , break , continue или throw . Я не решил, нравится мне это или нет. Кажется, что это добавляет некоторой ценности, потому что читатель знает по слову guard что будет дальше.

Кто-нибудь достаточно хорошо следит за Swift, чтобы сказать, пользуется ли guard уважением в этом сообществе?

Примеры:

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

Имхо тут сразу все обсуждают слишком широкий круг проблем, но на самом деле наиболее распространенным паттерном является «вернуть ошибку как есть». Так почему бы не подойти к решению большинства проблем, например:

code, err ?= fn()

что означает, что функция должна возвращаться при ошибке err! = nil?

для оператора: = мы можем ввести?: =

code, err ?:= fn()

ситуация с?: = кажется хуже из-за затенения, так как компилятор должен будет передать переменную "err" в одноименное возвращаемое значение err.

На самом деле я очень рад, что некоторые люди сосредотачиваются на том, чтобы упростить написание правильного кода, а не просто сократить неправильный код.

Некоторые примечания:

Интересный «отчет об опыте» от одного из разработчиков Midori в Microsoft о моделях ошибок.

Я думаю, что некоторые идеи из этого документа и Swift можно прекрасно применить к Go2.

При вводе нового ключевого слова throws функции могут быть определены следующим образом:

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

  return []byte{...}
}

Попытка вызвать эту функцию из другой, не вызывающей функции, приведет к ошибке компиляции из-за необработанной генерируемой ошибки.
Вместо этого мы должны иметь возможность распространять ошибку, которая, по общему мнению, является частым случаем, или обрабатывать ее.

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

  // ...
}

Для случаев, когда мы знаем, что метод не откажет, или в тестах, мы можем ввести try! аналогичный swift.

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

  // ...
}

Не уверен в этом (похоже на swift):

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. несколько возвращаемых значений func ReadRune() (ch Rune, size int) throws { ... }
ps2. мы можем вернуться с return try Get() или return try! Get()
ps3. теперь мы можем цеплять вызовы типа buffer.NewBuffer(try Get()) или buffer.NewBuffer(try! Get())
ps4. Не уверен в аннотациях (простой способ написать errors.Wrap(err, "context") )
ps5. это на самом деле исключения
ps6. самый большой выигрыш - ошибки времени компиляции для игнорируемых исключений

Предложения, которые вы пишете, точно описаны в ссылке Midori со всеми плохими
стороны этого ... И одним очевидным следствием "бросков" будет "люди
ненавижу это ». Зачем писать« бросает »каждый раз для большей части
функции?

Кстати, ваше намерение заставить ошибки проверяться и не игнорироваться может быть
применяется и к типам, не связанным с ошибками, и, по-моему, лучше иметь более
обобщенная форма (например, gcc __attribute __ ((warn_unused_result))).

Что касается формы оператора, я бы предложил либо краткую форму, либо
форма ключевого слова, подобная этой:

? = fn () ИЛИ проверить fn () - передать ошибку вызывающему
! = fn () ИЛИ nofail fn () - паника при ошибке

В субботу, 26 августа 2017 г., в 12:15, nvartolomei [email protected]
написал:

Некоторые примечания:

Отчет об интересном опыте
http://joeduffyblog.com/2016/02/07/the-error-model/ из одного из
дизайнеры Midori в Microsoft о моделях ошибок.

Я думаю, что некоторые идеи из этого документа и Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
можно красиво применить к Go2.

Введя новое ключевое слово Reseved throws, функции могут быть определены следующим образом:

func Get () [] выбрасывает байт {
если (...) {
вызывать ошибки.Новое ("упс")
}

return [] байт {...}
}

Попытка вызвать эту функцию из другой, не вызывающей функции, приведет к
приведет к ошибке компиляции из-за необработанной сбрасываемой ошибки.
Вместо этого мы должны иметь возможность распространять ошибку, что, по общему мнению, является
общий случай, или справиться с этим.

func ScrapeDate () time.Time выбрасывает {
body: = Get () // ошибка компиляции, необработанный выброс
body: = try Get () // мы четко заявили о потенциальном бросаемом

// ...
}

В случаях, когда мы знаем, что метод не сработает, или в тестах, мы можем
представить попробуйте! похож на swift.

func GetWillNotFail () time.Time {
body: = Get () // ошибка компиляции, throwable не обрабатывается
body: = try Get () // ошибка компиляции, throwable не может быть передан, потому что GetWillNotFail не аннотируется throws
body: = попробуйте! Get () // работает, но будет паника при бросках! = Nil

// ...
}

Не уверен в этом (похоже на swift):

func main () {
// 1:
делать {
fmt.Printf ("% v", попробуйте ScrapeDate ())
} catch err {// err содержит пойманный бросаемый
// ...
}

// 2:
делать {
fmt.Printf ("% v", попробуйте ScrapeDate ())
} catch err. (type) {// аналогично оператору switch
ошибка случая:
// ...
case io.EOF
// ...
}
}

ps1. несколько возвращаемых значений func ReadRune () (ch Rune, size int) throws {
...}
ps2. мы можем вернуться с помощью return try Get () или return try! Получить()
ps3. теперь мы можем объединять вызовы, такие как buffer.NewBuffer (попробуйте Get ()) или buffer.NewBuffer (попробуйте!
Получить())
ps4. Не уверен в аннотациях

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/21161#issuecomment-325106225 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

Я думаю, что оператор, предложенный @jba и @bcmills, является очень хорошей идеей, хотя он лучше читается как "??" вместо "=?" ИМО.

Глядя на этот пример:

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
}

Я думаю, что doStuff2 намного проще и быстрее читать, потому что он:

  1. тратит меньше вертикального пространства
  2. легко и быстро прочитать счастливый путь слева
  3. легко и быстро прочитать условия ошибки с правой стороны
  4. не имеет переменной err, загрязняющей локальное пространство имен функции

На мой взгляд, это предложение выглядит неполным и содержит слишком много волшебства. Как будет определен оператор ?? ? «Захватывает последнее возвращаемое значение, если не ноль»? «Захватывает последнее значение ошибки, если соответствует типу метода?»

Добавление новых операторов для обработки возвращаемых значений в зависимости от их положения и типа выглядит как уловка.

29 августа 2017 года, 13:03 +0300, Микаэль Густавссон [email protected] написал:

Я думаю, что оператор, предложенный @jba и @bcmills, является очень хорошей идеей, хотя он лучше читается как "??" вместо "=?" ИМО.
Глядя на этот пример:
func doStuff () (int, error) {
x, ошибка: = f ()
if err! = nil {
return 0, wrapError ("f не удалось", err)
}

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

   return y, nil

}

func doStuff2 () (int, error) {
x: = f () ?? (ошибка ошибки) {return 0, wrapError ("f failed", err)}
y: = g (x) ?? (ошибка ошибки) {return 0, wrapError ("g failed", err)}
вернуть y, ноль
}
Я думаю, что doStuff2 намного проще и быстрее читать, потому что он:

  1. тратит меньше вертикального пространства
  2. легко и быстро прочитать счастливый путь слева
  3. легко и быстро прочитать условия ошибки с правой стороны
  4. не имеет переменной err, загрязняющей локальное пространство имен функции

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub или отключите обсуждение.

@nvartolomei

Как будет определен оператор ?? ?

См. Https://github.com/golang/go/issues/21161#issuecomment -319434101 и https://github.com/golang/go/issues/21161#issuecomment -320758279.

Поскольку @bcmills рекомендовал пример @slvmnd , переделайте его с модификаторами оператора:

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
}

Не так кратко, как утверждение и проверка ошибок в одной строке, но читается достаточно хорошо. (Я бы посоветовал запретить форму присваивания: = в выражении if, иначе проблемы с областью видимости могут запутать людей, даже если они ясны в грамматике). синтаксический сахар, но он удобен для чтения и заслуживает внимания.

Однако я бы не рекомендовал использовать здесь Perl. (Basic Plus 2 подойдет). Здесь находятся модификаторы операторов цикла, которые, хотя иногда и полезны, порождают еще один набор довольно сложных проблем.

более короткая версия:
вернуть, если ошибка! = ноль
тогда тоже следует поддержать.

с таким синтаксисом возникает вопрос - должны ли быть также
поддерживаются такими операторами if, например:
func (args) если условие

может, вместо того, чтобы придумывать пост-действие - стоит ли вводить сингл
линия if's?

если ошибка! = ноль возврат
если err! = nil вернуть 0, wrapError ("не удалось", err)
если ошибка! = ноль do_smth ()

это кажется гораздо более естественным, чем особые формы синтаксиса, не так ли? Хотя я думаю
это вносит много боли в парсинг: /

Но ... это всего лишь небольшие настройки, а не специальная языковая поддержка ошибок.
обработка / распространение.

В понедельник, 18 сентября 2017 г., в 16:14 dsugalski [email protected] написал:

Поскольку @bcmills https://github.com/bcmills рекомендовал воскресить
quiescent thread, если мы собираемся рассмотреть возможность списывания с других языков,
похоже, что модификаторы операторов предложат разумное решение для всех
это. Чтобы взять пример @slvmnd https://github.com/slvmnd , переделайте с помощью
модификаторы оператора:

func doStuff () (int, err) {
x, ошибка: = f ()
return 0, wrapError ("f failed", err), если err! = nil

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

    return y, nil

}

Не так кратко, как наличие отчета и проверки ошибок в одном
строка, но она читается достаточно хорошо. (Я бы посоветовал запретить форму: =
присваивание в выражении if, иначе проблемы с областью видимости, скорее всего,
сбивать с толку людей, даже если они ясны в грамматике)
инвертированная версия "if" - это немного синтаксического сахара, но она прекрасно работает, чтобы
прочтите, и стоит подумать.

Однако я бы не рекомендовал использовать здесь Perl. (Basic Plus 2 - это
хорошо) Здесь лежат модификаторы операторов цикла, которые, хотя иногда
полезно, внесите еще один набор довольно сложных вопросов.

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/21161#issuecomment-330215402 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

Если подумать еще о предложении запрошенного @jba и другими, а именно того, что код, не связанный с ошибкой, явно отличается от кода ошибки. Все еще может быть интересной идеей, если она имеет значительные преимущества и для путей без ошибок кода, но чем больше я думаю об этом, тем менее привлекательной она кажется по сравнению с предлагаемыми альтернативами.

Я не уверен, сколько визуальных различий можно ожидать от чистого текста. В какой-то момент кажется более подходящим перенести это на IDE или слой раскраски кода вашего текстового редактора.

Но для видимого различия на основе текста стандарт форматирования, который у нас был, когда я впервые начал использовать его очень давно, заключался в том, что модификаторы операторов IF / UNLESS должны быть выровнены по правому краю, что делало их достаточно хорошо выделяющимися. (Хотя предоставлен стандарт, который было легче применять и, возможно, более четко различимый на терминале VT-220, чем в редакторах с более гибкими размерами окон)

По крайней мере, для меня я считаю, что случай модификатора оператора легко различить и читается лучше, чем текущая схема if-block. Конечно, это может быть не так для других людей - я читаю исходный код так же, как читаю английский текст, поэтому он соответствует существующему удобному шаблону, и не все это делают.

return 0, wrapError("f failed", err) if err != nil можно записать как if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) можно записать так же.

Может быть, все, что здесь нужно, это чтобы gofmt оставил if , написанные в одной строке на одной строке, вместо того, чтобы расширять их до трех строк?

Есть еще одна возможность, которая меня поражает. Многие трудности, с которыми я сталкиваюсь при попытке быстро написать одноразовый код Go, связаны с тем, что мне приходится проверять ошибки при каждом вызове, поэтому я не могу аккуратно вкладывать вызовы.

Например, я не могу вызвать http.Client.Do для нового объекта запроса без предварительного присвоения результата http.NewRequest временной переменной, а затем вызова Do для этого.

Интересно, могли бы мы разрешить:

f(y())

работать, даже если y возвращает кортеж (T, error) . Когда y возвращает ошибку, компилятор может прервать вычисление выражения и вызвать возвращение этой ошибки из f. Если f не возвращает ошибку, ей может быть выдана ошибка.

Тогда я мог:

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

и результат ошибки будет отличным от нуля, если NewRequest или Do завершились неудачно.

Однако у этого есть одна существенная проблема - приведенное выше выражение уже действительно, если f принимает два аргумента или аргументы с переменным числом аргументов. Кроме того, точные правила для этого, вероятно, будут весьма сложными.

В общем, я не думаю, что мне это нравится (я тоже не заинтересован в других предложениях в этой ветке), но подумал, что все равно вынесу эту идею на рассмотрение.

@rogpeppe или просто используйте json.NewEncoder

@gbbr Ха, да, плохой пример.

Лучшим примером может быть http.Request. Я изменил комментарий, чтобы использовать это.

Ух ты. Многие идеи еще больше ухудшают читаемость кода.
Я в порядке с подходом

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

Единственное, что действительно раздражает, - это определение объема возвращаемых переменных.
В этом случае вы должны использовать val но это входит в объем if .
Итак, вы должны использовать else но линтер будет против (и я тоже), и единственный способ -

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

Было бы неплохо иметь доступ к переменным из блока if :

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

@dmbreaker По сути, это то, для чего предназначен пункт о Мой предыдущий комментарий .

Я полностью за то, чтобы упростить обработку ошибок в Go (хотя лично я не против этого), но я думаю, что это добавляет немного волшебства к простому и чрезвычайно легкому для чтения языку.

@gbbr
Что за «это» вы здесь имеете в виду? Есть довольно много разных предложений о том, как действовать.

Возможно решение из двух частей?

Определите try как «оторвать крайнее правое значение в возвращаемом кортеже; если это не нулевое значение для его типа, вернуть его как крайнее правое значение этой функции с остальными, установленными в ноль». Это делает общий случай

 a := try ErrorableFunction(b)

и позволяет объединять

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

(Необязательно, сделайте его ненулевым, а не нулевым для эффективности.) Если ошибочные функции возвращают ненулевое / ненулевое значение, функция «прерывается со значением». Крайнее правое значение функции try 'ed должно быть присвоено крайнему правому значению вызывающей функции, в противном случае это ошибка проверки типа во время компиляции. (Таким образом, это не жестко запрограммировано для обработки только error , хотя, возможно, сообществу следует отговорить его использование для любого другого «умного» кода.)

Затем разрешите перехват попыток возврата с помощью ключевого слова, похожего на defer, либо:

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

или, возможно, более подробно, но в большей степени в соответствии с тем, как Go уже работает:

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

В случае catch параметр функции должен точно соответствовать возвращаемому значению. Если предоставлено несколько функций, значение будет проходить через все из них в обратном порядке. Конечно, вы можете поместить значение в функцию правильного типа. В случае примера на основе defer , если одна defer func вызывает set_catch следующая функция defer получит это как свое значение catch() . (Если вы достаточно глупы, чтобы установить его обратно в nil в процессе, вы получите сбивающее с толку возвращаемое значение. Не делайте этого.) Значение, переданное в set_catch, должно быть присвоено возвращаемому типу. В обоих случаях я ожидаю, что это будет работать как defer в том смысле, что это оператор, а не объявление, и будет применяться к коду только после выполнения оператора.

Я предпочитаю решение на основе отсрочки с точки зрения простоты (в основном здесь не вводятся новые концепции, это второй тип recover() а не что-то новое), но признаю, что у него могут быть некоторые проблемы с производительностью. Наличие отдельного ключевого слова catch может обеспечить большую эффективность, поскольку его легче полностью пропустить, когда происходит нормальный возврат, и, если кто-то хочет добиться максимальной эффективности, возможно, привязать их к областям действия, чтобы только один мог быть активным для каждой области или функции. , что, я думаю, обойдется почти без затрат. (Возможно, имя файла с исходным кодом и номер строки также должны быть возвращены из функции catch? Это дешево во время компиляции и позволит избежать некоторых причин, по которым люди прямо сейчас требуют полной трассировки стека.)

Любой из них также позволит эффективно обрабатывать повторяющуюся обработку ошибок в одном месте внутри функции и позволяет легко предлагать обработку ошибок как библиотечную функцию, что, по приведенным выше комментариям rsc, является одним из худших аспектов текущего случая; трудоемкость обработки ошибок имеет тенденцию поощрять "возврат ошибки", а не правильную обработку. Я знаю, что сам много с этим борюсь.

@thejerf В рамках этого предложения Иэн частично рассматривает способы устранения шаблона ошибок, не препятствуя функциям добавлять контекст или иным образом манипулировать ошибками, которые они возвращают.

Разделение обработки ошибок на try и catch кажется, что это будет работать против этой цели, хотя я полагаю, что это зависит от того, какие программы детализации обычно хотят добавить.

По крайней мере, я хотел бы увидеть, как это работает, на более реалистичных примерах.

Вся суть моего предложения состоит в том, чтобы разрешить добавление контекста или манипулирование ошибками способом, который я считаю более программно правильным, чем большинство предложений здесь, которые включают повторение этого контекста снова и снова, что само по себе подавляет желание поместить дополнительный контекст в .

Чтобы переписать исходный пример,

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

выходит как

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

    try syscall.Chdir(dir)
    return nil
}

за исключением того, что этот пример слишком тривиален для любого из этих предложений на самом деле, и я бы сказал, что в этом случае мы просто оставим исходную функцию в покое.

Лично я не считаю исходную функцию Chdir проблемой. Я настраиваю это специально для случая, когда функция зашумлена длительной повторяющейся обработкой ошибок, а не для функции с одной ошибкой. Я бы также сказал, что если у вас есть функция, в которой вы буквально делаете что-то другое для каждого возможного варианта использования, то, вероятно, правильный ответ - продолжать писать то, что у нас уже есть. Однако я подозреваю, что это очень редкий случай для большинства людей на том основании, что если бы это было обычным случаем, то вообще не было бы жалоб. Шум проверки ошибки имеет значение только потому, что люди хотят повторять «в основном одно и то же» снова и снова в функции.

Я также подозреваю, что большая часть того, чего хотят люди, будет удовлетворена

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
}

Если мы избавимся от проблемы попытки сделать один оператор if хорошо выглядящим на том основании, что он слишком мал, чтобы о нем заботиться, и избавимся от проблемы функции, которая действительно делает что-то уникальное для каждой ошибки, возврат на том основании, что A : это на самом деле довольно редкий случай и B: в этом случае накладные расходы шаблона на самом деле не так значительны по сравнению со сложностью уникального кода обработки, возможно, проблема может быть уменьшена до чего-то, что имеет решение.

Я тоже очень хочу увидеть

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

     ...
 }

или возможен какой-то эквивалент, когда это сработает.

И из всех предложений на странице ... разве это не похоже на Go? Он больше похож на Go, чем на текущий Go.

Честно говоря, большая часть моего профессионального инженерного опыта связана с PHP (я знаю), но главной привлекательностью для Go всегда была удобочитаемость. Хотя мне действительно нравятся некоторые аспекты PHP, больше всего я презираю «последнюю», «абстрактную», «статическую» бессмыслицу и применение чрезмерно сложных концепций к фрагменту кода, который выполняет одно действие.

Увидев это предложение, я сразу же вспомнил, как я смотрю на кусок и мне приходится делать двойной дубль и действительно «думать» о том, что этот фрагмент кода говорит / делает. Я не думаю, что этот код читабелен и особо не добавляет к языку. Мой первый инстинкт - посмотреть налево, и я думаю, что это всегда возвращает nil . Однако с этим изменением мне теперь пришлось бы смотреть влево и вправо, чтобы определить поведение кода, что означает больше времени на чтение и больше ментальной модели.

Однако это не означает, что в Go нет возможности улучшить обработку ошибок.

Мне жаль, что я (еще) не прочитал всю эту ветку (она очень длинная), но я вижу, как люди бросают альтернативный синтаксис, поэтому я хотел бы поделиться своей идеей:

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

Надеюсь, я не пропустил что-то выше, что сводит на нет это. Обещаю, что когда-нибудь прочту все комментарии :)

Я считаю, что оператор должен быть разрешен только для типа error , чтобы избежать семантического беспорядка преобразования типов.

Интересно, @buchanae , но разве это нас сильно

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

Я вижу, что это позволит a уйти, тогда как в текущем состоянии он ограничен блоками then и else.

@ object88 Вы правы, изменение тонкое, эстетическое и субъективное. Лично все, что я хочу от Go 2 по этой теме, - это небольшое изменение читабельности.

Лично я считаю его более читабельным, потому что строка не начинается с if и не требует !=nil . Переменные находятся на левом краю, где они (в большинстве?) Других строк.

Отличная точка зрения на объем a , я не учел этого.

Учитывая другие возможности этой грамматики, кажется, что это возможно.

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

и вероятно

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

которые могут быть там, где он разваливается.

Возможно, возврат ошибки должен быть частью каждого вызова функции в Go, чтобы вы могли представить:
`` ''
а: = helloWorld (); ошибаюсь? {
return fmt.Errorf ("Ошибка helloWorld:% s", ошибка)
}

А как насчет реальной обработки исключений? Я имею в виду попробовать, поймать, наконец, а как многие современные языки?

Нет, это делает код неявным и неясным (хотя действительно немного короче)

Вт, 23 ноября 2017 г., в 07:27, Камьяр Миремади [email protected]
написал:

А как насчет реальной обработки исключений? Я имею в виду попробовать, поймать, наконец
а как многие современные языки?

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/21161#issuecomment-346529787 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

Возвращаясь к @mpvl «s WriteToGCS пример вверх-нить , я хотел бы предложить (опять же ) , что фиксация / откат шаблон не является общим достаточно , чтобы оправдать значительные изменения в обработке ошибок Go содержит . Захватить закономерность в функции ( ссылка на игровую площадку ) несложно:

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

Тогда мы можем записать пример как

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

Я бы предложил более простое решение:

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

Для множественных возвратов множественных функций:

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

И для нескольких возвращаемых аргументов:

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

Знак ^ означает возврат, если параметр не равен нулю.
Обычно «переместите ошибку вверх по стеку, если она случится»

Есть ли недостатки у этого метода?

@gladkikhartem
Как изменить ошибку до того, как она будет возвращена?

@urandom
Перенос ошибок - важное действие, которое, на мой взгляд, должно выполняться явно.
Код Go - это удобочитаемость, а не магия.
Я хотел бы, чтобы перенос ошибок был более четким

Но в то же время я хотел бы избавиться от кода, который не несет большого количества информации и просто занимает место.

if err != nil {
    return err
}

Это похоже на клише Го - вы не хотите его читать, вы хотите просто пропустить его.

То, что я видел до сих пор в этом обсуждении, представляет собой комбинацию:

  1. уменьшение многословности синтаксиса
  2. исправление ошибки путем добавления контекста

Это соответствует исходному описанию проблемы @ianlancetaylor, в котором упоминаются оба аспекта, однако, на мой взгляд, их следует обсуждать / определять / экспериментировать отдельно и, возможно, в разных итерациях, чтобы ограничить объем изменений и просто из соображений эффективности ( большие изменения в языке сделать труднее, чем постепенные).

1. Снижение степени детализации синтаксиса

Мне нравится идея @gladkikhartem , даже в ее первоначальной форме, о которой я сообщаю здесь, поскольку она была отредактирована / расширена:

 result, ^ := someAction()

В контексте функции:

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

Этот короткий синтаксис - или в форме, предложенной @gladkikhartem с err^ - решает часть проблемы, связанную с многословием синтаксиса (1).

2. Контекст ошибки

Что касается второй части, добавляя больше контекста, мы могли бы даже полностью забыть об этом, а позже предложить автоматически добавлять трассировку стека к каждой ошибке, если используется специальный тип contextError . Такой новый собственный тип ошибки может содержать полные или короткие трассировки стека (представьте себе GO_CONTEXT_ERROR=full ) и быть совместимым с интерфейсом error , предлагая при этом возможность извлечь как минимум функцию и имя файла из верхнего стека вызовов. Вход.

При использовании contextError каким-то образом Go должен прикрепить трассировку стека вызовов точно в том месте, где возникает ошибка.

Снова с помощью примера функции:

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
}

Изменился только тип с error на contextError , который можно определить как:

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

(обратите внимание, как этот Stack() отличается от https://golang.org/pkg/runtime/debug/#Stack, поскольку мы надеемся иметь здесь небайтовую версию стека вызовов горутины)

Метод Cause() вернет nil или предыдущий contextError в результате вложенности.

Я очень хорошо осведомлен о возможных последствиях для памяти при ношении таких стеков, поэтому я намекнул на возможность иметь короткий стек по умолчанию, который будет содержать только одну или несколько записей. Разработчик обычно включает полные трассировки в версиях для разработки / отладки и оставляет значение по умолчанию (короткие трассировки) в противном случае.

Уровень техники:

  • https://godoc.org/github.com/pkg/errors (я не очень хорошо знаком с этим пакетом или предлагаю моделировать его, но это один из наиболее часто используемых / известных)

Просто пища для размышлений.

@gladkikhartem @ gdm85

Я думаю, вы упустили суть этого предложения. Исходный пост Яна:

Игнорировать ошибку уже легко (возможно, слишком просто) (см. №20803). Многие существующие предложения по обработке ошибок упрощают возврат ошибки без изменений (например, # 16225, # 18721, # 21146, # 21155). Немногие упрощают возврат ошибки с дополнительной информацией.

Возврат ошибок без изменений часто неверен и обычно, по крайней мере, бесполезен. Мы хотим поощрять осторожную обработку ошибок: рассмотрение только варианта использования «возврат без изменений» приведет к смещению стимулов в неверном направлении.

@bcmills, если

На «возврат без изменений» можно было бы противодействовать, как объяснено выше, с помощью «возврата без изменений с трассировкой стека» по умолчанию и (в реактивном стиле) при необходимости добавить удобочитаемое сообщение. Я не указал, как можно добавить такое удобочитаемое сообщение, но для некоторых идей можно увидеть, как работает упаковка в pkg/errors .

«Возврат ошибок без изменений часто является неправильным»: поэтому я предлагаю путь обновления для ленивого варианта использования, который является тем же вариантом использования, который в настоящее время указывается как вредный.

@bcmills
Я на 100% согласен с # 20803, что ошибки следует всегда обрабатывать или явно игнорировать (и я понятия не имею, почему этого не делали раньше ...)
да, я не касался вопроса предложения, и мне не нужно. Меня волнует реальное предлагаемое решение, а не стоящие за ним намерения, потому что намерения не совпадают с результатами. И когда я вижу || такой || Предлагаемые вещи - это меня очень огорчает.

Если встраивание информации, такой как коды ошибок и сообщения об ошибках, в сообщение об ошибке будет простым и прозрачным - вам не нужно будет поощрять осторожную обработку ошибок - люди сделают это сами.
Например, просто сделайте ошибку псевдонимом. Мы могли возвращать любые данные и использовать их вне функции без приведения типов. Сделал бы жизнь намного проще.

Мне нравится, что Go напоминает мне об ошибках, но я ненавижу, когда дизайн побуждает меня делать что-то сомнительное.

@ gdm85
Автоматическое добавление трассировки стека к ошибке - ужасная идея, просто посмотрите и трассировку стека Java.
Когда вы сами оборачиваете ошибки - сориентироваться и понять, что идет не так, намного проще. В этом весь смысл заворачивания.

@gladkikhartem Я не согласен с тем, что форма "автоматической упаковки" была бы намного хуже для навигации и помощи в понимании того, что происходит не так. Я также не понимаю, о чем вы говорите в трассировках стека Java (я предполагаю, что есть исключения? Эстетически некрасиво? Какая конкретная проблема?), Но хочу обсудить в конструктивном направлении: что может быть хорошим определением «тщательно обработанной ошибки»?

Я прошу как улучшить свое понимание передовых практик Go (более или менее каноничных, чем они могут быть), так и потому, что я чувствую, что такое определение может быть ключевым для внесения некоторого предложения по улучшению текущей ситуации.

@gladkikhartem Я знаю, что это предложение уже есть повсюду, но давайте сделаем все возможное, чтобы оно было сосредоточено на целях, которые я изначально поставил. Как я сказал при публикации этой проблемы, уже есть несколько различных предложений по упрощению if err != nil { return err } , и это место для обсуждения синтаксиса, который только улучшает этот конкретный случай. Спасибо.

@ianlancetaylor
Извините, если я убрал обсуждение с пути.

Если вы хотите добавить контекстную информацию к ошибке, я бы предложил использовать этот синтаксис:
(и заставьте людей использовать только один тип ошибки для одной функции для легкого извлечения контекста)

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

Символ ^ используется для обозначения параметра ошибки, а также для отличия определения функции от обработки ошибок для "someAction () {}"
{} может быть опущено, если ошибка возвращается без изменений

Добавление дополнительных ресурсов для ответа на мое собственное приглашение, чтобы лучше определить «осторожную обработку ошибок»:

Каким бы утомительным ни был текущий подход, я думаю, он менее запутан, чем альтернативы, хотя одна строка if-операторов может работать? Может быть?

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

...или даже...

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

Кто-то, возможно, уже поднял этот вопрос, но есть много-много сообщений, и я прочитал многие из них, но не все. Тем не менее, я лично считаю, что было бы лучше быть явным, чем неявным.

Я был поклонником программирования, ориентированного на железную дорогу, идея пришла из выражения with Elixir.
else блок будет выполнен после короткого замыкания e == nil .

Вот мое предложение с псевдокодом впереди:

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 Разве это не похоже на "Попробовать поймать", за исключением того, что "С" - это "Попытка", а "Иначе" - "Поймать"?
Почему бы не реализовать обработку исключений, такую ​​как Java или C #?
прямо сейчас на ходу, если программист не хочет обрабатывать исключение в этой функции, он возвращает его в результате этой функции. Тем не менее, нет способа заставить программистов обрабатывать исключение, если они этого не хотят, и много раз вам это действительно не нужно, но здесь мы получаем множество операторов if err! = Nil, которые делают код уродливым и не читается (много шума). Разве это не причина, по которой оператор Try Catch finally был изобретен в первую очередь на другом языке программирования?

Так что, думаю, лучше, если Go Authors "попробуют" не быть упрямыми !! и просто представьте в следующих версиях оператор «Попробуй поймать наконец». Спасибо.

@KamyarM
Вы не можете ввести обработку исключений в Go, потому что в Go нет исключений.
Представление try {} catch {} в Go похоже на введение try {} catch {} в C - это совершенно неправильно .

@ianlancetaylor
Как насчет того, чтобы вообще не менять обработку ошибок Go, а лучше изменить инструмент gofmt , подобный этому, для обработки однострочных ошибок?

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

Он обратно совместим, и вы можете применить его к своим текущим проектам.

Исключения представляют собой оформленные операторы goto, они превращают ваш стек вызовов в граф вызовов, и есть веская причина, по которой большинство серьезных неакадемических проектов блокируют или ограничивают их использование. Объект с сохранением состояния вызывает метод, который произвольно передает управление вверх по стеку, а затем возобновляет выполнение инструкций ... звучит как плохая идея, потому что это так.

@KamyarM По сути, это так, но на практике это не так. На мой взгляд, потому что мы здесь откровенны и не нарушаем никаких идиом Go.

Почему?

  1. Выражения внутри оператора with не могут объявлять новую переменную, поэтому в ней явно указано, что целью является оценка вне блочных переменных.
  2. Операторы внутри with будут вести себя как внутри try и catch блока. На самом деле он будет медленнее, так как при каждой следующей инструкции ему необходимо оценивать условия with в худшем случае.
  3. Конструктивно намерение состоит в том, чтобы удалить чрезмерное if s , а не для создания обработчика исключений в качестве обработчика всегда будет местном ( with «s выражение и else блок).
  4. Нет необходимости раскручивать стек из-за throw

пс. поправьте меня, если я ошибаюсь.

@ardhitama
KamyarM прав в том смысле, что оператор with выглядит так же уродливо, как и try catch, а также вводит уровень отступов для нормального потока кода.
Не говоря уже о первоначальной идее предложения изменять каждую ошибку в отдельности. Это просто не будет элегантно работать с попыткой поймать, с или любым другим методом , который группирует заявления вместе.

@gladkikhartem
Да, поэтому я предлагаю вместо этого принять «ориентированное на железную дорогу программирование» и стараться не убирать ясность. Это просто еще один угол для атаки на проблему, другие решения хотят ее решить, не позволяя компилятору автоматически писать за вас if err != nil .

with также не только для обработки ошибок, он может быть полезен для любого другого потока управления.

@gladkikhartem
Позвольте мне прояснить, что я считаю Try Catch Finally block красивым. If err!=nil ... на самом деле уродливый код.

Go - это просто язык программирования. Есть так много других языков. Я обнаружил, что многие в сообществе го смотрят на это как на свою религию и не готовы изменить или признать ошибки. Это не правильно.

@gladkikhartem

Я согласен, если авторы Go назовут его Go ++, Go # или GoJava и представят там Try Catch Finally ;)

@KamyarM

Избегать ненужных изменений необходимо - и это очень важно - в любом инженерном начинании. Когда люди говорят об изменении в этом контексте, они на самом деле имеют в виду «изменение к лучшему», что они эффективно передают с помощью аргументов, приводящих предпосылку к предполагаемому выводу.

Призыв "Просто открой свой разум!" Неубедителен. По иронии судьбы, он пытается сказать то, что большинство программистов считает древним и неуклюжим _ новым и улучшенным_.

Есть также много предложений и обсуждений, в которых сообщество Go обсуждает предыдущие ошибки. Но я согласен с вами, когда вы говорите, что Go - это просто язык программирования. Об этом говорится на веб-сайте Go и в других местах, и я поговорил с несколькими людьми, которые это подтвердили.

Я обнаружил, что многие в сообществе го смотрят на это как на свою религию и не готовы изменить или признать ошибки.

Go основан на академических исследованиях; личное мнение не имеет значения.

Даже ведущие разработчики компилятора Microsoft C # публично признали, что _exceptions_ - плохой способ управления ошибками, при этом оценив модель Go / Rust как лучшую альтернативу: http://joeduffyblog.com/2016/02/07/the-error-model/

Конечно, есть возможности для улучшения модели ошибок Go, но не за счет принятия решений, подобных исключениям, поскольку они только добавляют колоссальную сложность в обмен на несколько сомнительных преимуществ.

@ Dr-Terrible Спасибо за статью.

Но я не нашел нигде, где GoLang упоминается как академический язык.

Кстати, чтобы прояснить мою точку зрения, в этом примере

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
}

Аналогично реализации обработки исключений в C # следующим образом:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

Разве это не ужасный способ обработки исключений в C #? Мой ответ - да, не знаю, как у вас! В Go у меня нет другого выбора. Это ужасный выбор или шоссе. Так обстоит дело в GO, и у меня нет выбора.

Кстати, как также упоминалось в статье, которой вы поделились, любой язык может реализовать обработку ошибок, например Go, без какого-либо дополнительного синтаксиса, поэтому Go фактически не реализовал какой-либо революционный способ обработки ошибок. У него просто нет способа обработки ошибок, поэтому вы ограничены использованием оператора If для обработки ошибок.

Кстати, я знаю, что в GO есть нерекомендуемый Panic, Recover , Defer который похож на Try Catch Finally но, по моему личному мнению, синтаксис Try Catch Finally намного чище и лучше организован для обработки исключений.

@ Dr-Terrible

Также проверьте это:
https://github.com/manucorporat/try

@KamyarM , он не сказал, что Go - академический язык, он сказал, что он основан на академических исследованиях. Статья не была посвящена Go, но в ней исследуется парадигма обработки ошибок, используемая Go.

Если вы обнаружите, что manucorporat/try работает для вас, пожалуйста, используйте его в своем коде. Но затраты (производительность, сложность языка и т. Д.) На добавление try/catch к самому языку не стоят компромисса.

@KamyarM
Ваш пример не точен. Альтернативой

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

будет

            Operation1();
            Operation2();
            Operation3();
            Operation4();

обработка исключений в этом примере кажется намного лучшим вариантом. По идее должно быть хорошо, но на практике
вы должны отвечать с точным сообщением об ошибке для каждой ошибки, произошедшей в вашей конечной точке.
Все приложение в Go обычно на 50% обрабатывает ошибки.

         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
    }

И если у людей будет такой мощный инструмент, как try catch, я на 100% уверен, что они будут злоупотреблять им в пользу тщательной обработки ошибок.

Интересно, что упоминается академия, но Go - это сборник уроков, извлеченных из практического опыта. Если цель состоит в том, чтобы написать плохой API, который возвращает неверные сообщения об ошибках, лучше всего подойдет обработка исключений.

Однако мне не нужна ошибка invalid HTTP header когда мой запрос содержит искаженный JSON request body , обработка исключений - это волшебная кнопка включения и забывания, которая обеспечивает это в API C ++ и C #, которые используют их.

Для большого охвата API невозможно предоставить достаточно контекста ошибки для достижения значимой обработки ошибок. Это потому, что любое хорошее приложение на 50% обрабатывает ошибки в Go и должно быть на 90% на языке, который требует нелокальной передачи управления для обработки ошибок.

@gladkikhartem

Упомянутый вами альтернативный способ - это правильный способ написания кода на C #. Это всего лишь 4 строки кода, которые показывают удачный путь выполнения. В нем нет этих if err!=nil шумов. Если происходит исключение, функция, которая заботится об этом исключении, может обработать его, используя Try Catch Finally (это может быть та же самая функция, либо вызывающая сторона, либо вызывающая сторона вызывающего абонента, либо вызывающая сторона вызывающего абонента вызывающего абонента. ... или просто обработчик событий, обрабатывающий все необработанные ошибки в приложении. У программиста есть разные варианты.)

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
    }

Выглядит просто, но это непросто. Я думаю, вы могли бы создать собственный тип ошибки, который несет в себе системную ошибку, ошибку пользователя (без утечки внутреннего состояния пользователю, у которого могут быть не самые лучшие намерения) и код HTTP.

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

Но попробуйте

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

Все они содержат какую-то неочевидную магию, которая не упрощает читателя. В первых двух примерах err становится своего рода псевдоключевым словом или спонтанно возникающей переменной. В последних двух примерах совсем не ясно, что должен делать этот оператор : - будет ли ошибка автоматически возвращаться? Является ли правая часть оператора одним оператором или блоком?

FWIW, я бы написал функцию-оболочку, чтобы вы могли сделать return newPathErr("chdir", dir, syscall.Chdir(dir)) и она автоматически возвращала бы ошибку nil, если третий параметр равен nil. :-)

ИМО, лучшее предложение, которое я видел для достижения целей «упростить обработку ошибок в Go» и «вернуть ошибку с дополнительной контекстной информацией», взято из

a, b, err? := f1()

расширяется до этого:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

и я могу заставить его запаниковать вот так:

a, b, err! := f1()

расширяется до этого:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

Это сохранит обратную совместимость и устранит все болевые точки обработки ошибок на ходу.

Это не обрабатывает такие случаи, как функции bufio, которые возвращают ненулевые значения, а также ошибки, но я думаю, что можно выполнять явную обработку ошибок в тех случаях, когда вам небезразличны другие возвращаемые значения. И, конечно же, возвращаемые значения, не содержащие ошибок, должны быть подходящими для этого типа значением nil.

? модификатор уменьшит количество стандартных ошибок в функциях и! модификатор будет делать то же самое для мест, где assert будет использоваться на других языках, например, в некоторых основных функциях.

Это решение имеет то преимущество, что оно очень простое и не требует слишком многого, но я думаю, что оно отвечает требованиям, изложенным в этом заявлении.

В случае, если у вас есть ...

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
}

Если что-то пойдет не так в foo , то на сайте вызова bar ошибка будет дважды заключена в один и тот же текст без добавления какого-либо смысла. По крайней мере, я бы возразил против части предложения errors.Wrap .

Но если продолжить расширение, каков ожидаемый результат этого?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

Переназначены ли a и b на нулевые значения? Если так, то это магия, которой, как мне кажется, следует избегать. Выполняют ли они ранее присвоенные ценности? (Я сам не забочусь об именованных возвращаемых значениях, но их все же следует учитывать в рамках этого предложения.)

@ dup2X Ага давайте уберем язык, он должен быть больше таким

@ object88 естественно ожидать, что в случае ошибки все остальное обнулится. Это просто для понимания, и в нем нет никакого волшебства, это уже в значительной степени соглашение для кода Go, не требует запоминания и не имеет особых случаев. Если мы позволим сохранить ценности, это усложнит ситуацию. Если вы забудете проверить наличие ошибок, возвращенные значения могут быть использованы случайно, и в этом случае может произойти что угодно. Как вызов методов для частично выделенной структуры вместо паники при nil. Программисты могут даже начать ожидать возврата определенных значений в случае ошибки. На мой взгляд, это был бы бардак и ничего хорошего из этого не получилось бы.

Что касается упаковки, я не думаю, что сообщение по умолчанию дает что-то полезное. Было бы хорошо просто объединить ошибки в цепочку. Например, когда исключения содержат внутренние исключения. Очень полезно для отладки ошибок глубоко внутри библиотеки.

С уважением, я не согласен, @creker. У нас есть примеры этого сценария в Go stdlib с возвращаемыми значениями, отличными от nil, даже в случае ошибки, отличной от nil, и на самом деле они функциональны, например, несколько функций в структуре bufio.Reader . Нас, программистов Go, активно поощряют проверять / обрабатывать все ошибки; игнорировать ошибки кажется более вопиющим, чем получать ненулевые возвращаемые значения и ошибку. В случае, который вы цитируете, если вы возвращаете nils и не проверяете ошибку, вы все равно можете работать с недопустимым значением.

Но если отложить это в сторону, давайте рассмотрим это немного дальше. Какова будет семантика оператора ? ? Может ли его применяться только к типам, реализующим интерфейс error ? Можно ли его применить к другим типам или возвращаемым аргументам? Если его можно применить к типам, которые не реализуют ошибку, запускается ли он каким-либо значением / указателем, отличным от nil? Может ли оператор ? применяться более чем к одному возвращаемому значению, или это ошибка компилятора?

@erwbgy
Если вы хотите просто вернуть ошибку без прикрепления к ней ничего полезного - было бы намного проще просто указать компилятору обрабатывать все необработанные ошибки как «if err! = Nil return ...», например:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

И не надо дополнительных сумасшедших? символ в этом случае.

@ object88
Я попытался применить большинство предложений по упаковке ошибок, показанных здесь, в реальном коде и столкнулся с одной серьезной проблемой - код становится слишком плотным и нечитаемым.
Он просто жертвует шириной кода в пользу высоты кода.
Обертывание ошибок с помощью обычного if err! = Nil на самом деле позволяет распространять код для лучшей читаемости, поэтому я не думаю, что нам вообще нужно что-то менять для переноса ошибок.

@ object88

В случае вашего сайта, если вы возвращаете nils и не проверяете ошибку, вы все равно можете работать с недопустимым значением.

Но это приведет к очевидной и легко обнаруживаемой ошибке, такой как паника на нуле. Если вам действительно нужно вернуть значимые значения при ошибке, вы должны сделать это явно и точно задокументировать, какое значение можно использовать и в каком случае. Просто возвращать случайные данные, которые оказались в переменных при ошибке, опасно и приведет к незаметным ошибкам. Опять же, из этого ничего не получается.

@gladkikhartem проблема с if err! = nil заключается в том, что в нем полностью потеряна фактическая логика, и вам нужно активно ее искать, если вы хотите понять, что делает код на своем успешном пути, и не заботитесь обо всей этой обработке ошибок . Это похоже на чтение большого количества кода C, где у вас есть несколько строк фактического кода, а все остальное - просто проверка ошибок. Люди даже прибегают к макросам, которые оборачивают все это и переходят в конец функции.

Я не понимаю, как логика может стать слишком плотной в правильно написанном коде. Это логика. Каждая строка вашего кода содержит актуальный код, который вам нужен, это то, что вам нужно. Чего вы не хотите, так это повторения строк и строк шаблона. Используйте комментарии и разбейте код на блоки, если это помогает. Но это больше похоже на проблему с реальным кодом, а не с языком.

Это работает на детской площадке, если вы ее не переформатируете:

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

Мне кажется, что исходное предложение и многие другие, упомянутые выше, пытаются придумать четкий сокращенный синтаксис для этих 100 символов и помешать gofmt настаивать на добавлении разрывов строк и переформатировании блока на 3 строки.

Итак, давайте представим, что мы изменили gofmt, чтобы перестать настаивать на многострочных блоках, начнем с приведенной выше строки и попытаемся придумать способы сделать ее короче и понятнее.

Я не думаю, что часть перед точкой с запятой (назначение) нужно менять, чтобы осталось 69 символов, которые мы могли бы сократить. Из них 49 - это оператор return, возвращаемые значения и упаковка ошибок, и я не вижу особой пользы в изменении синтаксиса этого (скажем, делая операторы return необязательными, что сбивает пользователей с толку).

Остается найти сокращение для ; if err != nil { _ } где подчеркивание представляет кусок кода. Я думаю, что любое сокращение должно явно включать err для ясности, даже если оно делает сравнение nil несколько невидимым, поэтому нам остается придумать сокращение для ; if _ != nil { _ } .

Представьте на мгновение, что мы используем один символ. Я собираюсь выбрать § в качестве заполнителя для любого символа, который мог бы быть. Тогда строка кода будет такой:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

Я не понимаю, как вы могли бы добиться большего без изменения существующего синтаксиса присваивания или синтаксиса возврата, или без невидимой магии. (Есть еще некоторая магия в том, что тот факт, что мы сравниваем err с nil, не так очевиден.)

Это 88 символов, всего 12 символов в строке длиной 100 символов.

Итак, мой вопрос: действительно ли это стоит делать?

Изменить: я думаю, что моя точка зрения заключается в том, что когда люди смотрят на блоки Go if err != nil и говорят: «Я бы хотел, чтобы мы могли избавиться от этого дерьма», 80-90% того, о чем они говорят, - это _stuff, которое у вас есть делать для обработки ошибок_. Фактические накладные расходы, вызванные синтаксисом Go, минимальны.

@lpar , вы в основном следуете той же логике, которую я применил выше , поэтому, естественно, я согласен с вашими рассуждениями. Но я думаю, вы не обращаете внимания на визуальную привлекательность размещения всех ошибок справа:

a, b := Frob("one string")  § err { return ... }

читается лучше, чем простое сокращение количества символов.

@lpar вы можете сохранить еще больше символов, если удалите довольно много бесполезных fmt.Errorf , измените return на некоторый специальный синтаксис и добавите стек вызовов к ошибкам, чтобы они имели реальный контекст для них, а не были просто прославленными строками. Это оставило бы вас с чем-то вроде этого

a, b, err? := Frob("one string")

Проблема с ошибками Go для меня всегда заключалась в отсутствии контекста. Возврат и упаковка строк вообще бесполезны для определения того, где на самом деле произошла ошибка. Вот почему, например, github.com/pkg/errors стал для меня необходимостью. С такими ошибками я получаю преимущества простоты обработки ошибок Go и преимущества исключений, которые идеально фиксируют контекст и позволяют найти точное место сбоя.

И даже если мы возьмем ваш пример таким, какой он есть, тот факт, что обработка ошибок находится справа, является значительным улучшением читабельности. Вам больше не нужно пропускать несколько строк шаблона, чтобы понять фактический смысл кода. Вы можете говорить, что хотите, о важности обработки ошибок, но когда я читаю код, чтобы понять его, меня не волнуют ошибки. Все, что мне нужно, это успешный путь. И когда мне понадобятся ошибки, я буду их искать. Ошибки по своей природе являются исключительным случаем и должны занимать как можно меньше места.

Я думаю, что вопрос о том, является ли fmt.Errorf «бесполезным» по сравнению с errors.Wrap , ортогонален этой проблеме, поскольку они оба примерно одинаково многословны. (В реальных приложениях я тоже не использую, я использую что-то еще, что также регистрирует ошибку и строку кода, в которой она произошла.)

Так что, я думаю, некоторым людям действительно нравится, что обработка ошибок справа. Я просто не уверен в этом, даже имея опыт работы с Perl и Ruby.

@lpar Я использую errors.Wrap потому что он автоматически захватывает стек вызовов - мне действительно не нужны все эти сообщения об ошибках. Меня больше волнует место, где это произошло, и, возможно, какие аргументы были переданы функции, вызвавшей ошибку. Вы даже говорите, что делаете то же самое - записываете строку кода, чтобы предоставить некоторый контекст для сообщений об ошибках. Учитывая, что мы можем думать о способах сокращения шаблонного кода, но при этом давать ошибкам больше контекста (это в значительной степени предложение здесь).

Что касается ошибок, находящихся справа. Для меня это не просто место, а снижение когнитивной нагрузки, необходимой для чтения кода, заваленного обработкой ошибок. Я не верю аргументу, что ошибки настолько важны, что вы хотите, чтобы они занимали столько же места, сколько и они. Я бы предпочел, чтобы они уезжали как можно чаще. Это не главная история.

@creker

Это скорее описывает тривиальную ошибку разработчика, чем ошибку в производственной системе, генерирующую ошибку из-за неправильного ввода данных пользователем. Если все, что вам нужно для определения ошибки, - это номер строки и путь к файлу, скорее всего, вы только что написали код и уже знаете, что не так.

@ как и исключения. В большинстве случаев стека вызовов и сообщения об исключении достаточно, чтобы определить место и причину возникновения ошибки. В более сложных случаях вы, по крайней мере, знаете место, где произошла ошибка. Исключения дают вам это преимущество по умолчанию. С Go вам нужно либо объединить ошибки в цепочку, в основном имитируя стек вызовов, либо включить фактический стек вызовов.

В правильно написанном коде большую часть времени вы знаете точную причину по номеру строки и пути к файлу, потому что ошибка вполне ожидаема. Вы на самом деле написали код, готовясь к тому, что это может произойти. Если произойдет что-то неожиданное, тогда да, стек вызовов не даст вам причины, но значительно сократит пространство для поиска.

@в виде

По моему опыту, ошибки пользовательского ввода обрабатываются практически сразу. Настоящие проблемные производственные ошибки происходят глубоко в коде (например, одна служба не работает, в результате чего другая служба выдает ошибки), и очень полезно получить правильную трассировку стека. Как бы то ни было, трассировки стека java чрезвычайно полезны при отладке производственных проблем, а не сообщений.

@creker
Ошибки - это просто значения , и они являются частью входов и выходов функции. Они не могли быть «неожиданными».
Если вы хотите узнать, почему функция выдала ошибку - используйте тестирование, ведение журнала и т. Д.

@gladkikhartem в реале все не так просто. Да, вы ожидаете ошибок в том смысле, что сигнатура функции включает ошибку в качестве возвращаемого значения. Но я ожидаю, что точно знаю, почему это произошло и что вызвало это, так что вы действительно знаете, что делать, чтобы исправить это или не исправлять вообще. Плохой ввод данных пользователем обычно очень просто исправить, просто просмотрев сообщение об ошибке. Если вы используете буферы profocol и какое-то обязательное поле не установлено, это ожидаемо, и это действительно легко исправить, если вы правильно проверяете все, что получаете по сети.

На данный момент я больше не понимаю, о чем мы спорим. Трассировка стека или цепочка сообщений об ошибках очень похожи, если они реализованы должным образом. Они сокращают пространство поиска и предоставляют полезный контекст для воспроизведения и исправления ошибки. Нам нужно подумать о способах упрощения обработки ошибок при обеспечении достаточного контекста. Я никоим образом не утверждаю, что простота важнее правильного контекста.

Это аргумент Java - переместите весь код ошибки в другое место, чтобы вам не приходилось на него смотреть. Я считаю, что это заблуждение; не только потому, что механизм Java для этого в значительной степени не работает, но потому, что, когда я смотрю на код, то, как он будет вести себя при возникновении ошибки, для меня так же важно, как и то, как он будет вести себя, когда все работает.

Никто не приводит таких аргументов. Не будем путать то, что здесь обсуждается, с обработкой исключений, при которой вся обработка ошибок находится в одном месте. Назвать это «в значительной степени провальным» - это всего лишь мнение, но я не думаю, что Go в любом случае вернется к этому. Обработка ошибок Go отличается, и ее можно улучшить.

@creker Я попытался то же самое и попросил пояснить, что считается значимым / полезным сообщением об ошибке.

По правде говоря, я бы в любой день отдал текст сообщения об ошибке переменного качества (который имеет предвзятость разработчика, написавшего его в тот момент и с этим знанием) в обмен на стек вызовов и аргументы функции. С текстом сообщения об ошибке ( fmt.Errorf или errors.New ) вы заканчиваете поиск текста в исходном коде при чтении стеков вызовов / трассировок (которые, по-видимому, ненавидят, и я надеюсь, что не по эстетическим причинам) соответствует поиску напрямую по номеру файла / строки ( errors.Wrap и аналогичные).

Два разных стиля, но цель одна: попытаться воспроизвести в уме то, что происходило во время выполнения в этих условиях.

По этой теме в выпуске № 19991, возможно, содержится обоснованное резюме подхода ко второму стилю определения значимых ошибок.

переместите весь код ошибки в другое место, чтобы вам не приходилось на него смотреть

@lpar , если вы отвечаете на мою точку зрения о перемещении обработки ошибок вправо: есть большая разница между сносками / концевыми сносками (Java) и боковыми примечаниями (мое предложение). Боковые заметки требуют лишь крохотного сдвига глаз без потери контекста.

@ gdm85

вы в конечном итоге ищете текст в исходном коде

Именно то, что я имел в виду, говоря о схожести трассировок стека и связанных сообщений об ошибках. Оба они записывают путь, по которому произошла ошибка. Только в случае сообщений вы можете получить совершенно бесполезные сообщения, которые могут быть откуда угодно в программе, если вы не будете достаточно осторожны при их написании. Единственное преимущество связанных ошибок - это возможность записывать значения переменных. И даже это можно автоматизировать в случае аргументов функции или даже переменных в целом и, по крайней мере, для меня, охватит почти все, что мне нужно, от ошибок. Они по-прежнему будут значениями, вы все равно можете переключить их, если вам нужно. Но в какой-то момент вы, вероятно, зарегистрируете их, и возможность увидеть трассировку стека будет чрезвычайно полезна.

Вы только посмотрите, что Go делает с паникой. Вы получаете полную трассировку стека каждой горутины. Я не могу вспомнить, сколько раз это помогало мне выявить причину ошибки и исправить ее в кратчайшие сроки. Меня часто удивляло, насколько это просто. Он отлично работает, весь язык очень предсказуем, что вам даже не нужен отладчик.

Кажется, что вокруг всего, что связано с Java, есть клеймо, и люди часто не приводят никаких аргументов. Плохо только потому, что. Я не фанат Java, но такие рассуждения никому не помогают.

Опять же, разработчик не должен исправлять ошибки. Это одно из преимуществ обработки ошибок. Способ Java научил разработчиков тому, что это и есть обработка ошибок, а не это. Ошибки могут существовать на уровне приложения и за его пределами на уровне потока. Ошибки в Go обычно используются для управления стратегией восстановления системы - во время выполнения, а не во время компиляции.

Это может быть непонятно, когда языки нарушают управление потоком в результате ошибки, раскрывая стек и теряя память обо всем, что они делали до возникновения ошибки. Ошибки действительно полезны во время выполнения в Go; Я не понимаю, почему они должны содержать такие вещи, как номера строк - работающий код не заботится об этом.

@в виде

Опять же, ошибки не предназначены для исправления ошибок разработчиком.

Это совершенно неправильно. Ошибки есть именно по этой причине. Они не ограничиваются этим, но это одно из основных применений. Ошибки указывают на то, что с системой что-то не так, и вам следует что-то с этим сделать. В случае ожидаемых и простых ошибок вы можете попытаться исправить, например, тайм-аут TCP. Для чего-то более серьезного вы сбрасываете его в журналы, а затем устраняете проблему.

Это одно из преимуществ обработки ошибок. Способ Java научил разработчиков тому, что это и есть обработка ошибок, а не это.

Я не знаю, чему вас научила Java, но я использую исключения по той же причине - для управления стратегией восстановления, которую система использует во время выполнения. В Go нет ничего особенного с точки зрения обработки ошибок.

Это может быть непонятно, когда языки нарушают управление потоком в результате ошибки, распутывая стек и теряя память обо всем, что они делали до того, как произошла ошибка.

Может быть, для кого-то, а не для меня.

Ошибки действительно полезны во время выполнения в Go; Я не понимаю, почему они должны содержать такие вещи, как номера строк - работающий код не заботится об этом.

Если вы заботитесь об исправлении ошибок в своем коде, то номера строк - лучший способ сделать это. Об этом нас научила не Java, в C есть __LINE__ и __FUNCTION__ именно по этой причине. Вы хотите регистрировать свои ошибки и записывать точное место, где они произошли. А когда что-то идет не так, по крайней мере, есть с чего начать. Это не случайное сообщение об ошибке, вызванное неисправимой ошибкой. Если вам не нужна такая информация, игнорируйте ее. Тебе это не повредит. Но, по крайней мере, он есть и может быть использован при необходимости.

Я не понимаю, почему здесь люди продолжают переключать разговор на исключения и значения ошибок. Никто не сравнивал. Единственное, что обсуждалось, это то, что трассировки стека очень полезны и несут много контекстной информации. Если это непонятно, то вы, вероятно, живете в совершенно другой вселенной, где не существует трассировки.

Это совершенно неправильно.

Но производственная система, о которой я говорю, все еще работает, и она использует ошибки для управления потоком, написана на Go и заменила более медленную реализацию на языке, который использовал трассировку стека для распространения ошибок.

Если это непонятно, то вы, вероятно, живете в совершенно другой вселенной, где не существует трассировки.

Чтобы объединить информацию стека вызовов для каждой функции, возвращающей тип ошибки, сделайте это по своему усмотрению. Трассировка стека работает медленнее и не подходит для использования за пределами игрушечных проектов по соображениям безопасности. Технический фол - заставлять их первоклассных граждан Go просто помогать бездумным стратегиям распространения ошибок.

если вам не нужна такая информация, игнорируйте ее. Тебе это не повредит.

Раздутие программного обеспечения - причина того, что серверы переписаны на Go. То, чего вы не видите, может снизить пропускную способность вашего конвейера.

Я бы предпочел примеры реального программного обеспечения, которое извлекает выгоду из наличия этой функции, вместо умеренно несущественного урока по обработке тайм-аутов TCP и дампа журналов.

Трассировка стека выполняется медленнее

Учитывая, что трассировки стека генерируются на пути к ошибке, никого не волнует, насколько они медленные. Нормальная работа программы уже была прервана.

и непригоден для использования вне игрушечных проектов по соображениям безопасности

Пока я еще не видел, чтобы ни одна производственная система отключала трассировку стека из-за «соображений безопасности» или вообще в этом отношении. С другой стороны, возможность быстро определить путь, по которому код произвел ошибку, оказалась чрезвычайно полезной. И это для больших проектов, когда над кодовой базой работает много разных команд, и никто не знает всю систему полностью.

То, чего вы не видите, может снизить пропускную способность вашего конвейера.

Нет, действительно не так. Как я сказал ранее, трассировки стека генерируются при ошибках. Если ваше программное обеспечение не сталкивается с ними постоянно, пропускная способность не пострадает.

Учитывая, что трассировки стека генерируются на пути к ошибке, никого не волнует, насколько они медленные. Нормальная работа программы уже была прервана.

Не соответствует действительности.

  • Ошибки могут возникать при нормальной работе.
  • Ошибки можно исправить, и программа может продолжить работу, поэтому производительность все еще под вопросом.
  • Замедление одной подпрограммы истощает ресурсы других подпрограмм, которые _are_ работают по счастливому пути.

@ object88 представьте себе настоящий производственный код. Сколько ошибок вы ожидаете от этого? Я бы не думал много. По крайней мере, в правильно написанном приложении. Если горутина находится в цикле занятости и постоянно выдает ошибки на каждой итерации, значит, с кодом что-то не так. Но даже если это так, учитывая, что большинство приложений Go имеют привязку к вводу-выводу, даже это не будет серьезной проблемой.

@в виде

Но производственная система, о которой я говорю, все еще работает, и она использует ошибки для управления потоком, написана на Go и заменила более медленную реализацию на языке, который использовал трассировку стека для распространения ошибок.

Мне очень жаль, но это бессмысленное предложение, не имеющее ничего общего с тем, что я сказал. Не собираюсь на это отвечать.

Трассировка стека выполняется медленнее

Медленнее, но насколько? Это имеет значение? Я так не думаю. Приложения Go в целом привязаны к вводу-выводу. В этом случае глупо гоняться за циклами процессора. У вас гораздо большие проблемы во время выполнения Go, которые съедают процессор. Это не аргумент, чтобы отказаться от полезной функции, помогающей исправлять ошибки.

непригоден для использования вне игрушечных проектов по соображениям безопасности.

Я не собираюсь покрывать несуществующие «соображения безопасности». Но напомню, что обычно трассировки приложений хранятся внутри, и только разработчики имеют к ним доступ. И попытки скрыть имена ваших функций в любом случае - пустая трата времени. Это не безопасность. Надеюсь, мне не нужно вдаваться в подробности.

Если вы настаиваете на соображениях безопасности, я бы хотел, чтобы вы, например, подумали о macOS / iOS. У них нет проблем с созданием паники и аварийных дампов, содержащих стеки всех потоков и значений всех регистров ЦП. Не думайте, что на них влияют эти "соображения безопасности".

Технический фол - заставлять их первоклассных граждан Go просто помогать бездумным стратегиям распространения ошибок.

Не могли бы вы быть более субъективными? "бездумные стратегии распространения ошибок" где вы это видели?

Раздутие программного обеспечения - причина того, что серверы переписаны на Go. То, чего вы не видите, может снизить пропускную способность вашего конвейера.

Опять же, на сколько?

Я бы предпочел примеры реального программного обеспечения, которое извлекает выгоду из наличия этой функции, вместо умеренно несущественного урока по обработке тайм-аутов TCP и дампа журналов.

Здесь, кажется, я разговариваю с кем угодно, только не с программистом. Отслеживание приносит пользу любому программному обеспечению. Это общий метод для всех языков и всех типов программного обеспечения, помогающий исправлять ошибки. Вы можете прочитать википедию, если хотите получить дополнительную информацию.

Так много непродуктивных дискуссий без единого мнения означает, что нет элегантного способа решить эту проблему.

@ object88
Трассировка стека может быть медленной, если вы хотите получить трассировку всех горутин, потому что Go должен дождаться разблокировки других горутин.
Если вы просто отслеживаете горутину, которую вы сейчас используете, - это не так уж и медленно.

@creker
Отслеживание приносит пользу всему программному обеспечению, но это зависит от того, что вы отслеживаете. В большинстве проектов Go я принимал участие в трассировке стеков, потому что здесь задействован параллелизм. Данные перемещаются назад и вперед, множество вещей обменивается данными друг с другом, а некоторые горутины - это всего лишь несколько строк кода. Трассировка стека в таком случае не помогает.
Вот почему я использую ошибки, обернутые контекстной информацией, записанной в журнал, чтобы воссоздать ту же трассировку стека, но которая привязана не к фактическому стеку горутин, а к самой логике приложения.
Чтобы я мог просто сделать cat * .log | grep "orderID = xxx" и получить трассировку стека фактической последовательности действий, которые привели к ошибке.
Из-за параллельной природы Go контекстно-насыщенные ошибки более ценны, чем трассировки стека.

@gladkikhartem, спасибо, что

Я понимаю ваш аргумент и частично согласен с ним. Тем не менее, мне приходится иметь дело со стеками по крайней мере из 5 функций. Этого уже достаточно, чтобы можно было понять, что происходит и с чего начать. Но в сильно параллельном приложении с множеством очень маленьких горутин трассировки стека теряют свои преимущества. Я согласен с этим.

@creker

представьте себе настоящий производственный код. Сколько ошибок вы ожидаете от этого? [...], учитывая, что большинство приложений Go привязаны к вводу-выводу, даже это не будет серьезной проблемой.

Хорошо, что вы упомянули операции, связанные с вводом-выводом. Метод чтения исправную ошибку в EOF. Так что на счастливом пути это произойдет много раз.

@urandom

Трассировки стека непреднамеренно раскрывают информацию, ценную для профилирования системы.

  • Имена пользователей
  • Пути файловой системы
  • Тип / версия серверной базы данных
  • Поток транзакции
  • Структура объекта
  • Алгоритмы шифрования

Я не знаю, заметит ли обычное приложение накладные расходы на сбор кадров стека в типах ошибок, но могу сказать вам, что для приложений, критичных к производительности, многие небольшие функции Go вручную встроены из-за накладных расходов на вызов текущей функции. Отслеживание только усугубит ситуацию.

Я считаю, что цель Go - иметь простое и быстрое программное обеспечение, и трассировка была бы шагом назад. Мы должны иметь возможность писать небольшие функции и возвращать ошибки из этих функций без снижения производительности, которое поощряет нестандартные типы ошибок и ручное встраивание.

@creker

Я не буду приводить вам примеры, вызывающие дальнейший диссонанс. Мне жаль, что я вас разочаровал.

Я бы предложил использовать новое ключевое слово «returnif», имя которого сразу раскрывает его функцию. Кроме того, он достаточно гибкий, чтобы его можно было использовать в большем количестве случаев, чем обработка ошибок.

Пример 1 (с использованием именованного возврата):

а, эрр = что-то (б)
if err! = nil {
возвращение
}

Станет:

а, эрр = что-то (б)
returnif err! = ноль

Пример 2 (без использования именованного возврата):

a, err: = что-то (b)
if err! = nil {
вернуть, ошибиться
}

Станет:

a, err: = что-то (b)
returnif err! = nil {a, err}

Что касается вашего именованного примера возврата, вы имеете в виду ...

a, err = something(b)
returnif err != nil

@ambernardino
Почему бы вместо этого просто не обновить инструмент fmt, и вам не нужно обновлять синтаксис языка и добавлять новые бесполезные ключевые слова

a, err := something(b)
if err != nil { return a, err }

или

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem идея не в том, чтобы вводить это каждый раз, когда вы хотите распространить ошибку, я бы предпочел это и должно работать так же

a, err? := something(b)

@mrkaspa
Идея состоит в том, чтобы сделать код более читабельным . Набрать код - не проблема, а прочитать - тоже.

@gladkikhartem rust использует этот подход, и я не думаю, что это делает его менее читаемым

@gladkikhartem Я не думаю, что ? делает его менее читабельным. Я бы сказал, что он полностью устраняет шум. Проблема для меня в том, что с шумом он также исключает возможность предоставления полезного контекста. Я просто не понимаю, где вы могли бы вставить обычное сообщение об ошибке или обернуть ошибки. Стек вызовов - очевидное решение, но, как уже было сказано, работает не для всех.

@mrkaspa
И я думаю, что это делает его менее читабельным, что дальше? Пытаемся найти лучшее решение или просто делимся мнениями?

@creker
'?' Персонаж добавляет когнитивную нагрузку на читателя, потому что не так очевидно, что будет возвращено, и, конечно, человек должен знать, что это за штука делает. И конечно? Знак вызывает у читателя вопросы.

Как я уже сказал ранее, если вы хотите избавиться от err! = Nil, компилятор может обнаружить неиспользуемые параметры ошибок и сам их переслать.
И

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

станет более читаемым

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

та же магия, только меньше набирать и думать меньше

@gladkikhartem ну, я не думаю, что есть решение, которое не потребовало бы от читателей изучения чего-то нового. Это следствие смены языка. Мы должны пойти на компромисс - либо мы живем с многословным синтаксисом вашего лица, который точно показывает, что делается в примитивных терминах, либо мы вводим новый синтаксис, который может скрыть многословие, добавить немного синтаксического сахара и т. Д. Другого пути нет. Просто отвергать все, что помогает читателю узнать что-то новое, контрпродуктивно. Мы могли бы просто закрыть все проблемы с Go2 и положить конец этому.

Что касается вашего примера, он вводит еще больше волшебства и скрывает любую точку внедрения, чтобы ввести синтаксис, который позволил бы разработчику предоставить контекст для ошибок. И, что наиболее важно, он полностью скрывает любую информацию о том, какой вызов функции может вызвать ошибку. Это все больше и больше пахнет исключениями. И если мы серьезно относимся к простому повторному выбрасыванию ошибок, трассировка стека становится обязательной, потому что это единственный способ сохранить контекст в этом случае.

Первоначальное предложение на самом деле уже довольно хорошо охватывает все это. Он достаточно подробный и дает вам хорошее место, чтобы обернуть ошибку и предоставить полезный контекст. Но одна из главных проблем - это волшебное «заблуждение». Я считаю это некрасивым, потому что в нем недостаточно магии и недостаточно многословности. Это вроде как посередине. Что может сделать лучше, так это привнесение большего количества магии.

Что, если || вызовет новую ошибку, которая автоматически закроет исходную ошибку. Таким образом, пример становится таким

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

err просто исчезает, и вся упаковка выполняется неявно. Теперь нам нужен способ получить доступ к этим внутренним ошибкам. Я думаю, что добавление другого метода, такого как Inner() error в интерфейс error , не сработает. Один из способов - ввести встроенную функцию, например unwrap(error) []error . Что он делает, так это возвращает фрагмент всех внутренних ошибок в том порядке, в котором они были упакованы. Таким образом, вы можете получить доступ к любой внутренней ошибке или диапазону их.

Реализация этого сомнительна, учитывая, что error - это просто интерфейс, и вам нужно место, куда помещать эти завернутые ошибки для любого настраиваемого типа error .

На мой взгляд, это отвечает всем требованиям, но может показаться слишком волшебным. Но, учитывая, что интерфейс ошибок довольно особенный по определению, приближение его к первоклассному гражданину может быть неплохой идеей. Обработка ошибок многословна, потому что это обычный код Go, в этом нет ничего особенного. Это может быть хорошо на бумаге и для создания ярких заголовков, но ошибки слишком особенные, чтобы требовать такого обращения. Им нужен специальный кожух.

В первоначальном предложении говорится о сокращении количества проверок ошибок или продолжительности каждой отдельной проверки?

Если последнее, то опровергать необходимость предложений на том основании, что есть только одно условное утверждение и одно повторение, тривиально. Некоторым людям не нравятся циклы, должны ли мы также предоставлять неявную конструкцию цикла?

Предлагаемые изменения синтаксиса до сих пор служили интересным мысленным экспериментом, но ни одно из них не является таким ясным, произносимым или простым, как оригинал. Go - это не bash, и ничего не должно быть "волшебным" в отношении ошибок.

Все больше и больше я читаю эти предложения, все больше и больше я вижу людей, аргументы которых - не что иное, как «он добавляет что-то новое, так что это плохо, нечитабельно, оставьте все как есть».

@ поскольку предложение дает краткое изложение того, чего оно пытается достичь. То, что делается, довольно четко определено.

такой же ясный, произносимый или простой, как оригинал

Любое предложение будет вводить новый синтаксис, и новизна для некоторых людей звучит так же, как «нечитабельный, сложный и т. Д.». То, что оно новое, не делает его менее ясным, понятным или простым. "||" и "?" примеры так же ясны и просты, как существующий синтаксис, если вы знаете, что он делает. Или нам следует начать жаловаться на то, что «->» и «<-» слишком волшебны и читатель должен знать, что они означают? Заменим их вызовами методов.

Go - это не bash, и ничего не должно быть "волшебным" в отношении ошибок.

Это совершенно необоснованно и ни к чему не может относиться. При чем здесь Bash, я не понимаю.

@creker
Да, я полностью согласен с вами, что событие принесло больше волшебства. Мой пример - это просто продолжение? идея оператора печатать меньше материала.

Я согласен с тем, что нам нужно чем-то пожертвовать и внести какие-то изменения и, конечно же, немного магии. Это просто баланс плюсов юзабилити и минусов такой магии.

Оригинал || предложение довольно хорошее и проверено на практике, но форматирование, на мой взгляд, некрасивое, я бы предложил изменить форматирование на

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PS что вы думаете о таком варианте волшебного синтаксиса?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem оба выглядят неплохо с точки зрения удобочитаемости, но у меня плохое предчувствие от последнего. Он вводит эту странную блочную область видимости, в которой я не уверен.

Я бы посоветовал вам смотреть на синтаксис не изолированно, а в контексте функции. В этом методе есть несколько различных блоков обработки ошибок.

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
}

Как предлагаемые изменения убирают эту функцию?

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
}

@Bcmills рекламного пакета припадки лучше.

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
}

Продолжаем тыкать в это. Может быть, мы сможем найти более полные примеры использования.

@erwbgy , вариант выглядит лучше всего, но я не уверен, что выплата так велика.

  • Каковы значения, возвращаемые без ошибок? Всегда ли они нулевые? Если для именованного возврата _было_ ранее назначенное значение, отменяется ли оно? Если указанное значение используется для хранения результата функции, вызвавшей ошибку, возвращается ли оно?
  • Можно ли применить оператор ? к значению, не содержащему ошибки? Могу я сделать что-то вроде (!ok)? или ok!? (что немного странно, потому что вы объединяете назначение и операцию)? Или этот синтаксис подходит только для error ?

@rodcorsi , это меня ReadDir а ReadBuildTargetDirectoryForFileInfo или чем-то вроде этого глупого. Или, возможно, у вас есть большое количество аргументов. Обработка ошибок для preparePath также будет удалена за пределы экрана. На устройстве с ограниченным размером экрана по горизонтали (или не такой широкой областью просмотра, как Github) вы, вероятно, потеряете часть =? . У нас очень хорошо получается вертикальная прокрутка; не так много по горизонтали.

@gladkikhartem , похоже, он привязан к какому-то (только последнему?) аргументу, реализующему интерфейс error . Это очень похоже на объявление функции, и это просто ... странно. Есть ли способ привязать его к возвращаемому значению в стиле ok ? В целом вы покупаете только 1 линию.

@ object88
перенос слов решает действительно серьезные проблемы с кодом. он не используется широко?

@ object88 относительно очень длинного вызова функции. Разберемся здесь с главным вопросом. Проблема не в том, что обработка ошибок вытесняется за пределы экрана. Проблема заключается в длинном имени функции и / или большом списке аргументов. Это необходимо исправить, прежде чем можно будет привести какие-либо аргументы в пользу того, что обработка ошибок не выполняется.

Я еще не видел IDE или удобного для кода текстового редактора, который по умолчанию был настроен на перенос слов. И я вообще не нашел способа сделать это с Github, кроме ручного взлома CSS после загрузки страницы.

И я действительно думаю, что ширина кода является важным фактором - она ​​говорит о "читабельности", которая является стимулом для этого предложения. Утверждают, что вокруг ошибок «слишком много кода». Не то, чтобы функциональности там нет или что ошибки нужно реализовывать каким-то другим способом, но то, что код плохо читается.

@ object88
да, этот код будет работать для любой функции, возвращающей интерфейс ошибки в качестве последнего параметра.

Что касается сохранения строк, вы просто не можете поместить больше информации в меньшее количество строк. Код должен быть равномерно распределен, не слишком плотным и не иметь места после каждого оператора.
Я согласен, это похоже на объявление функции, но в то же время очень похоже на существующее if ...; err! = nil {утверждение, чтобы люди не слишком запутались.

Ширина кода - важный фактор. Что, если у меня есть 80-строчный редактор и 80 строк кода - это вызов функции, а после этого у меня есть ||

Для полноты картины я приведу пример с синтаксисом || , моим автоматическим переносом ошибок и автоматическим обнулением возвращаемых значений, не связанных с ошибками.

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
}

Относительно вашего вопроса о других возвращаемых значениях. В случае ошибки они всегда будут иметь нулевое значение. Я уже объяснил, почему считаю это важным.

Проблема в том, что ваш пример не так уж и важен. Но он все же показывает, что представляет собой это предложение, по крайней мере, для меня. Я хочу, чтобы он решил эту самую распространенную и широко используемую идиому.

err := func()
if err != nil {
    return err
}

Мы все можем согласиться с тем, что такой код - большая часть (если не самая большая) обработки ошибок в целом. Так что решение этого дела логично. И если вы хотите сделать что-то еще, связанное с ошибкой, примените некоторую логику - дерзайте. Вот где должно быть многословие, там, где программист может прочитать и понять реальную логику. Чего нам не нужно, так это тратить время и место на чтение бессмысленных шаблонов. Это бессмысленная, но все же важная часть кода Go.

По поводу ранее разговоров о неявном возврате нулевых значений. Если вам нужно вернуть значимое значение при ошибке - дерзайте. Опять же, многословие здесь хорошо и помогает понимать код. Нет ничего плохого в том, чтобы отказаться от синтаксического сахара, если вам нужно сделать что-то более сложное. И || достаточно гибок, чтобы решить оба случая. Вы можете не указывать значения, не содержащие ошибок, и они будут неявно обнулены. Или вы можете указать их явно, если вам нужно. Я помню, что для этого есть даже отдельное предложение, которое также включает случаи, когда вы хотите вернуть ошибку и обнулить все остальное.

@ object88

Утверждают, что вокруг ошибок «слишком много кода».

Это не просто код. Основная проблема в том, что вокруг ошибок слишком много бессмысленных шаблонов и очень распространенный случай обработки ошибок. Многословие важно, когда есть что-то ценное для чтения. В if err == nil then return err нет ничего ценного, кроме того, что вы хотите повторно выдать ошибку. Для такой примитивной логики требуется много места. И чем больше у вас есть логика, вызовы библиотек, оболочки и т. Д., Которые вполне могут вернуть ошибку, тем больше этот шаблон начинает доминировать над важными вещами - фактической логикой вашего кода. И эта логика может фактически содержать некоторую важную логику обработки ошибок. Но он теряется в этой повторяющейся природе большинства шаблонов вокруг него. И это можно решить, и другие современные языки, с которыми конкурирует Go, пытаются решить эту проблему. Поскольку обработка ошибок так важна, это не просто обычный код.

@creker
Я согласен с тем, что если err != nil return err слишком много шаблонов, мы опасаемся, что если мы создадим простой способ просто пересылать ошибку вверх по стеку - статистические программисты, особенно юниоры, будут использовать самый простой метод, а не делать что уместно в определенной ситуации.
То же самое и с обработкой ошибок Go - она ​​заставляет вас делать достойные вещи.
Таким образом, в этом предложении мы хотим побудить других вдумчиво обрабатывать ошибки и устранять их.

Я бы сказал, что мы должны сделать простую обработку ошибок некрасивой и долгой для реализации, но изящная обработка ошибок с оберткой или трассировкой стека выглядит красиво и легко.

@gladkikhartem Мне всегда

@gladkikhartem Go уже имеет эту проблему, и я не думаю, что мы можем что-то с этим поделать. Разработчики всегда будут лениться, пока вы не заставите их с ошибкой компиляции или паникой во время выполнения, чего, кстати, Go не делает.

На самом деле Go ничего не делает. Представление о том, что Go заставляет вас обрабатывать ошибки, вводит в заблуждение и всегда имело место. Авторы Go заставляют вас делать это в своих сообщениях в блогах и на конференциях. Фактический язык позволяет вам делать все, что вы хотите. И главная проблема здесь в том, что Go выбирает по умолчанию - по умолчанию ошибка молча игнорируется. Есть даже предложение изменить это. Если Go заставлял вас делать что-то приличное, то он должен был сделать следующее. Либо верните ошибку во время компиляции, либо паникуйте во время выполнения, если возвращенная ошибка не обрабатывается должным образом. Насколько я понимаю, это делает Rust - по умолчанию возникает паника. Это то, что я призываю к принуждению к правильным поступкам.

Что на самом деле заставляло меня обрабатывать ошибки в Go, так это совесть разработчика, и ничего больше. Но всегда соблазнительно уступить. Прямо сейчас, если я явно не буду читать сигнатуру функции, никто мне ничего не скажет, что она возвращает ошибку. Вот реальный пример. Долгое время я понятия не имел, что fmt.Println возвращает ошибку. Мне не нужно его возвращаемое значение, я просто хочу что-то напечатать. Так что у меня нет стимула смотреть на то, что он возвращает. Это та же проблема, что и у C. Ошибки - это значения, и вы можете игнорировать их сколько угодно, пока ваш код не сломается во время выполнения, и вы ничего об этом не узнаете, потому что не будет сбоев с полезной паникой, например, с необработанными исключениями.

@gladkikhartem из того, что я понимаю об этом предложении, это не для того, чтобы побудить разработчиков вдумчиво оборачивать ошибки. Речь идет о том, чтобы побудить тех, кто будет выступать с предложениями, не забыть об этом. Потому что часто люди придумывают решения, которые просто повторяют ошибку и забывают, что вы на самом деле хотите придать ей больше контекста, и только затем повторно ее.

Я пишу это предложение в основном для того, чтобы побудить людей, которые хотят упростить обработку ошибок Go, подумать о способах упростить перенос контекста вокруг ошибок, а не просто вернуть ошибку без изменений.

@creker
Мой редактор имеет ширину 100 символов, потому что у меня есть файловый менеджер, консоль git и т. Д., В нашей команде никто не пишет код длиной более 100 символов, это просто глупо (за некоторыми исключениями)

Go не требует обработки ошибок, в отличие от линтеров. (Может, для этого надо просто линтер написать?)

Хорошо, если мы не можем придумать решение и каждый понимает предложение по-своему - почему бы не указать какие-то требования к тому, что нам нужно? сначала согласовать требования, а затем разработать решение было бы намного проще.

Например:

  1. Синтаксис нового предложения должен содержать в тексте оператор return , иначе читателю будет неочевидно, что происходит. ( согласен не согласен )
  2. Новое предложение должно поддерживать функции, возвращающие несколько значений (согласен / не согласен)
  3. Новое предложение должно занимать меньше места (1 строка, 2 строки, не согласен)
  4. Новое предложение должно поддерживать очень длинные выражения (согласен / не согласен)
  5. Новое предложение должно допускать несколько утверждений в случае ошибки (согласен / не согласен)
  6. .....

@creker , около 75% моих разработок выполняется на 15-дюймовом ноутбуке в VSCode. Я максимально увеличиваю свою горизонтальную площадь, но все же есть предел, особенно если я занимаюсь редактированием бок о бок. Я бы поспорил, что среди студентов , ноутбуков гораздо больше, чем настольных.Я не хотел бы ограничивать доступность языка, потому что мы ожидаем, что у всех будут широкоформатные мониторы.

И, к сожалению, независимо от того, насколько большой у вас экран, github по-прежнему ограничивает область просмотра.

@gladkikhartem

Здесь применима лень новичка, но либеральное использование errors.New в некоторых из этих примеров также демонстрирует непонимание языка. Ошибки не должны выделяться в возвращаемых значениях, если они не являются динамическими, и если эти ошибки будут помещены в сопоставимую переменную области действия пакета, синтаксис будет короче на странице и фактически приемлем и в производственном коде. Те, кто больше всего страдает от "шаблонной" обработки ошибок Go, берут наибольшее количество ярлыков и не имеют достаточного опыта для правильной обработки ошибок.

Не ясно, что составляет simplifying error handling , но прецедент состоит в том, что less runes != simple . Я думаю, что есть несколько определителей простоты, которые могут измерить конструкцию количественно измеримым способом:

  • Количество способов выражения конструирования
  • Сходство этой конструкции с другой конструкцией и взаимосвязь между этими конструкциями.
  • Количество логических операций, суммируемых конструкцией
  • Сходство конструкции с естественным языком (т. Е. Отсутствие отрицания и т. Д.)

Например, исходное предложение увеличивает количество способов распространения ошибок с 2 до 3. Оно похоже на логическое ИЛИ, но имеет другую семантику. Он суммирует условный возврат низкой сложности (по сравнению с, скажем, copy или append , или >> ). Новый метод менее естественен, чем старый, и, если его произнести вслух, вероятно, будет abs, err := path(foo) || return err -> if theres an error, it's returning err в этом случае будет загадкой, почему можно использовать вертикальные полосы, если вы можно написать так же, как это было сказано вслух при проверке кода.

@в виде
Полностью согласен с тем, что less runes != simple .
Под простым я подразумеваю читабельный и понятный.
Так что любой, кто не знаком с go, должен прочитать его и понять, что он делает.
Это должно быть похоже на шутку - вам не нужно это объяснять.

Текущая обработка ошибок на самом деле понятна, но не полностью читаема, если у вас слишком много if err != nil return.

@ object88 это нормально. Я сказал больше в общих чертах, потому что этот аргумент возникает довольно часто. Например, давайте представим какой-нибудь нелепый древний экран терминала, который можно было бы использовать для написания Go. Что это за аргумент? Где предел его нелепости? Если мы серьезно относимся к этому, то мы должны изучить неопровержимые факты - какой размер и разрешение экрана наиболее популярны. И только из этого мы можем что-то нарисовать. Но аргумент обычно просто представляет собой какой-то размер экрана, который никто не использует, но есть небольшая вероятность, что кто-то сможет.

@gladkikhartem нет,

Я согласен, нам следует лучше сформулировать то, что мы хотим, потому что предложение не полностью охватывает все аспекты.

@в виде

Новый метод менее естественен, чем старый, и, если бы его произносили вслух, вероятно, он выглядел бы как abs, err: = path (foo) || return err -> если есть ошибка, он возвращает err, и в этом случае было бы загадкой, почему можно использовать вертикальные полосы, если вы можете написать это так же, как это было сказано вслух при проверке кода.

Новый метод менее естественен только по одной причине - сейчас он не является частью языка. Другой причины нет. Представьте себе Go уже с этим синтаксисом - это было бы естественно, потому что вы с ним знакомы. Так же, как вы знакомы с -> , select , go и другими вещами, отсутствующими в других языках. Почему вместо возврата можно использовать вертикальные полосы? Отвечаю вопросом. Почему есть способ добавить фрагменты за один вызов, если то же самое можно сделать с помощью цикла? Почему есть способ скопировать данные из интерфейса читателя в интерфейс писателя за один вызов, если вы можете сделать то же самое с циклом? и т. д. и т. д. Потому что вы хотите, чтобы ваш код был более компактным и более читаемым. Вы приводите эти аргументы, когда Go уже опровергает их многочисленными примерами. Опять же, давайте будем более открытыми и не будем ничего сбивать с толку только потому, что это новое и еще не написано на языке. Этим мы ничего не добьемся. Есть проблема, многие требуют решения, давайте разберемся с ней. Го - это не священный идеальный язык, который будет осквернен чем-либо, что к нему добавят.

Почему есть способ добавить фрагменты за один вызов, если то же самое можно сделать с помощью цикла?

Написание проверки ошибок оператора if тривиально, мне было бы интересно увидеть вашу реализацию append .

Почему есть способ скопировать данные из интерфейса читателя в интерфейс писателя за один вызов, если вы можете сделать то же самое с циклом?

Читатель и писатель абстрагируются от источников и адресатов операции копирования, стратегии буферизации и иногда даже контрольных значений в цикле. Вы не можете выразить эту абстракцию с помощью цикла и среза.

Вы приводите эти аргументы, когда Go уже опровергает их многочисленными примерами.

Я не верю, что это так, по крайней мере, не в этих примерах.

Опять же, давайте будем более открытыми и не будем ничего сбивать с толку только потому, что это новое и еще не написано на языке.

Учитывая, что у Go есть гарантия совместимости, вам следует внимательно изучать новые функции, поскольку вам придется иметь дело с ними вечно, если они ужасны. То, что здесь пока никто не сделал, является фактическим доказательством концепции и используется небольшой командой разработчиков.

Если вы посмотрите на историю некоторых предложений (например, дженериков), вы увидите, что после того, как вы сделали именно это, часто приходит осознание: «Вау, это на самом деле не очень хорошее решение, давайте пока не будем вносить никаких изменений». Альтернатива - это язык, полный предложений и непростой способ их задним числом исключить.

Что касается широких и тонких экранов, стоит обратить внимание на многозадачность .

У вас может быть несколько окон бок о бок, чтобы время от времени отслеживать что-то еще, пока вы набираете немного кода, а не просто смотрите на редактор, полностью переключая контексты в другое окно для поиска функции, например, StackOverflow, и вернитесь в редактор.

@в виде
Полностью согласен с тем, что большинство предлагаемых функций непрактично, и я начинаю думать, что || и ? всякое могло быть дело.

@creker
copy () и append () - нетривиальные задачи для реализации

У меня есть линтеры на CI / CD, и они буквально заставляют меня обрабатывать все ошибки. Они не являются частью языка, но им все равно - мне просто нужны результаты.
(и, кстати, у меня твердое мнение - если кто-то не использует линтеры в Go - он просто ........)

Насчет размера экрана - это даже не смешно, серьезно. Пожалуйста, прекратите это неуместное обсуждение. Ваш экран может быть сколь угодно широким - у вас всегда будет вероятность того, что часть кода || return &PathError{Err:err} не будет видна. Просто погуглите слово "ide" и посмотрите, какое пространство доступно для кода.

И, пожалуйста, внимательно прочтите чужой текст, я не говорил, что Go заставляет вас обрабатывать все ошибки

То же самое и с обработкой ошибок Go - она ​​заставляет вас делать достойные вещи.

@gladkikhartem Go ничего не заставляет с точки зрения обработки ошибок, вот в чем проблема. Прилично это или нет, не важно, это придирки. Хотя для меня это означает обрабатывать все ошибки во всех случаях, кроме, может быть, таких вещей, как fmt.Println .

если кто-то не использует линтер в Go - он просто

Может быть. Но если что-то на самом деле не принудительно, то это не сработает. Некоторые будут использовать это, другие нет.

Насчет размера экрана - это даже не смешно, серьезно. Пожалуйста, прекратите это неуместное обсуждение.

Я не тот, кто начал выбрасывать случайные числа, которые должны как-то влиять на принятие решений. Я четко заявляю, что понимаю проблему, но она должна быть объективной. Не «У меня IDE шириной 80 символов, Go должен учитывать это и игнорировать всех остальных».

Если мы говорим о моем размере экрана. Код Visual Studio дает мне 270 символов горизонтального пространства. Я не собираюсь утверждать, что занимать столько места - это нормально. Но мой код может легко превышать 120 символов, если учесть структуры с комментариями и особенно длинные именованные типы полей. Если бы я использовал синтаксис || то он легко уместился бы в 100-120 в случае вызова функции с 3-5 аргументами и обернутой ошибки с настраиваемым сообщением.

Эфирный путь, если что-то вроде || должно быть реализовано, тогда gofmt, вероятно, не должен заставлять вас писать это в одной строке. В некоторых случаях это может занять слишком много места.

@erwbgy , вариант выглядит лучше всего, но я не уверен, что выплата так велика.

@ object88 Для меня

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

проще:

val, err? := func()

Ничто не мешает сделать более сложную обработку ошибок текущим способом.

Каковы значения, возвращаемые без ошибок? Всегда ли они нулевые? Если для именованного возврата было ранее присвоенное значение, будет ли оно отменено? Если указанное значение используется для хранения результата функции, вызвавшей ошибку, возвращается ли оно?

Все остальные возвращаемые параметры являются подходящими нулевыми значениями. Для именованных параметров я ожидал, что они сохранят любое ранее присвоенное значение, поскольку им уже гарантировано присвоение некоторого значения.

Может? применить оператор к значению, не являющемуся ошибкой? Могу я сделать что-нибудь вроде (! Ок)? или ок !? (что немного странно, потому что вы объединяете назначение и операцию)? Или этот синтаксис подходит только для ошибок?

Нет, я не думаю, что имеет смысл использовать этот синтаксис для чего-либо, кроме значений ошибок.

Я думаю, что «обязательные» функции будут разрастаться из-за отчаяния в поисках более читаемого кода.

sqlx

db.MustExec(schema)

html шаблон

var t = template.Must(template.New("name").Parse("html"))

Я предлагаю панический оператор (не уверен, следует ли называть его «оператором»)

a,  😱 := someFunc(b)

то же самое, но, возможно, более быстрое, чем

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱, вероятно, слишком сложно набирать, мы могли бы использовать что-то вроде!, Или !!, или

a,  !! := someFunc(b)
!! = maybeReturnsError()

Может быть !! паника и! возвращается

Время для моих 2 цента. Почему мы не можем просто использовать стандартную библиотеку debug.PrintStack() для трассировки стека? Идея состоит в том, чтобы распечатать трассировку стека только на самом глубоком уровне, на котором произошла ошибка.

Почему мы не можем просто использовать стандартную библиотеку debug.PrintStack() для трассировки стека?

Ошибки могут передаваться по многим стекам. Их можно пересылать по каналам, сохранять в переменных и т. Д. Часто бывает полезнее знать эти точки перехода, чем знать фрагмент, в котором впервые была сгенерирована ошибка.

Более того, сама трассировка стека часто включает внутренние (неэкспортированные) вспомогательные функции. Это удобно, когда вы пытаетесь отладить неожиданный сбой, но не помогает при ошибках, возникающих в процессе нормальной работы.

Каков наиболее удобный подход для новичков в программировании?

Нашел более простой вариант. Нужен только один if !err
Ничего особенного, интуитивно понятный, без лишних знаков препинания, код значительно меньше

`` идти
absPath, ошибка: = p.preparePath ()
вернуть ноль, ошибиться, если ошибка

err: = doSomethingWith (absPath) если! err
doSomethingElse () если! err

doSomethingRegardlessOfErr ()

// Обработка ошибок в одном месте; если нужно; ловушка без отступов
if err {
вернуть "ошибка без загрязнения кода", err
}
`` ''

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

С возвращением, старые добрые почтовые условия MUMPS ;-)

Спасибо, но нет.

@dmajkic Это не помогает с «возвратом ошибки с дополнительной контекстной информацией».

@erwbgy, эта проблема называется _proposal: Go 2: упростите обработку ошибок с помощью || err suffix_ мой комментарий был в этом контексте. Извините, если я вмешался в предыдущее обсуждение.

@cznic Ага . Пост-условия не подходят, но предварительные условия тоже выглядят загрязненными:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic Предложение - это нечто большее, чем просто заголовок - ianlancetaylor описывает три способа обработки ошибок и особо отмечает, что несколько предложений упрощают возврат ошибки с дополнительной информацией.

@erwbgy Я рассмотрел все проблемы, указанные @ianlancetaylor. Все они try() ) или использованием специальных не буквенно-цифровых символов. Лично мне это не нравится, так как код перегружен! "# $% & Имеет тенденцию выглядеть оскорбительно, как ругань.

Я согласен и чувствую, что говорят первые несколько строк этой проблемы: слишком много кода Go идет на обработку ошибок. Предложение, которое я сделал, согласуется с этим мнением, с предложением, очень близким к тому, что сейчас чувствует Go, без необходимости в дополнительных ключевых словах или ключевых символах.

Как насчет условной отсрочки

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
}

Это существенно изменит значение defer - это просто то, что запускается в конце области видимости, а не то, что вызывает преждевременный выход из области видимости.

Если они представят Try Catch на этом языке, все эти проблемы будут решены очень легко.

Они должны ввести что-то вроде этого. Если для значения error установлено значение, отличное от nil, это может прервать текущий рабочий процесс и автоматически запустить раздел catch, а затем раздел finally и текущие библиотеки могут работать без изменений. Проблема решена!

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 Это лучше, чем ничего, но если они просто будут использовать ту же парадигму try-catch, с которой все знакомы, будет намного лучше.

@KamyarM Похоже, вы предлагаете добавить механизм для

Похоже на Swift, у которого также есть «исключения», которые не совсем работают как исключения.

Различные языки показали, что try catch на самом деле является второстепенным решением, в то время как я предполагаю, что Go не сможет решить это, как с монадой Maybe и так далее.

@ianlancetaylor Я только что сослался на Try-Catch на других языках программирования, таких как C ++, Java, C #, ... а не на решение, которое у меня было здесь. Было бы лучше, если бы у GoLang был Try-Catch с первого дня, поэтому нам не нужно было иметь дело с этим способом обработки ошибок (который на самом деле не был новым. Вы можете написать такую ​​же обработку ошибок GoLang с любым другим языком программирования, если хотите. кодировать подобным образом), но я предлагаю способ обратной совместимости с текущими библиотеками, которые могут возвращать объект ошибки.

Исключения Java - это крушение поезда, поэтому я категорически не согласен с вами здесь, @KamyarM. То, что что-то знакомо, не означает, что это хороший выбор.

Что я имею в виду.

@KamyarM Спасибо за разъяснения. Мы явно рассматривали и отклоняли исключения. Ошибки не исключительны; они случаются по разным совершенно нормальным причинам. https://blog.golang.org/errors-are-values

Исключительно или нет, но они решают проблему раздувания кода из-за шаблона обработки ошибок. Та же проблема повредила Objective-C, который работает почти так же, как Go. Ошибки - это просто значения типа NSError, ничего особенного в них нет. И у него та же проблема с множеством ifs и переносом ошибок. Вот почему Swift все изменил. В итоге получилось сочетание двух - он работает как исключения, что означает, что он завершает выполнение, и вы должны перехватить исключение. Но он не раскручивает стек и работает как обычный возврат. Таким образом, технический аргумент против использования исключений для потока управления здесь неприменим - эти «исключения» выполняются так же быстро, как и обычный возврат. Это скорее синтаксический сахар. Но у Swift с ними уникальная проблема. Многие API-интерфейсы Cocoa являются асинхронными (обратные вызовы и GCD) и просто несовместимы с такой обработкой ошибок - исключения бесполезны без чего-то вроде await. Но почти весь код Go является синхронным, и эти «исключения» действительно могут работать.

@urandom
Исключения в Java - это неплохо. Проблема в плохих программистах, которые не знают, как им пользоваться.

Если в вашем языке есть ужасные функции, кто-то в конечном итоге воспользуется этой функцией. Если в вашем языке нет такой функции, вероятность 0%. Это простая математика.

@as Я не согласен с вами, что try-catch - ужасная особенность. Это очень полезная функция, которая делает нашу жизнь намного проще, и поэтому мы здесь комментируем, так что, возможно, команда Google GoLang добавляет аналогичную функциональность. Я лично ненавижу эти коды обработки ошибок if-elses в GoLang, и мне не очень нравится концепция defer-panic-recovery (она похожа на try-catch, но не так организована, как с блоками Try-Catch-finally) . Он добавляет в код столько шума, что во многих случаях код становится нечитаемым.

Функциональность для обработки ошибок без шаблонов уже существует в языке. Добавление дополнительных функций, чтобы насытить новичков, использующих языки, основанные на исключениях, не кажется хорошей идеей.

А как насчет того, кто пришел из C / C ++, Objective-C, где у нас точно такая же проблема с шаблоном? И неприятно видеть, что современный язык вроде Go страдает точно такими же проблемами. Вот почему вся эта шумиха вокруг ошибок как ценностей кажется такой фальшивой и глупой - это уже делается годами, десятками лет. Такое ощущение, что Го ничему не научился из этого опыта. Особенно глядя на Swift / Rust, который на самом деле пытается найти лучший способ. Согласитесь с существующим решением, таким как Java / C #, с исключениями, но, по крайней мере, это гораздо более старые языки.

@KamyarM Вы когда-нибудь пользовались железнодорожным программированием? Луч?

Вы бы не стали так хвалить исключения, если бы использовали их, imho.

@ShalokShalom Ничего особенного. Но разве это не государственная машина? В случае неудачи сделать это, а в случае успеха - то? Что ж, я думаю, что не все типы ошибок должны обрабатываться как исключения. Когда требуется только проверка вводимых пользователем данных, можно просто вернуть логическое значение с подробной информацией об ошибках проверки. Исключения должны быть ограничены вводом-выводом или доступом к сети или неправильными входами функций, и в случае, если ошибка действительно критична, и вы хотите любой ценой остановить успешный путь выполнения.

Одна из причин, по которой некоторые люди говорят, что Try-Catch не очень хороша, - это его производительность. Вероятно, это вызвано использованием таблицы карты обработчиков для каждого места, где может возникнуть исключение. Я где-то читал, что даже исключения быстрее (нулевая стоимость, когда исключения не возникают, но имеют гораздо большую стоимость, когда они на самом деле происходят) по сравнению с проверкой If Error (она всегда проверяется независимо от наличия ошибки). Кроме этого, я не думаю, что есть какие-либо проблемы с синтаксисом Try-Catch. Отличается только способ его реализации компилятором, а не его синтаксис.

@ShalokShalom Пожалуйста, проверьте это:
https://mortoray.com/2013/09/12/the-true-cost-of-zero-cost-exceptions/

Люди, пришедшие из C / C ++, хвалят Go за то, что он НЕ имеет исключений и
за мудрый выбор, противодействие тем, кто утверждает, что он «современный» и
спасибо богу за читаемый рабочий процесс (особенно после C ++).

Вт, 17 апр 2018 в 03:46, Антоненко Артем [email protected]
написал:

А как насчет того, кто пришел из C / C ++, Objective-C, где у нас такой же
точная проблема с шаблоном? И неприятно видеть современный язык
вроде Go страдают точно такими же проблемами. Вот почему вся эта шумиха
вокруг ошибок, поскольку значения кажутся фальшивыми и глупыми - это уже было сделано
годами, десятками лет. Такое ощущение, что Го ничему не научился
опыт. Особенно глядя на Swift / Rust, который на самом деле пытается найти
лучший способ. Согласитесь с существующим решением, таким как Java / C #, урегулированным с
исключения, но, по крайней мере, это гораздо более старые языки.

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/21161#issuecomment-381793840 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

@kirillx Я никогда не говорил, что хочу исключения, как в C ++. Пожалуйста, прочтите мой комментарий еще раз. И какое это имеет отношение к C, где обработка ошибок еще более ужасна? У вас есть не только тонны шаблонов, но и отсутствие отсрочки и множественных возвращаемых значений, что заставляет вас возвращать значения с использованием аргументов указателя и использовать goto для организации логики очистки. Go использует ту же концепцию ошибок, но решает некоторые проблемы с задержкой и множественными возвращаемыми значениями. Но шаблон все еще существует. Другие современные языки также не хотят исключений, но также не хотят ограничиваться стилем C из-за его многословности. Вот почему у нас есть это предложение и такой интерес к этой проблеме.

Люди, выступающие за исключения, должны прочитать эту статью: https://ckwop.me.uk/Why-Exceptions-Suck.html

Причина, по которой исключения стиля Java / C ++ по своей сути плохи, не имеет ничего общего с производительностью конкретных реализаций. Исключения плохи, потому что они являются BASIC'ом "при ошибке goto", когда gotos невидимы в контексте, в котором они могут вступить в силу. Исключения скрывают обработку ошибок, о которой можно легко забыть. Проверенные исключения Java должны были решить эту проблему, но на практике этого не произошло, потому что люди просто улавливали и съедали исключения или сбрасывали трассировки стека повсюду.

Я пишу на Java почти неделю и категорически не хочу видеть в Go исключения в стиле Java, какими бы высокопроизводительными они ни были.

@lpar - это не все циклы for, в то время как циклы, if elses, переключают

В то время как, for и if / else не предполагают, что поток выполнения незаметно прыгает в другое место без маркера, указывающего, что это будет.

Что изменится, если кто-то просто передаст ошибку предыдущему вызывающему абоненту в GoLang, а этот вызывающий абонент просто вернет его предыдущему вызывающему и так далее (кроме большого количества шума кода)? Сколько кодов нам нужно просмотреть и пройти, чтобы узнать, кто будет обрабатывать ошибку? То же самое и с try-catch.

Что может остановить программиста? Иногда функции действительно не нужно обрабатывать ошибку. мы просто хотим передать ошибку в пользовательский интерфейс, чтобы пользователь или системный администратор мог решить ее или найти обходной путь.

Если функция не хочет обрабатывать исключение, она просто не использует блок try-catch, чтобы предыдущий вызывающий мог его обработать. Я не думаю, что с синтаксисом есть проблемы. Кроме того, он намного чище. Однако производительность и способ ее реализации на языке различаются.

Как вы можете видеть ниже, нам нужно добавить 4 строки кода, чтобы не обрабатывать ошибку:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

Если вы хотите передать ошибки вызывающей стороне для обработки, это нормально. Дело в том, что это видно в тот момент, когда вам вернули ошибку.

Учитывать:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

Глядя на код, вы знаете, что при вызове функции может возникнуть ошибка. Вы знаете, что в случае возникновения ошибки поток кода завершится в точке A. Вы знаете, что единственным другим местом потока кода будет точка B. Также существует неявный контракт, согласно которому, если err равно nil, x является некоторым допустимым значением, ноль или иначе.

В отличие от Java:

x = SomeFunc(100, 4)

Глядя на код, вы не представляете, может ли возникнуть ошибка при вызове функции. Если возникает ошибка и выражается как исключение, тогда происходит goto , и вы можете оказаться где-то внизу окружающего кода ... или, если исключение не обнаружено, вы можете закончить где-то внизу совершенно другого фрагмента вашего кода. Или вы можете попасть в чужой код. Фактически, поскольку обработчик исключений по умолчанию можно заменить, вы потенциально можете оказаться буквально где угодно, в зависимости от того, что было сделано другим кодом.

Кроме того, не существует неявного контракта о том, что x действителен - функции обычно возвращают значение null, чтобы указать на ошибки или отсутствующие значения.

В Java эти проблемы могут возникать при каждом вызове - не только из-за плохого кода, это то, о чем вам нужно беспокоиться при _все_ коде Java. Вот почему в средах разработки Java есть всплывающие подсказки, которые показывают, может ли функция, на которую вы указываете, вызвать исключение, и какие исключения она может вызвать. Вот почему Java добавила проверенные исключения, так что для распространенных ошибок у вас должно быть хотя бы какое-то предупреждение о том, что вызов функции может вызвать исключение и отклонить поток программы. Между тем, возвращенные значения NULL и непроверенный характер NullPointerException представляют собой такую ​​проблему, что они добавили класс Optional в Java 8, чтобы попытаться улучшить его, даже несмотря на то, что стоимость явно оборачивается возвращаемое значение для каждой отдельной функции, возвращающей объект.

Мой опыт показывает, что NullPointerException из неожиданного нулевого значения, которое мне передали, - это единственный наиболее распространенный способ, по которому мой код Java завершается сбоем, и я обычно получаю большую обратную трассировку, которая почти полностью бесполезна, и сообщение об ошибке, которое не указывает причину, потому что оно было сгенерировано далеко от кода ошибки. В Go я, честно говоря, не обнаружил, что паника при нулевом разыменовании является серьезной проблемой, хотя у меня гораздо меньше опыта работы с Go. Для меня это означает, что Java должна учиться у Go, а не наоборот.

Я не думаю, что с синтаксисом есть проблемы.

Я не думаю, что кто-то говорит, что синтаксис - это проблема исключений в стиле Java.

@lpar , почему паника нулевого разыменования в Go лучше, чем NullPointerException в Java? В чем разница между «Паникой» и «Бросить»? В чем разница в их семантике?

Паники могут быть восстановлены, а броски зафиксированы? Правильно?

Я только что вспомнил одно отличие: с помощью panic вы можете вызвать панику объекта ошибки или строкового объекта или может быть любым другим типом объектов (поправьте меня, если я ошибаюсь), но с помощью throw вы можете выбросить объект типа Exception или подкласс только за исключением.

Почему паника нулевого разыменования в Go лучше, чем NullPointerException в Java?

Потому что первое, по моему опыту, почти никогда не случается, тогда как второе случается постоянно по причинам, которые я объяснил.

@lpar Ну, я не программировал на Java в последнее время, и я думаю, что это новая вещь (последние 5 лет), но у C # есть безопасный оператор навигации, чтобы избежать нулевых ссылок для создания исключений, но что есть в Go? Я не уверен, но полагаю, что у него нет ничего, чтобы справиться с такими ситуациями. Поэтому, если вы хотите избежать паники, вам все равно нужно добавить в код эти уродливые вложенные операторы if-not-nil-else.

Обычно вам не нужно проверять возвращаемые значения, чтобы убедиться, что они равны нулю в Go, если вы проверяете возвращаемое значение ошибки. Так что никаких уродливых вложенных операторов if.

Нулевое разыменование было плохим примером. Если вы не поймаете это, Go и Java работают точно так же - вы получите сбой с трассировкой стека. Как может трассировка стека быть бесполезной, я сейчас не знаю. Вы знаете точное место, где это произошло. Как в C #, так и в Go для меня обычно тривиально исправить такой сбой, потому что, по моему опыту, разыменование нуля происходит из-за простой ошибки программиста. В данном конкретном случае ничему не нужно учиться.

@lpar

Потому что первое, по моему опыту, почти никогда не случается, тогда как второе случается постоянно по причинам, которые я объяснил.

Это случайно, и я не видел в вашем комментарии причины, по которой Java как-то хуже при nil / null, чем Go. Я наблюдал многочисленные сбои при разыменовании нуля в коде Go. Они точно такие же, как нулевое разыменование в C # / Java. Возможно, вы используете больше типов значений в Go, что помогает (они также есть в C #), но ничего не меняет.

Что касается исключений, давайте посмотрим на Swift. У вас есть ключевое слово throws для функций, которые могут вызывать ошибку. Функцию без него бросить не получится. С точки зрения реализации он работает как return - вероятно, какой-то регистр зарезервирован для возврата ошибки, и каждый раз, когда вы бросаете, функция возвращается нормально, но несет с собой значение ошибки. Так что проблема неожиданных ошибок решена. Вы точно знаете, какая функция может сработать, вы знаете точное место, где это могло произойти. Ошибки являются значениями и не требуют раскрутки стека. Их просто возвращают, пока вы не поймаете.

Или что-то похожее на Rust, где у вас есть специальный тип Result, который несет результат и ошибку. Ошибки могут распространяться без каких-либо явных условных операторов. Плюс тонна совершенства сопоставления с образцом, но, вероятно, это не для Go.

Оба этих языка берут оба решения (C и Java) и объединяют их во что-то лучшее. Распространение ошибок из исключений + значения ошибок и очевидный поток кода из C + без уродливого шаблонного кода, который не делает ничего полезного. Поэтому я думаю, что будет разумным взглянуть на эти конкретные реализации, а не отказываться от них полностью только потому, что они чем-то напоминают исключения. Есть причина, по которой исключения используются во многих языках, потому что они имеют положительную сторону. В противном случае языки игнорировали бы их. Особенно после C ++.

Как может трассировка стека быть бесполезной, я сейчас не знаю.

Я сказал «почти бесполезно». Например, мне нужна только одна строка информации, но это десятки строк.

Это случайно, и я не видел в вашем комментарии причины, по которой Java как-то хуже при nil / null, чем Go.

Значит, ты не слушаешь. Вернитесь и прочтите часть о неявных контрактах.

Ошибки могут распространяться без каких-либо явных условных операторов.

И в этом-то и заключается проблема - распространяются ошибки и изменяется поток управления без каких-либо явных указаний на то, что это произойдет. Видимо, вы не думаете, что это проблема, но другие не согласны.

Страдают ли исключения, реализованные в Rust или Swift, теми же проблемами, что и Java, я не знаю, я оставлю это кому-то, имеющему опыт работы с рассматриваемыми языками.

@KamyarM Вы в основном делаете nil излишним и получаете для него полную безопасность типов:

https://fsharpforfunandprofit.com/posts/the-option-type/

И в этом-то и заключается проблема - распространяются ошибки и изменяется поток управления без каких-либо явных указаний на то, что это произойдет.

Мне это кажется правдой. Если я разрабатываю какой-то пакет, который потребляет другой пакет, и этот пакет генерирует исключение, теперь _I_ также должен знать об этом, независимо от того, хочу ли я использовать эту функцию. Это необычный аспект среди предлагаемых языковых функций; большинство из них программист может выбрать или просто не использовать по своему усмотрению. Исключения по самому своему намерению пересекают всевозможные границы, ожидаемые или нет.

Я сказал «почти бесполезно». Например, мне нужна только одна строка информации, но это десятки строк.

А огромные трассы Go с сотнями горутин как-то полезнее? Я не понимаю, к чему вы клоните. Java и Go здесь абсолютно одинаковы. И иногда вам действительно полезно наблюдать за полным стеком, чтобы понять, как ваш код оказался там, где произошел сбой. Трассировки C # и Go мне в этом неоднократно помогали.

Значит, ты не слушаешь. Вернитесь и прочтите часть о неявных контрактах.

Прочитал, ничего не изменилось. По моему опыту, это не проблема. Для этого нужна документация на обоих языках (например, net.ParseIP ). Если вы забыли проверить, является ли ваше значение nil / null или нет, у вас точно такая же проблема на обоих языках. В большинстве случаев Go вернет ошибку, а C # выдаст исключение, поэтому вам даже не нужно беспокоиться о nil. Хороший API не просто возвращает значение null, не вызывая исключения или чего-то еще, чтобы сообщить, что не так. В других случаях вы проверяете это явно. По моему опыту, наиболее распространенные типы ошибок с нулевым значением - это когда у вас есть буферы протокола, где каждое поле является указателем / объектом, или у вас есть внутренняя логика, где поля класса / структуры могут быть нулевыми в зависимости от внутреннего состояния, и вы забываете проверить его перед доступ. Это наиболее распространенный для меня шаблон, и ничто в Go не решает эту проблему значительно. Я могу назвать две вещи, которые немного помогают - полезные пустые значения и типы значений. Но это больше касается простоты программирования, потому что вам не нужно создавать каждую переменную перед использованием.

И в этом-то и заключается проблема - распространяются ошибки и изменяется поток управления без каких-либо явных указаний на то, что это произойдет. Видимо, вы не думаете, что это проблема, но другие не согласны.

Это проблема, я никогда не говорил иначе, но люди здесь настолько зациклены на исключениях Java / C # / C ++, что игнорируют все, что немного похоже на них. Почему Swift требует, чтобы вы помечали функции с помощью throws чтобы вы могли точно видеть, чего следует ожидать от функции, и где поток управления может нарушиться и в Rust, который вы используете? для явного распространения ошибки с помощью различных вспомогательных методов, чтобы придать ей больше контекста. Они оба используют ту же концепцию ошибок, что и значения, но оборачивают ее синтаксическим сахаром, чтобы уменьшить шаблонность.

А огромные трассы Go с сотнями горутин как-то полезнее?

С помощью Go вы имеете дело с ошибками, регистрируя их вместе с местоположением в точке, где они были обнаружены. Нет обратной трассировки, если вы не добавите ее. Мне нужно было сделать это только один раз.

По моему опыту, это не проблема.

Что ж, мой опыт отличается, и я думаю, что опыт большинства людей отличается, и в качестве доказательства я цитирую тот факт, что в Java 8 были добавлены дополнительные типы.

В этой ветке обсуждались сильные и слабые стороны Go и его системы обработки ошибок, включая обсуждение исключений или нет, я настоятельно рекомендую прочитать это:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

Мои 2 цента на обработку ошибок (извините, если такая идея была упомянута выше).

В большинстве случаев мы хотим избавиться от ошибок. Это приводит к таким фрагментам:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Давайте автоматически повторно выдадим ошибку, отличную от nil, если она не была присвоена переменной (без какого-либо дополнительного синтаксиса). Приведенный выше код станет:

a := fn()
use(a)

// or just

use(fn())

Компилятор сохранит err в неявную (невидимую) переменную, проверит ее на nil и продолжит (если err == nil) или вернет (если err! = Nil) и вернет nil в конце функции, если во время выполнения функции не было ошибок, как обычно, но автоматически и неявно.

Если необходимо обработать err , его необходимо присвоить явной переменной и использовать:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

Ошибка может быть подавлена ​​таким образом:

a, _ := fn()
use(a)

В редких (фантастических) случаях, когда возвращается более одной ошибки, явная обработка ошибок будет обязательной (как сейчас):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

Это и мой аргумент - в большинстве случаев мы хотим повторно генерировать ошибки, обычно это вариант по умолчанию. И, возможно, дайте ему какой-то контекст. За исключением случаев, контекст автоматически добавляется трассировкой стека. С ошибками, как в Go, мы делаем это вручную, добавляя сообщение об ошибке. Почему бы не сделать это проще? И это именно то, что пытаются сделать другие языки, при этом балансируя это с вопросом ясности.

Итак, я согласен с «Давайте автоматически повторно генерируем ошибку, отличную от nil, если она не была назначена переменной (без какого-либо дополнительного синтаксиса)», но последняя часть меня беспокоит. Вот где корень проблемы с исключениями и почему, я думаю, люди категорически против говорить о чем-либо, хоть немного связанном с ними. Они изменяют поток управления без какого-либо дополнительного синтаксиса. Это плохо.

Если вы посмотрите, например, на Swift, этот код не будет компилироваться

func a() throws {}
func b() throws {
  a()
}

a может вызвать ошибку, поэтому вам нужно написать try a() чтобы даже распространить ошибку. Если вы удалите throws из b он не будет компилироваться даже с try a() . Вы должны обработать ошибку внутри b . Это гораздо лучший способ обработки ошибок, который решает как проблему нечеткого потока управления исключениями, так и многословность ошибок Objective-C. Последние в значительной степени похожи на ошибки в Go и на замену Swift. Что мне не нравится, так это вещи try, catch которые тоже использует Swift. Я бы предпочел оставить ошибки как часть возвращаемого значения.

Итак, я бы предложил действительно иметь дополнительный синтаксис. Таким образом, сайт вызова сам по себе говорит о том, что это потенциальное место, где поток управления может измениться. Я бы также предположил, что отказ от написания этого дополнительного синтаксиса приведет к ошибке компиляции. Это, в отличие от того, как сейчас работает Go, заставит вас справиться с ошибкой. Вы можете добавить возможность просто игнорировать ошибку с помощью чего-то вроде _ потому что в некоторых случаях было бы очень сложно обрабатывать каждую маленькую ошибку. Мол, printf . Меня не волнует, если он что-то не регистрирует. В Go уже есть этот надоедливый импорт. Но это, по крайней мере, решается с помощью инструментов.

Есть две альтернативы ошибке времени компиляции, о которых я могу думать прямо сейчас. Как и в случае с Go, пусть ошибка игнорируется. Мне это не нравится, и это всегда было моей проблемой при обработке ошибок Go. Он ничего не заставляет, поведение по умолчанию - молча игнорировать ошибку. Это плохо, так нельзя писать надежные и простые для отладки программы. У меня было слишком много случаев в Objective-C, когда я был ленив или не вовремя и игнорировал ошибку, просто чтобы столкнуться с ошибкой в ​​том же коде, но без какой-либо диагностической информации о том, почему это произошло. По крайней мере, во многих случаях это позволило бы мне решить проблему прямо здесь.

Обратной стороной является то, что люди могут начать игнорировать ошибки, помещать, так сказать, try, catch(...) везде. Это возможно, но в то же время, когда ошибки игнорируются по умолчанию, это сделать еще проще. Я думаю, что аргумент об исключениях здесь не применим. За некоторыми исключениями, некоторые люди пытаются создать иллюзию того, что их программа более стабильна. Проблема в том, что необработанное исключение вызывает сбой программы.

Другой альтернативой была бы паника. Но это просто расстраивает и вызывает воспоминания об исключениях. Это определенно побудит людей заняться «защитным» кодированием, чтобы их программа не вылетела из строя. Для меня современный язык должен делать как можно больше во время компиляции и оставлять как можно меньше решений во время выполнения. Там, где уместна паника, находится наверху стека вызовов. Например, отказ от обработки ошибки в основной функции автоматически вызовет панику. Это также относится к горутинам? Наверное, не стоит.

Зачем искать компромиссы?

@ nick-korsakov исходное предложение (этот выпуск) хочет добавить больше контекста к ошибкам:

Игнорировать ошибку уже легко (возможно, слишком просто) (см. №20803). Многие существующие предложения по обработке ошибок упрощают возврат ошибки без изменений (например, # 16225, # 18721, # 21146, # 21155). Немногие упрощают возврат ошибки с дополнительной информацией.

См. Также этот комментарий .

В этом комментарии я предлагаю, чтобы для достижения прогресса в этом обсуждении (а не зацикливания) мы должны лучше определить цели, например, что является тщательно обработанным сообщением об ошибке. Все это довольно интересно читать, но похоже, что на него влияет трехсекундная проблема с памятью золотой рыбки (не очень сосредоточена / движется вперед, повторяются приятные творческие изменения синтаксиса и аргументы об исключениях / паниках и т. Д.).

Другой велосипедный навес:

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 означает «возврат, если не пустое значение». В этом случае я предполагаю, что errors.Errorf вернет nil если ошибка равна нулю. Я думаю, что это примерно такая большая экономия, на которую мы можем рассчитывать, при этом придерживаясь цели легкой упаковки.

Типы сканера в стандартной библиотеке сохраняют состояние ошибки внутри структуры, методы которой могут ответственно проверять наличие ошибки перед продолжением.

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 }

Используя типы для хранения состояния ошибки, можно избавить код, который использует такой тип, от избыточных проверок ошибок.

Это также не требует творческих и причудливых изменений синтаксиса или неожиданных передач управления на языке.

Я также должен предложить что-то вроде try / catch, где err определяется внутри try {}, а если для err задано значение, отличное от nil, поток прерывается от try {} к блокам обработчика ошибок (если они есть).

Внутренне исключений нет, но все дело должно быть ближе
к синтаксису, который выполняет if err != nil break проверки после каждой строки, в которой может быть назначена ошибка.
Например:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

Я знаю, что он похож на C ++, но он также хорошо известен и чище, чем ручной if err != nil {...} после каждой строки.

@в виде

Тип сканера работает, потому что он выполняет всю работу, поэтому может позволить себе отслеживать собственные ошибки в процессе. Давайте не будем обманывать себя, что это универсальное решение, пожалуйста.

@carlmjohnson

Если нам нужна одна линейная обработка простой ошибки, мы могли бы изменить синтаксис, чтобы оператор return мог быть началом однострочного блока.
Это позволило бы людям писать:

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

Я думаю, что спецификацию нужно изменить на что-то вроде (это может быть довольно наивно :))

Block = "{" StatementList "}" | "return" Expression .

Я не думаю, что специальный возврат к регистру действительно лучше, чем просто изменение gofmt, чтобы упростить if err, проверяет одну строку вместо трех.

@urandom

Ошибка объединения за пределы одного боксового типа, и ее действия не следует поощрять. Для меня это указывает на отсутствие усилий по переносу или добавлению контекста ошибки между ошибками, происходящими из разных несвязанных действий.

Подход со сканером - одна из худших вещей, которые я читал в контексте всей этой мантры «ошибки - ценности»:

  1. Это бесполезно почти во всех случаях использования, требующих большого количества шаблонов обработки ошибок. Функции, вызывающие несколько внешних пакетов, от этого не выиграют.
  2. Это запутанная и незнакомая концепция. Его введение только запутает будущих читателей и сделает ваш код более сложным, чем он должен быть, просто для того, чтобы вы могли обойти недостатки языкового дизайна.
  3. Он скрывает логику и пытается быть похожим на исключения, извлекая из него худшее (сложный поток управления), не получая при этом никаких преимуществ.
  4. В некоторых случаях это приведет к потере вычислительных ресурсов. Каждый звонок будет тратить время на бесполезную проверку ошибок, которая произошла много лет назад.
  5. Он скрывает точное место, где произошла ошибка. Представьте себе случай, когда вы анализируете или сериализуете какой-либо формат файла. У вас будет цепочка вызовов чтения / записи. Представьте, что первый не сработал. Как бы вы узнали, где именно произошла ошибка? Какое поле анализировалось или сериализовалось? «Ошибка ввода-вывода», «тайм-аут» - эти ошибки в данном случае не нужны. Вы можете предоставить контекст для каждого чтения / записи (например, имя поля). Но на этом этапе лучше просто отказаться от всего подхода, поскольку он работает против вас.

В некоторых случаях это приведет к потере вычислительных ресурсов.

Ориентиры? Что такое «вычислительный ресурс»?

Он скрывает точное место, где произошла ошибка.

Нет, это не так, потому что ошибки, отличные от nil, не перезаписываются

Функции, вызывающие несколько внешних пакетов, от этого не выиграют.
Это запутанная и незнакомая концепция
Подход сканера - одна из худших вещей, которые я читал в контексте всей этой «ошибки - это ценности».

У меня такое впечатление, что вы не понимаете подхода. Это логически эквивалентно регулярной проверке ошибок в автономном типе, я предлагаю вам внимательно изучить пример, так что, возможно, это может быть худшее, что вы понимаете, а не просто худшее, что вы _ прочитали_.

Извините, я собираюсь добавить свое собственное предложение в кучу. Я прочитал большую часть из того, что здесь есть, и хотя мне нравятся некоторые из предложений, я чувствую, что они пытаются сделать слишком много. Проблема в шаблоне ошибки. Мое предложение состоит в том, чтобы просто исключить этот шаблон на уровне синтаксиса и оставить в покое способы передачи ошибок.

Предложение

Уменьшите шаблон ошибок, разрешив использование токена _! качестве синтаксического сахара, чтобы вызвать панику при назначении ненулевого значения error

val, err := something.MayError()
if err != nil {
    panic(err)
}

мог стать

val, _! := something.MayError()

и

if err := something.MayError(); err != nil {
    panic(err)
}

мог стать

_! = something.MayError()

Конечно, конкретный символ вызывает споры. Я также рассматривал _^ , _* , @ и другие. Я выбрал _! в качестве фактического предложения, потому что подумал, что он будет наиболее знакомым с первого взгляда.

Синтаксически _! (или выбранный токен) будет символом типа error доступным в той области, в которой он используется. Он начинается как nil , и каждый раз, когда он назначается, выполняется проверка nil . Если установлено ненулевое значение error , запускается паника. Поскольку _! (или, опять же, выбранный токен) не будет синтаксически допустимым идентификатором в go, конфликт имен не будет проблемой. Эта эфирная переменная будет представлена ​​только в тех областях, где она используется, аналогично именованным возвращаемым значениям. Если необходим синтаксически действительный идентификатор, возможно, можно использовать заполнитель, который будет переписан на уникальное имя во время компиляции.

Обоснование

Одна из наиболее частых критических замечаний, которую я вижу в своем адресе, - это многословие обработки ошибок. Ошибки на границах API - это не плохо. Однако необходимость переноса ошибок в границы API может быть проблемой, особенно для глубоко рекурсивных алгоритмов. Чтобы обойти дополнительную многословность, связанную с распространением ошибок в рекурсивном коде, можно использовать паники. Я считаю, что это довольно распространенная техника. Я использовал его в своем собственном коде, и я видел, как он используется в дикой природе, в том числе в парсере go. Иногда вы выполнили проверку в другом месте своей программы и ожидаете, что ошибка будет равна нулю. Если будет получена ошибка, отличная от нуля, это нарушит ваш инвариант. Когда инвариант нарушается, можно паниковать. В сложном коде инициализации иногда имеет смысл превратить ошибки в панику и восстановить их, чтобы вернуть их куда-нибудь с более глубоким знанием контекста. Во всех этих сценариях есть возможность уменьшить количество шаблонов ошибок.

Я понимаю, что философия го - как можно больше избегать паники. Они не являются инструментом для распространения ошибок через границы API. Однако они являются особенностью языка и имеют законные варианты использования, такие как описанные выше. Паника - фантастический способ упростить распространение ошибок в частном коде, а упрощение синтаксиса во многом поможет сделать код более чистым и, возможно, более ясным. Я чувствую, что легче с первого взгляда распознать _! (или @ , или `_ ^ и т. Д.), Чем форму" if-error-panic ". Токен может значительно уменьшить объем кода, который необходимо написать / прочитать, чтобы передать / понять:

  1. могла быть ошибка
  2. если есть ошибка, мы ее не ожидаем
  3. если есть ошибка, вероятно, она обрабатывается по цепочке

Как и в случае с любой синтаксической функцией, существует вероятность злоупотребления. В этом случае у сообщества го уже есть набор лучших практик для борьбы с паникой. Поскольку это синтаксическое дополнение является синтаксическим сахаром для паники, этот набор передовых методов может быть применен к его использованию.

Помимо упрощения допустимых вариантов использования для паники, это также упрощает быстрое создание прототипа. Если у меня есть идея, которую я хочу записать в коде, и я просто хочу, чтобы ошибки приводили к сбою программы, пока я играю с ней, я мог бы использовать это дополнение к синтаксису, а не форму «if-error-panic». Если я могу выражаться меньшим количеством строк на ранних этапах разработки, это позволит мне быстрее воплощать свои идеи в коде. Когда у меня есть полное представление о коде, я возвращаюсь и реорганизую свой код, чтобы возвращать ошибки на соответствующих границах. Я бы не стал оставлять бесплатные паники в производственном коде, но они могут быть мощным инструментом разработки.

Паника - это просто исключения под другим названием, и что мне нравится в Go, так это то, что исключения бывают исключительными. Я не хочу поощрять больше исключений, придавая им синтаксический сахар.

@carlmjohnson Должно быть правдой одно из двух:

  1. Паника - это часть языка с законными вариантами использования, или
  2. Паники не имеют законных вариантов использования, поэтому их следует удалить из языка.

Я подозреваю, что ответ - 1.
Я также не согласен с тем, что «паники - это просто исключения под другим именем». Я думаю, что такое махание руками мешает настоящей дискуссии. Существуют ключевые различия между паникой и исключениями, как видно на большинстве других языков.

Я понимаю, что коленный рефлекс «паника - это плохо», но личные чувства по поводу использования паники не меняют того факта, что паника используется и действительно полезна. Компилятор go использует панику для выхода из глубоко рекурсивных процессов как в синтаксическом анализаторе, так и на этапе проверки типов (в последний раз я смотрел).
Использование их для распространения ошибок через глубоко рекурсивный код кажется не только приемлемым, но и одобренным разработчиками го.

Паника сообщает нечто конкретное:

здесь что-то пошло не так, что здесь не было готово справиться

В коде всегда найдутся места, где это правда. Особенно на ранней стадии разработки. Go был изменен для улучшения опыта рефакторинга раньше: добавлены псевдонимы типов. Возможность распространять нежелательные ошибки с помощью паники до тех пор, пока вы не сможете конкретизировать, если и как их обрабатывать на уровне, более близком к исходному, может сделать написание и прогрессивный рефакторинг кода гораздо менее многословным.

Мне кажется, что большинство предложений здесь предполагают значительные изменения языка. Это самый прозрачный подход, который я мог придумать. Это позволяет всей текущей когнитивной модели обработки ошибок в go оставаться неизменной, обеспечивая при этом сокращение синтаксиса для конкретного, но распространенного случая. В настоящее время передовой опыт диктует, что «код go не должен вызывать панику за границы API». Если у меня есть общедоступные методы в пакете, они должны возвращать ошибки, если что-то пойдет не так, за исключением редких случаев, когда ошибку невозможно исправить (например, нарушения инварианта). Это дополнение к языку не отменяет эту передовую практику. Это просто способ уменьшить количество шаблонов во внутреннем коде и сделать зарисовки более понятными. Это, безусловно, упрощает линейное чтение кода.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

намного читабельнее, чем

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

На самом деле нет принципиальной разницы между паникой в ​​Go и исключением в Java или Python и т. Д., Кроме синтаксиса и отсутствия иерархии объектов (что имеет смысл, потому что Go не имеет наследования). Как они работают и как используются.

Конечно, паника занимает законное место в языке. Паника предназначена для обработки ошибок, которые должны возникать только из-за ошибки программиста, которую иначе нельзя исправить. Например, если вы делите на ноль в целочисленном контексте, возвращаемое значение невозможно, и вы сами виноваты в том, что сначала не проверили ноль, поэтому возникает паника. Точно так же, если вы читаете фрагмент за пределами границ, попробуйте использовать nil в качестве значения и т. Д. Эти вещи вызваны ошибкой программиста, а не ожидаемым состоянием, например, не работает сеть или файл с плохими разрешениями, поэтому они просто паникуют. и взорвать стек. Go предоставляет некоторые вспомогательные функции, которые вызывают панику, как шаблон, потому что ожидается, что они будут использоваться с жестко запрограммированными строками, где любая ошибка должна быть вызвана ошибкой программиста. Нехватка памяти сама по себе не является ошибкой программиста, но она также неисправима и может произойти где угодно, так что это не ошибка, а паника.

Люди также иногда используют панику как способ короткого замыкания стека, но это обычно не одобряется из-за соображений удобочитаемости и производительности, и я не вижу никаких шансов на то, что Go изменится, чтобы стимулировать его использование.

Паники Go и непроверенные исключения Java в значительной степени идентичны и существуют по одним и тем же причинам и для обработки одних и тех же сценариев использования. Не поощряйте людей использовать панику для других случаев, потому что эти случаи имеют те же проблемы, что и исключения на других языках.

Люди также иногда используют панику как способ короткого замыкания стека, но обычно это не одобряется из-за соображений удобочитаемости и производительности.

Прежде всего, это изменение синтаксиса напрямую связано с проблемой удобочитаемости:

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

против

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

Оставляя на данный момент удобочитаемость, другая причина - это производительность.
Да, это правда, что использование операторов panics и defer влечет за собой снижение производительности, но во многих случаях эта разница незначительна для выполняемой операции. Дисковый и сетевой ввод-вывод в среднем занимают гораздо больше времени, чем любая потенциальная магия стека для управления задержками / паниками.

Я часто слышу, как этот момент повторяется при обсуждении паники, и я считаю неискренним сказать, что паника - это снижение производительности. Они, конечно, МОГУТ быть, но не обязательно. Как и многое другое в языке. Если вы паникуете внутри жесткого цикла, когда снижение производительности действительно имеет значение, вам также не следует откладывать выполнение в этом цикле. Фактически, любая функция, которая сама выбирает панику, обычно не должна улавливать панику. Точно так же функция go, написанная сегодня, не будет одновременно возвращать ошибку и панику. Это неясно, глупо и не рекомендуется. Возможно, именно так мы привыкли видеть исключения, используемые в Java, Python, Javascript и т. Д., Но паники обычно используются в коде go иначе, и я не верю, что добавление оператора специально для случая распространения ошибки поднятие стека вызовов с помощью паники изменит способ использования паники. В любом случае они используют панику. Смысл этого расширения синтаксиса состоит в том, чтобы признать тот факт, что разработчики используют panic, и оно имеет совершенно законное использование, и уменьшить шаблонный код вокруг него.

Можете ли вы привести несколько примеров проблемного кода, который, по вашему мнению, может включить эта функция синтаксиса, но которые в настоящее время невозможны / противоречат передовой практике? Если кто-то сообщает об ошибках пользователям своего кода с помощью паники / восстановления, это в настоящее время не одобряется и, очевидно, все равно будет продолжаться, даже если был добавлен такой синтаксис. Если бы вы могли, пожалуйста, ответьте на следующие вопросы:

  1. Какие нарушения, по вашему мнению, могут возникнуть из-за такого расширения синтаксиса?
  2. Что var1, err := trySomeTask1(); if err != nil { panic(err) } означает, что var1, _! := trySomeTask1() - нет? Почему?

Мне кажется, что суть вашего аргумента в том, что «паника - это плохо, и мы не должны ее использовать».
Я не могу распознать и обсудить причины этого, если они не будут известны.

Эти вещи вызваны ошибкой программиста, а не ожидаемыми условиями, такими как отключение сети или файл с плохими разрешениями, поэтому они просто паникуют и взрывают стек.

Мне, как и большинству сусликов, нравится идея ценности ошибок. Я считаю, что это помогает четко сообщить, какие части API гарантируют результат, а какие могут дать сбой, без необходимости просматривать документацию.

Он позволяет собирать ошибки и дополнять их дополнительной информацией. Все это очень важно на границах API, где ваш код пересекается с пользовательским кодом. Однако в рамках этих границ API часто нет необходимости делать все это со своими ошибками. Особенно, если вы ожидаете удачного пути и имеете другой код, отвечающий за обработку ошибки в случае сбоя этого пути.

Бывают случаи, когда обработка ошибок не является задачей вашего кода.
Если я пишу библиотеку, меня не волнует, не работает ли сетевой стек - это вне моего контроля как разработчика библиотеки. Я верну эти ошибки обратно в код пользователя.

Даже в моем собственном коде бывают случаи, когда я пишу фрагмент кода, единственная задача которого - вернуть ошибки родительской функции.

Например, предположим, что у вас есть http.HandlerFunc, читающий файл с диска в качестве ответа - это почти всегда будет работать, и если он не удастся, вероятно, либо программа написана неправильно (ошибка программиста), либо есть проблема с файловой системой вне сферы ответственности программы. Как только http.HandlerFunc паникует, он завершает работу, и какой-то базовый обработчик улавливает эту панику и записывает 500 клиенту. Если в какой-то момент в будущем я захочу обработать эту ошибку по-другому, я могу заменить _! на err и делать все, что захочу, со значением ошибки. Дело в том, что при жизни программы мне это, скорее всего, не понадобится. Если у меня возникают подобные проблемы, значит, обработчик не является частью кода, отвечающей за обработку этой ошибки.

Я могу и обычно пишу if err != nil { panic(err) } или if err != nil { return ..., err } в своих обработчиках для таких вещей, как сбои ввода-вывода, сбои сети и т. Д. Когда мне действительно нужно проверить ошибку, я все равно могу это сделать. Однако в большинстве случаев я просто пишу if err != nil { panic(err) } .

Или другой пример. Если я рекурсивно ищу в дереве (скажем, в реализации http-маршрутизатора), я объявляю функцию func (root *Node) Find(path string) (found Value, err error) . Эта функция отложит функцию для восстановления любых паник, сгенерированных при спуске по дереву. Что, если программа создает неправильные попытки? Что делать, если какой-то ввод-вывод выйдет из строя из-за того, что программа не запущена от имени пользователя с правильными разрешениями? Эти проблемы не являются проблемой моего алгоритма поиска по дереву (если я явно не сделаю это позже), но это возможные ошибки, с которыми я могу столкнуться. Возврат их на всем пути вверх по стеку приводит к большому количеству дополнительных деталей, включая сохранение в стеке того, что в идеале будет несколькими нулевыми значениями ошибок. Вместо этого я могу вызвать панику с ошибкой до этой общедоступной функции API и вернуть ее пользователю. На данный момент это все еще требует лишнего многословия, но это не обязательно.

В других предложениях обсуждается, как рассматривать одно возвращаемое значение как особенное. По сути, это та же мысль, но вместо использования функций, уже встроенных в язык, они стремятся изменить поведение языка в определенных случаях. С точки зрения простоты реализации этот тип предложения (синтаксический сахар для чего-то уже поддерживаемого) будет самым простым.

Отредактируйте, чтобы добавить:
Я не женат на предложении, которое я сделал в том виде, в каком оно написано, но я действительно считаю, что важно взглянуть на проблему обработки ошибок под новым углом. Никто не предлагает ничего подобного, и я хочу посмотреть, сможем ли мы переосмыслить наше понимание проблемы. Проблема в том, что существует слишком много мест, где ошибки обрабатываются явным образом, когда в этом нет необходимости, и разработчики хотели бы иметь способ распространять их вверх по стеку без лишнего шаблонного кода. Оказывается, в Go уже есть такая функция, но для нее нет хорошего синтаксиса. Это обсуждение обертывания существующих функций в менее подробный синтаксис, чтобы сделать язык более эргономичным без изменения поведения. Разве это не победа, если мы сможем ее достичь?

@mccolljr Спасибо, но одна из целей этого предложения - побудить людей разработать новые способы обработки всех трех случаев обработки ошибок: игнорировать ошибку, вернуть ошибку без изменений, вернуть ошибку с дополнительной контекстной информацией. Ваше паническое предложение не касается третьего случая. Это важный вопрос.

@mccolljr Я думаю, что границы API встречаются гораздо чаще, чем вы думаете. Я не считаю вызовы внутри API обычным делом. Во всяком случае, могло быть и наоборот (здесь были бы интересны некоторые данные). Поэтому я не уверен, что разработка специального синтаксиса для вызовов внутри API - правильное направление. Кроме того, использование ошибок return ed вместо ошибок panic ed в API обычно является хорошим способом (особенно если мы разработаем план решения этой проблемы). Ошибки panic ed имеют свое применение, но ситуации, когда они явно лучше, кажутся редкими.

Я не верю, что добавление оператора специально для случая распространения ошибки вверх по стеку вызовов через панику изменит способ использования паники.

Думаю, вы ошибаетесь. Люди обратятся к вашему сокращенному оператору, потому что это так удобно, и тогда они будут использовать панику гораздо чаще, чем раньше.

Независимо от того, полезны ли паники иногда или редко, и полезны ли они в пределах границ API или внутри них, - это отвлекающий маневр. Есть много действий, которые можно предпринять при ошибке. Мы ищем способ сократить код обработки ошибок, не отдавая предпочтение одному действию над другими.

но во многих случаях эта разница незначительна для выполняемой операции

Хотя это правда, я думаю, что это опасный путь. Сначала это выглядело незначительным, но в конечном итоге, когда было уже поздно, возникли узкие места. Я считаю, что мы должны с самого начала помнить о производительности и пытаться найти лучшие решения. В уже упомянутых Swift и Rust есть распространение ошибок, но они реализованы как, по сути, простые возвраты, завернутые в синтаксический сахар. Да, легко повторно использовать существующее решение, но я бы предпочел оставить все как есть, чем упрощать и поощрять людей использовать панику, скрытую за незнакомым синтаксическим сахаром, который пытается скрыть тот факт, что это, в основном, исключения.

Сначала это выглядело незначительным, но в конечном итоге, когда было уже поздно, возникли узкие места.

Нет, спасибо. Мнимые узкие места производительности - это геометрически незначительные узкие места.

Нет, спасибо. Мнимые узкие места производительности - это геометрически незначительные узкие места.

Пожалуйста, оставьте свои личные переживания вне этой темы. У вас, очевидно, есть какие-то проблемы со мной, и вы не хотите приносить что-либо полезное, поэтому просто игнорируйте мои комментарии и оставьте голос против, как вы делали практически с каждым комментарием раньше. Нет нужды публиковать эти бессмысленные ответы.

У меня нет проблем с вами, вы просто заявляете об узких местах в производительности без каких-либо данных, подтверждающих это, и я указываю на это словами и пальцами.

Ребята, пожалуйста, ведите беседу уважительно и по теме. Этот вопрос касается обработки ошибок Go.

https://golang.org/conduct

Я хотел бы еще раз вернуться к части «вернуть ошибку / с дополнительным контекстом», поскольку я предполагаю, что игнорирование ошибки уже покрывается уже существующим _ .

Я предлагаю ключевое слово из двух слов, за которым может следовать строка (необязательно). Причина, по которой это ключевое слово состоит из двух слов, двоякая. Во-первых, в отличие от оператора, который по своей природе загадочен, легче понять, что он делает, без особых предварительных знаний. Я выбрал «или пузырь», потому что надеюсь, что слово or с отсутствием присвоенной ошибки будет означать для пользователя, что здесь обрабатывается ошибка, если оно не равно нулю. Некоторые пользователи уже связывают or с обработкой ложного значения из других языков (perl, python), и чтение data := Foo() or ... может подсознательно сказать им, что data непригоден для использования, если or достигнута часть инструкции. Во-вторых, ключевое слово bubble , будучи относительно коротким, может означать для пользователя, что что-то идет вверх (стек). Слово up также может подойти, хотя я не уверен, достаточно ли понятна вся or up . Наконец, все это ключевое слово, прежде всего потому, что оно более читабельно, а во-вторых, потому что это поведение не может быть записано самой функцией (вы можете вызвать panic, чтобы избежать функции, в которой вы находитесь, но тогда вы можете '' Не останавливайся, кому-то придется восстанавливать).

Следующее предназначено только для распространения ошибки, поэтому может использоваться только в функциях, которые возвращают ошибку и нулевые значения любых других возвращаемых аргументов:

Для возврата ошибки без каких-либо изменений:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

Для возврата ошибки с дополнительным сообщением:

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

И, наконец, представьте глобальный механизм адаптера для настраиваемой модификации ошибок:

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

Наконец, для тех немногих мест, где необходима действительно сложная обработка, уже существующий подробный способ уже является лучшим способом.

Интересный. Наличие глобального обработчика пузырьков дает людям, которые хотят трассировки стека, место для вызова трассировки, что является хорошим преимуществом этого метода. OTOH, если он имеет сигнатуру func(string, error) error , это означает, что всплытие должно выполняться со встроенным типом ошибки, а не с каким-либо другим типом, например конкретным типом, реализующим error .

Кроме того, наличие or bubble предполагает возможность or die или or panic . Я не уверен, что это особенность или ошибка.

в отличие от оператора, который по своей сути загадочен, легче понять, что он делает, без особых предварительных знаний

Это может быть хорошо, когда вы впервые столкнетесь с этим. Но чтение и запись снова и снова - это кажется слишком многословным и занимает слишком много места, чтобы передать довольно простую вещь - необработанную ошибку с пузырем вверх по стеку. Поначалу операторы выглядят загадочно, но они лаконичны и хорошо контрастируют со всем остальным кодом. Они четко отделяют основную логику от обработки ошибок, потому что на самом деле это разделитель. На мой взгляд, наличие такого количества слов в одной строке ухудшит читаемость. По крайней мере, объедините их в orbubble или отбросьте один из них. Не вижу смысла в двух ключевых словах. Он превращает Go в разговорный язык, и мы знаем, как это происходит (например, VB)

Я не большой поклонник глобального адаптера. Если мой пакет устанавливает собственный процессор, а ваш также устанавливает собственный процессор, кто победит?

@ object88
Я думаю, это похоже на регистратор по умолчанию. Вы устанавливаете вывод только один раз (в вашей программе), и это влияет на все используемые вами пакеты.

Обработка ошибок сильно отличается от регистрации; один описывает информативный вывод программы, другой управляет потоком программы. Если я настраиваю адаптер для выполнения одного действия в моем пакете, что мне нужно для правильного управления логическим потоком, а другой пакет или программа изменяет это, вы находитесь в плохом месте.

Пожалуйста, верните «Попробуй поймать наконец», и нам больше не нужно сражаться. Это делает всех счастливыми. Нет ничего плохого в заимствовании функций и синтаксиса из других языков программирования. Это сделала Java, и C # тоже, и оба они действительно успешные языки программирования. Сообщество GO (или авторы), пожалуйста, будьте открыты для изменений, когда это необходимо.

@KamyarM , я с уважением не согласен; try / catch не делает всех счастливыми. Даже если вы хотите реализовать это в своем коде, выброшенное исключение означает, что каждый, кто использует ваш код, должен обрабатывать исключения. Это не изменение языка, которое можно локализовать для вашего кода.

@ object88
На самом деле, мне кажется, что пузырьковый процессор описывает информативный вывод ошибок программы, что не сильно отличается от логгера. И я полагаю, вы хотите, чтобы в вашем приложении было единое представление об ошибке, которое не менялось от пакета к пакету.

Хотя, возможно, вы можете привести небольшой пример, возможно, я что-то упускаю.

Большое вам спасибо за ваши большие пальцы вниз. Я говорю именно об этом. Сообщество GO не открыто для изменений, я это чувствую, и мне это очень не нравится.

Вероятно, это не связано с этим случаем, но в другой день я искал Go-эквивалент тернарного оператора C ++ и наткнулся на этот альтернативный подход:

v: = map [bool] int {true: первое_выражение, false: второе_выражение} [условие]
вместо просто
v = условие? первое_выражение: второе_выражение;

Какую из двух форм вы, ребята, предпочитаете? Нечитаемый код выше (Go My Way), вероятно, с большим количеством проблем с производительностью или второй простой синтаксис в C ++ (Highway)? Я предпочитаю парней с шоссе. Я не знаю как вы.

Итак, чтобы подвести итог, пожалуйста, принесите новые синтаксисы, позаимствуйте их из других языков программирования. Там нет ничего плохого.

Наилучшие пожелания,

Сообщество GO не открыто для изменений, я это чувствую, и мне это очень не нравится.

Я думаю, что это неверно характеризует отношение, лежащее в основе того, что вы переживаете. Да, сообщество вызывает много возражений, когда кто-то предлагает попробовать / поймать или?:. Но причина не в том, что мы сопротивляемся новым идеям. Почти у всех есть опыт использования языков с этими функциями. Мы хорошо с ними знакомы, и кто-то из нас годами пользуется ими ежедневно. Наше сопротивление основано на том факте, что это _ старые идеи_, а не новые. Мы уже приняли изменение: отказ от try / catch и отказ от использования?:. Мы сопротивляемся тому, чтобы изменить _back_ на то, что мы уже использовали и не любили.

На самом деле, мне кажется, что пузырьковый процессор описывает информативный вывод ошибок программы, что не сильно отличается от логгера. И я полагаю, вы хотите, чтобы в вашем приложении было единое представление об ошибке, которое не менялось от пакета к пакету.

Что, если кто-то захочет использовать всплытие для передачи трассировки стека, а затем использовать это для принятия решения. Например, если ошибка возникает из-за операции с файлом, произойдет сбой, но если ошибка возникла из сети, подождите и повторите попытку. Я мог бы увидеть, как встроить некоторую логику для этого в обработчик ошибок, но если есть только один обработчик ошибок на время выполнения, это будет рецептом для конфликта.

@urandom , возможно, это тривиальный пример, но допустим, что мой адаптер возвращает другую структуру, реализующую error , которую я ожидаю использовать в другом месте моего кода. Если другой адаптер приходит и заменяет мой адаптер, мой код перестает работать правильно.

@KamyarM Язык и его идиомы идут

Попробовать-поймать-наконец было бы очень агрессивным подобным изменением: оно коренным образом изменило бы способ структурирования программ Go. Напротив, большинство других предложений, которые вы видите здесь, являются локальными для каждой функции: ошибки по-прежнему являются значениями, возвращаемыми явно, поток управления избегает нелокальных переходов и т. Д.

Чтобы использовать ваш пример тернарного оператора: да, вы можете подделать его сегодня, используя карту, но я надеюсь, что вы на самом деле не найдете этого в производственном коде. Он не следует идиомам. Вместо этого вы обычно увидите что-то вроде:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

Дело не в том, что мы не хотим заимствовать синтаксис, а в том, что мы должны подумать о том, как он будет вписываться в остальной язык и остальной код, который уже существует сегодня.

@KamyarM Я использую и Go, и Java, и я категорически _не_ хочу, чтобы Go копировал обработку исключений из Java. Если вам нужна Java, используйте Java. И, пожалуйста, переведите обсуждение тернарных операторов к соответствующему вопросу, например, # 23248.

@lpar Итак, если я работаю в компании и по какой-то неизвестной причине они выбрали GoLang в качестве языка программирования, мне просто нужно уволиться с работы и подать заявку на язык Java !? Давай, чувак!

@bcmills Вы можете посчитать предложенный вами код. Я думаю, что это 6 строк кода вместо одной, и, вероятно, вы получите за это пару точек цикломатической сложности кода (вы, ребята, используете Linter. Верно?).

@carlmjohnson и @bcmills Любой устаревший и зрелый синтаксис не означает, что он плохой. На самом деле я думаю, что синтаксис if else намного старше синтаксиса тернарного оператора.

Хорошо, что вы принесли с собой эту идиому GO. Я думаю, что это только одна из проблем этого языка. Всякий раз, когда есть запрос на изменения, кто-то говорит: «О нет, это против идиомы Go». Я рассматриваю это как просто предлог, чтобы сопротивляться изменениям и блокировать любые новые идеи.

@KamyarM, пожалуйста, будьте вежливы. Если вы хотите узнать больше о том, что стоит за ограничением языка, я рекомендую https://commandcenter.blogspot.com/2012/06/less-is-exponential-more.html.

Также общий комментарий, не связанный с недавним обсуждением try / catch.

В этой ветке было много предложений. Говоря за себя, я все еще не чувствую, что у меня есть четкое представление о проблеме (ах), которую необходимо решить. Я хотел бы услышать о них больше.

Я также был бы рад, если бы кто-то захотел взять на себя незавидную, но важную задачу по ведению организованного и обобщенного списка обсуждаемых проблем.

@josharian Я только что откровенно говорил там. Я хотел показать точные проблемы на языке или сообществе. Считайте это дополнительной критикой. GoLang открыт для критики, верно?

@KamyarM Если бы вы работали в компании, которая выбрала Rust в качестве языка программирования, вы бы

Причина, по которой программисты Go не хотят исключений в стиле Java, не связана с незнанием их. Я впервые столкнулся с исключениями в 1988 году через Lisp, и я уверен, что в этой ветке есть и другие люди, которые сталкивались с ними еще раньше - идея восходит к началу 1970-х.

То же самое можно сказать и о троичных выражениях. Прочтите историю Go - Кен Томпсон, один из создателей Go, реализовал тернарный оператор на языке B (предшественник C) в Bell Labs в 1969 году. Думаю, можно с уверенностью сказать, что он знал о его преимуществах и недостатках при рассмотрении включать ли его в Go.

Go открыт для критики, но мы требуем, чтобы обсуждение на форумах Go было вежливым. Откровенность - не то же самое, что быть невежливым. См. Раздел «Ценности Gopher» на сайте https://golang.org/conduct. Спасибо.

@lpar Да, если бы у Rust был такой форум, я бы так и сделал ;-) Серьезно, я бы сделал это. Потому что я хочу, чтобы мой голос был услышан.

@ianlancetaylor Использовал ли я какие-нибудь вульгарные слова или язык? Использовал ли я дискриминационный язык, запугивал кого-то или какие-либо нежелательные сексуальные домогательства? Я так не думаю.
Да ладно, мы здесь только говорим о языке программирования Go. Это не о религии, политике или чем-то подобном.
Я был откровенен. Я хотел, чтобы мой голос был услышан. Думаю, поэтому существует этот форум. Чтобы голоса были услышаны. Вам может не понравиться мое предложение или моя критика. Это нормально. Но я думаю, вам нужно дать мне возможность поговорить и обсудить, иначе мы все можем прийти к выводу, что все идеально, нет никаких проблем и поэтому нет необходимости в дальнейших обсуждениях.

@josharian Спасибо за статью, я на это посмотрю.

Что ж, я просмотрел свои комментарии, чтобы увидеть, нет ли там чего-нибудь плохого. Единственное, что я мог бы оскорбить (я до сих пор называю эту критику, кстати), - это идиомы языка программирования GoLang! Ха-ха!

Возвращаясь к нашей теме, если вы слышите мой голос, пожалуйста, идите. Авторы рассмотрят возможность возврата блоков Try catch. Предоставьте программисту право решать, использовать его в нужном месте или нет (у вас уже есть что-то подобное, я имею в виду восстановление с задержкой паники, тогда почему бы не попробовать Catch, который более знаком программистам?).
Я предложил обходной путь для текущей обработки ошибок Go для обеспечения обратной совместимости. Я не говорю, что это лучший вариант, но думаю, что он жизнеспособен.

Я уйду от дальнейшего обсуждения этой темы.

Спасибо за предоставленную возможность.

@KamyarM Вы

Еще раз: будьте вежливы. Придерживайтесь технических аргументов. Избегайте аргументов ad hominem, которые нападают на людей, а не на идеи. Если вы искренне не понимаете, что я имею в виду, я готов обсудить это в автономном режиме; напишите мне. Спасибо.

Я добавлю свой 2c и надеюсь, что он не будет буквально повторять что-то в других N сотнях комментариев (или не наступит на обсуждение предложения urandom).

Мне нравится исходная идея, которая была опубликована, но с двумя основными изменениями:

  • Синтаксический байкешеддинг: я твердо верю, что все, что имеет неявный поток управления, должно быть оператором само по себе, а не перегрузкой существующего оператора. Я выброшу там ?! , но меня устраивает то, что не так легко спутать с существующим оператором в Go.

  • Правая часть этого оператора должна принимать функцию, а не выражение с произвольно введенным значением. Это позволило бы разработчикам писать довольно краткий код обработки ошибок, но при этом четко понимать свои намерения и гибко подходить к тому, что они могут делать, например

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
}

RHS никогда не следует оценивать, если ошибки не происходит, поэтому этот код не будет выделять никаких замыканий или чего-либо еще на счастливом пути.

Также довольно просто «перегрузить» этот шаблон, чтобы он работал в более интересных случаях. Я имею в виду три примера.

Во-первых, мы могли бы сделать return условным, если RHS - это func(error) (error, bool) , например так (если мы позволим это, я думаю, мы должны использовать отдельный оператор для безусловных возвратов. Я используйте ?? , но мое заявление «Меня не волнует, если оно отличается» по-прежнему применяется):

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
}

В качестве альтернативы мы могли бы принять функции RHS, которые имеют типы возвращаемых значений, соответствующие типу внешней функции, например:

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
}

И, наконец, если мы действительно хотим, мы можем обобщить это для работы не только с ошибками, изменив тип аргумента:

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
}

... Что я лично считаю действительно полезным, когда мне нужно сделать что-то ужасное, например, вручную декодировать map[string]interface{} .

Для ясности, я в первую очередь показываю расширения в качестве примеров. Я не уверен, какие из них (если таковые имеются) обеспечивают хороший баланс между простотой, ясностью и общей полезностью.

Я хотел бы еще раз вернуться к части «вернуть ошибку / с дополнительным контекстом», поскольку я предполагаю, что игнорирование ошибки уже охвачено уже существующим _.

Я предлагаю ключевое слово из двух слов, за которым может следовать строка (необязательно).

@urandom первая часть вашего предложения приемлема, всегда можно начать с BubbleProcessor для второй доработки. Проблемы, поднятые @ object88 , действительны IMO; Я недавно видел такие советы, как «вы не должны перезаписывать клиент / транспорт по умолчанию http », это станет еще одним из них.

В этой ветке было много предложений. Говоря за себя, я все еще не чувствую, что у меня есть четкое представление о проблеме (ах), которую необходимо решить. Я хотел бы услышать о них больше.

Я также был бы рад, если бы кто-то захотел взять на себя незавидную, но важную задачу по ведению организованного и обобщенного списка обсуждаемых проблем.

Может быть, вы @josharian, если @ianlancetaylor назначит вас? : blush: Я не знаю, как планируются / обсуждаются другие вопросы, но, возможно, это обсуждение используется просто как «ящик для предложений»?

@KamyarM

@bcmills Вы можете посчитать предложенный вами код. Я думаю, что это 6 строк кода вместо одной, и, вероятно, вы получите за это пару точек цикломатической сложности кода (вы, ребята, используете Linter. Верно?).

Скрытие цикломатической сложности затрудняет ее просмотр, но не устраняет ее (помните strlen ?). Подобно тому, как "сокращение" обработки ошибок упрощает игнорирование семантики обработки ошибок, но ее труднее увидеть.

Любые утверждения или выражения в источнике, которые перенаправляют управление потоком, должны быть очевидными и лаконичными, но если это выбор между очевидным или кратким, в этом случае следует предпочесть очевидное.

Хорошо, что вы принесли с собой эту идиому GO. Я думаю, что это только одна из проблем этого языка. Всякий раз, когда есть запрос на изменения, кто-то говорит: «О нет, это против идиомы Go». Я рассматриваю это как просто предлог, чтобы сопротивляться изменениям и блокировать любые новые идеи.

Есть разница между новым и полезным. Вы верите, что само существование идеи заслуживает одобрения? В качестве упражнения взгляните на средство отслеживания проблем и попробуйте представить Go сегодня, если бы каждая идея была одобрена независимо от того, что думает сообщество.

Возможно, вы считаете, что ваша идея лучше других. Вот где начинается обсуждение. Вместо того, чтобы превращать разговор в разговор о том, как вся система сломана из-за идиом, обращайтесь к критике напрямую, по пунктам или найдите золотую середину между вами и вашими коллегами.

@ gdm85
Я добавил процессор для какой-то настройки со стороны возвращаемой ошибки. И хотя я действительно считаю, что это немного похоже на использование регистратора по умолчанию в том смысле, что вы можете избежать его использования в течение большей части времени, я сказал, что открыт для предложений. И для записи, я не верю, что регистратор по умолчанию и http-клиент по умолчанию даже удаленно относятся к одной и той же категории.

Мне также нравится предложение @gburgessiv , хотя я не большой поклонник самого загадочного оператора (может быть, по крайней мере, выберите ? как в Rust, хотя я все еще думаю, что это загадочно). Будет ли это выглядеть более читабельным:

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
}

И, надеюсь, его предложение также будет включать реализацию по умолчанию функции, аналогичную его returnErrorf, где-нибудь в пакете errors . Может быть, errors.Returnf() .

@KamyarM
Вы уже высказали здесь свое мнение и не получили ни одного комментария или реакции, относящейся к причине исключения. Я не вижу, чего можно добиться, если повторить одно и то же, кроме нарушения других дискуссий. И если это ваша цель, это просто не круто.

@josharian , попробую вкратце резюмировать дискуссию. Это будет предвзято, так как у меня есть предложение, и неполное, так как я не собираюсь перечитывать всю ветку.

Проблема, которую мы пытаемся решить, - это визуальный беспорядок, вызванный обработкой ошибок Go. Вот хороший пример ( источник ):

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
}

Некоторые комментаторы в этой ветке не думают, что это нужно исправлять; они счастливы, что обработка ошибок навязчива, потому что работа с ошибками так же важна, как и обработка случаев, когда они не связаны с ошибками. Для них ни одно из предложений здесь не стоит.

Предложения, которые действительно пытаются упростить код, делятся на несколько групп.

Некоторые предлагают форму обработки исключений. Учитывая, что Go мог выбрать обработку исключений с самого начала и не стал этого делать, это вряд ли будет принято.

Многие из предложений здесь выбирают действие по умолчанию, такое как возврат из функции (исходное предложение) или паника, и предлагают небольшой синтаксис, который позволяет легко выразить это действие. На мой взгляд, все эти предложения терпят неудачу, потому что они отдают предпочтение одному действию за счет других. Я регулярно использую возвраты, t.Fatal и log.Fatal для обработки ошибок, иногда все в один и тот же день.

Другие предложения не дают возможности дополнить или обернуть исходную ошибку или значительно усложнить ее исправление. Они также неадекватны, поскольку перенос - единственный способ добавить контекст к ошибкам, и если мы сделаем его слишком простым для пропуска, это будет происходить даже реже, чем сейчас.

Большинство оставшихся предложений добавляют немного сахара, а иногда и немного магии, чтобы упростить вещи, не ограничивая возможные действия или возможность обертывания. Предложения My и @bcmills добавляют минимальное количество сахара и нулевое волшебство, чтобы немного улучшить читаемость, а также предотвратить неприятные ошибки .

Несколько других предложений добавляют некоторого рода ограниченный нелокальный поток управления, например секцию обработки ошибок в начале или конце функции.

И последнее, но не менее важное: @mpvl понимает, что обработка ошибок может стать очень сложной при наличии паники. Он предлагает более радикальное изменение обработки ошибок Go, чтобы улучшить правильность и удобочитаемость. У него есть веские аргументы, но, в конце концов, я думаю, что его дела не требуют кардинальных изменений и могут быть рассмотрены с помощью существующих механизмов .

Приношу свои извинения всем, чьи идеи здесь не представлены.

У меня такое чувство, что меня спросят о разнице между сахаром и магией. (Я сам об этом спрашиваю.)

Sugar - это синтаксис, который сокращает код без существенного изменения правил языка. Оператор короткого присваивания := - это сахар. То же самое и с тернарным оператором C ?: .

Магия - это более серьезное нарушение языка, такое как введение переменной в область видимости без ее объявления или выполнение нелокальной передачи управления.

Граница определенно размыта.

Спасибо за это, @jba. Очень полезно. Чтобы выделить основные моменты, на данный момент выявлены следующие проблемы:

визуальный беспорядок, вызванный обработкой ошибок Go

и

обработка ошибок может стать очень сложной при возникновении паники

Если есть какие-либо другие принципиально разные проблемы (не решения), которые мы с @jba пропустили, позвоните (кому угодно). FWIW, я бы рассмотрел эргономику, беспорядок кода, цикломатическую сложность, заикание, шаблонный код и т. Д. Как варианты проблемы «визуального беспорядка» (или группы проблем).

@josharian Хотите ли вы рассматривать проблемы определения

Мне кажется, что большим объемом кода). Спасибо! Хотите отредактировать мой комментарий и добавить его в однострочное описание?

У меня такое чувство, что меня спросят о разнице между сахаром и магией. (Я сам об этом спрашиваю.)

Я использую это определение магии: взглянув на небольшой исходный код, можно ли понять, что он должен делать, с помощью некоторого варианта следующего алгоритма:

  1. Найдите все идентификаторы, ключевые слова и грамматические конструкции, присутствующие в строке или в функции.
  2. По поводу грамматических конструкций и ключевых слов обратитесь к документации на официальном языке.
  3. Для идентификаторов должен существовать четкий механизм их обнаружения с использованием информации в коде, который вы просматриваете, с использованием областей, в которых в настоящее время находится код, в соответствии с определением языка, из которого вы можете получить определение идентификатора, которое будет быть точным во время выполнения.

Если этот алгоритм _ надежно_ дает правильное понимание того, что будет делать код, это не волшебство. Если нет, значит, в этом есть некоторая магия. То, насколько рекурсивно вы должны применять это, когда вы пытаетесь следовать ссылкам на документацию и определениям идентификаторов вплоть до других определений идентификаторов, влияет на _сложность_, но не на _магичность_ рассматриваемых конструкций / кода.

Примеры магии включают: идентификаторы без четкого пути к их источнику, потому что вы импортировали их без пространства имен (импорт точек в Go, особенно если у вас их несколько). Любая способность языка может иметь нелокальное определение того, что будет разрешать какой-либо оператор, например, в динамических языках, где код может нелокально полностью переопределить ссылку на функцию или переопределить то, что язык делает для несуществующих идентификаторов. Объекты, созданные схемами, загружаемыми из базы данных во время выполнения, поэтому во время кода можно более или менее слепо надеяться, что они будут там.

Хорошая вещь в том, что это исключает почти всю субъективность.

Возвращаясь к обсуждаемой теме, кажется, что уже сделано множество предложений, и есть вероятность, что кто-то другой решит эту проблему с помощью другого предложения, которое заставит всех сказать: «Да! Вот и все!» приближаются к нулю.

Мне кажется, что, возможно, разговор должен двигаться в направлении категоризации различных аспектов сделанных здесь предложений и понимания расстановки приоритетов. Я особенно хотел бы увидеть это с прицелом на выявление противоречивых требований, которые расплывчато применяются здесь людьми.

Например, я видел жалобы на добавление дополнительных переходов в поток управления. Но для себя, говоря языком очень оригинального предложения, я ценю отсутствие необходимости добавлять || &PathError{"chdir", dir, err} восемь раз в функцию, если они являются общими. (Я знаю, что у Go не такая аллергия на повторяющийся код, как на некоторые другие языки, но все же повторяющийся код подвержен очень высокому риску ошибок расхождения.) Но в значительной степени по определению, если есть механизм для исключения такой обработки ошибок, код не может течь сверху вниз, слева направо, без скачков. Что обычно считается более важным? Я подозреваю, что тщательное изучение требований, которые люди неявно предъявляют к коду, выявило бы другие взаимно противоречащие друг другу требования.

Но в целом я чувствую, что если бы сообщество могло прийти к согласию по требованиям после всего этого анализа, правильное решение вполне могло бы просто выпасть из них, или, по крайней мере, правильный набор решений будет настолько явно ограничен, что проблема становится решаемой.

(Я также хотел бы указать, что, поскольку это предложение, текущее поведение в целом должно быть подвергнуто тому же анализу, что и новые предложения. Цель - значительное улучшение, а не совершенствование; отклонение двух или трех значительных улучшений, потому что ни одно из них идеальны - это путь к параличу. Все предложения в любом случае обратно совместимы, поэтому в тех случаях, когда текущий подход в любом случае уже является лучшим (имхо, случай, когда каждая ошибка обрабатывается законно по-разному, что, по моему опыту, редко, но случается) , текущий подход будет по-прежнему доступен.)

Я думал об этом с тех пор, как второй раз написал if err! = Nil в функции, мне кажется, что довольно простым решением было бы разрешить условный возврат, который выглядит как первая часть тернарного с пониманием того, что if условие не выполняется, мы не возвращаемся.

Я не уверен, насколько хорошо это будет работать с точки зрения синтаксического анализа / компиляции, но кажется, что это должно быть достаточно легко интерпретировать как оператор if, где '?' отображается без нарушения совместимости там, где его не видно, поэтому подумал, что выкину его там как вариант.

Кроме того, это может быть использовано и в других целях, помимо обработки ошибок.

так что вы можете сделать что-то вроде этого:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

Мы также могли бы делать такие вещи, когда у нас есть некоторый код очистки, предполагая, что handleErr вернул ошибку:

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
}

Возможно, это также следует из того, что вы могли бы уменьшить это до одного лайнера, если бы захотели:

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
}

предыдущий пример выборки из @jba потенциально может выглядеть так:

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
}

Было бы интересно узнать реакцию на это предложение, возможно, это не большой выигрыш в сохранении шаблона, но он остается довольно ясным и, надеюсь, требует только небольшого обратно совместимого изменения (возможно, некоторые очень неточные предположения на этом фронте).

Возможно, вы могли бы выделить это отдельным возвратом? ключевое слово, которое может внести ясность и упростить жизнь с точки зрения того, что вам не нужно беспокоиться о совместимости с return (учитывая все инструменты), тогда их можно просто переписать внутренне, как операторы if / return, что даст нам следующее:

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
}

Кажется, нет большой разницы между

return err != nil ? err

и

 if err != nil { return err }

Кроме того, иногда вы можете захотеть сделать что-то другое, кроме возврата, например, call panic или log.Fatal .

Я размышлял об этом с тех пор, как сделал предложение на прошлой неделе, и пришел к выводу, что согласен с @thejerf : мы обсуждали предложение за предложением, не делая шаг назад и не проверяя, что нам нравится. о каждом, неприязнь к каждому и каковы приоритеты для "правильного" решения.

Наиболее часто заявляемые требования заключаются в том, что в конечном итоге Go должен уметь обрабатывать 4 случая обработки ошибок:

  1. Игнорирование ошибки
  2. Возврат ошибки без изменений
  3. Возврат ошибки с добавленным контекстом
  4. Паника (или убийство программы)

Предложения можно разделить на три категории:

  1. Вернуться к стилю обработки ошибок try-catch-finally .
  2. Добавьте новый синтаксис / встроенные функции для обработки всех 4 случаев, перечисленных выше
  3. Утверждение, что go обрабатывает некоторые случаи достаточно хорошо, и предлагает синтаксис / встроенные функции для помощи в других случаях.

Критика данных предложений, похоже, разделена между опасениями по поводу читабельности кода, неочевидными переходами, неявным добавлением переменных в области видимости и краткостью. Лично я думаю, что в критике предложений было много личного мнения. Я не говорю, что это плохо, но мне кажется, что на самом деле нет объективных критериев, по которым можно было бы оценивать предложения.

Вероятно, я не тот человек, который пытается составить этот список критериев, но я думаю, что было бы очень полезно, если бы кто-то составил его. Я попытался обрисовать свое понимание дискуссии, так что это отправная точка для разбивки: 1. того, что мы видели, 2. что с этим не так, 3. почему эти вещи не так и 4. что мы сделали. вместо этого хотел бы видеть. Я думаю, что я захватил приличное количество первых трех пунктов, но у меня проблемы с ответом на пункт 4, не прибегая к «тому, что есть в Go на данный момент».

У @jba есть еще один замечательный резюмирующий комментарий выше, для большего контекста. Он говорит многое из того, что я здесь сказал, другими словами.

@ianlancetaylor или кто-либо другой, более тесно вовлеченный в проект, чем я, не могли бы вы добавить «формальный» (все в одном комментарии, организованный и в некоторой степени исчерпывающий, но никоим образом не обязывающий) набор критериев, которым необходимо соответствовать? Может быть, если мы обсудим эти критерии и определим 4-6 пунктов, которым должны соответствовать предложения, мы сможем перезапустить обсуждение с дополнительным контекстом?

Я не думаю, что смогу написать исчерпывающий формальный набор критериев. Лучшее, что я могу сделать, - это неполный список важных вещей, которыми следует пренебрегать только в том случае, если это дает значительную пользу.

  • Хорошая поддержка 1) игнорирования ошибки; 2) возврат ошибки без изменений; 3) обертывание ошибки дополнительным контекстом.
  • Хотя код обработки ошибок должен быть ясным, он не должен доминировать над функцией. Код, не связанный с обработкой ошибок, должен быть легко читаем.
  • Существующий код Go 1 должен продолжать работать, или, по крайней мере, должна существовать возможность механического перевода Go 1 в новый подход с полной надежностью.
  • Новый подход должен побудить программистов правильно обрабатывать ошибки. В идеале должно быть легко делать правильные поступки, какими бы они ни были в любой ситуации.
  • Любой новый подход должен быть короче и / или менее повторяющимся, чем текущий, но при этом оставаться ясным.
  • Сегодня язык действительно работает, и за каждое изменение приходится платить. Очевидно, что выгода от изменения окупается. Это должно быть не просто стирка, это должно быть явно лучше.

Я надеюсь собрать здесь заметки в какой-то момент здесь сам, но я хочу обратиться к тому, что, ИМХО, является еще одним важным камнем преткновения в этом обсуждении, - тривиальностью примеров (-ов).

Я извлек это из своего реального проекта и очистил для внешнего выпуска. (Я думаю, что stdlib - не лучший источник, поскольку в нем, среди прочего, отсутствуют проблемы с журналированием ..)

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
}

(Давайте не будем слишком зацикливаться на коде. Я, конечно, не могу вас остановить, но помните, что это не мой настоящий код, и он был немного искажен. И я не могу помешать вам опубликовать свой собственный образец кода.)

Наблюдения:

  1. Это не идеальный код; Я часто возвращаю простые ошибки, потому что это так просто, именно с таким кодом, с которым у нас есть проблемы. Предложения должны оцениваться как по лаконичности, так и по тому, насколько легко они демонстрируют исправление этого кода.
  2. Предложение if forwardPort == 0 _намеренно_ продолжается с ошибками, и да, это реальное поведение, а не то, что я добавил для этого примера.
  3. Этот код ЛИБО возвращает действительного подключенного клиента ИЛИ возвращает ошибку и никаких утечек ресурсов, поэтому обработка вокруг .Close () (только если функция ошибается) является преднамеренной. Обратите внимание, что ошибки из Close исчезают, что довольно типично для реального Go.
  4. Номер порта ограничен в другом месте, поэтому url.Parse не может завершиться ошибкой (при проверке).

Я бы не стал утверждать, что это демонстрирует все возможные ошибки, но охватывает довольно широкий диапазон. (Я часто защищаю Go on HN и тому подобное, указывая на то, что к тому времени, когда мой код готов, на моих сетевых серверах часто бывает _все виды_ ошибочного поведения; исследуя свой собственный производственный код, с 1/3 чтобы полностью половина ошибок вызывала что-то, кроме простого возврата.)

Я также (повторно) опубликую свое собственное (обновленное) предложение применительно к этому коду (если кто-то не убедит меня, что до этого есть что-то еще лучше), но в интересах не монополизировать разговор я собираюсь подождать по крайней мере, на выходных. (Это меньше текста, чем кажется, потому что это огромный кусок исходного кода, но все же ....)

Использование try где try - это просто ярлык для if! = Nil return уменьшает код на 6 строк из 59, что составляет примерно 10%.

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
}

Примечательно, что в нескольких местах я хотел написать try x() но не смог, так как мне нужно было установить err для правильной работы defers.

Еще один. Если у нас есть try быть чем-то, что может произойти в строках назначения, оно сокращается до 47 строк.

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
}

Немного магический (можно до -1), но поддерживает механический перевод

Вот как (несколько обновленное) могло бы выглядеть мое предложение:

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

Определения:

pop допустимо для функциональных выражений, в которых крайнее правое значение является ошибкой. Он определяется как «если ошибка не равна нулю, возвратиться из функции с нулевыми значениями для всех других значений в результатах и ​​этой ошибке, в противном случае вывести набор значений без ошибки». pop не имеет привилегированного взаимодействия с erroring() ; нормальный возврат ошибки будет по-прежнему виден для erroring() . Это также означает, что вы можете возвращать ненулевые значения для других возвращаемых значений и по-прежнему использовать отложенную обработку ошибок. Метафора выталкивает крайний правый элемент из «списка» возвращаемых значений.

erroring() определяется как переход вверх по стеку к запускаемой отложенной функции, а затем к предыдущему элементу стека (функция, в которой выполняется defer, в данном случае NewClient), для доступа к значению возвращаемой в данный момент ошибки. в ходе выполнения. Если у функции нет такого параметра, либо panic, либо возврат nil (в зависимости от того, что имеет больше смысла). Это значение ошибки не обязательно должно быть из pop ; это все, что возвращает ошибку от целевой функции.

seterr(error) позволяет изменить возвращаемое значение ошибки. Тогда это будет ошибка, которую будут замечать любые будущие вызовы erroring() , которые, как показано здесь, допускают такое же основанное на отсрочке связывание, которое может быть выполнено сейчас.

Здесь я использую упаковку hashicorp и мультиошибку; вставьте свои собственные умные пакеты по желанию.

Даже с определенной дополнительной функцией сумма короче. Я ожидаю амортизации этих двух функций при дополнительном использовании, поэтому они должны учитываться только частично.

Заметьте, я просто оставляю обработку forwardPort в покое, а не пытаюсь заглушить еще немного синтаксиса. В исключительном случае это нормально, если это будет более подробным.

Самое интересное в этом предложении ИМХО можно увидеть, только если вы представите, что пытаетесь написать это с обычными исключениями. В конечном итоге он становится довольно глубоко вложенным, и обработка _collection_ ошибок, которые могут возникнуть, при обработке исключений довольно утомительна. (Как и в реальном коде Go, ошибки .Close обычно игнорируются, ошибки, возникающие в самих обработчиках исключений, как правило, игнорируются в коде, основанном на исключениях.)

Это расширяет существующие шаблоны Go, такие как defer и использование ошибок как значений для упрощения исправления шаблонов обработки ошибок, которые в некоторых случаях трудно выразить с помощью текущего Go или исключений, не требует радикальной хирургии. к среде выполнения (я не думаю), а также на самом деле не _require_ Go 2.0.

К недостаткам можно отнести требование erroring , pop и seterr качестве ключевых слов, что влечет за собой накладные расходы defer для этих функций, а также факт, что обработка ошибок с учётом факторинговых к функциям обработки, и что он ничего не делает для "принудительной" правильной обработки. Хотя я не уверен, что последнее возможно, поскольку согласно (правильному) требованию обратной совместимости вы всегда можете сделать текущую вещь.

Здесь очень интересное обсуждение.

Я хотел бы сохранить переменную ошибки с левой стороны, чтобы не вводились магически появляющиеся переменные. Как и в исходном предложении, я хотел бы, чтобы все сообщения об ошибках обрабатывались в той же строке. Я бы не стал использовать || -оператор, поскольку он мне кажется "слишком логическим" и каким-то образом скрывает "возврат".

Так что я бы сделал его более читабельным, используя расширенное ключевое слово «return?». В C # знак вопроса используется в некоторых местах для ярлыков. E. g. вместо написания:

if(foo != null)
{ foo.Bar(); }

вы можете просто написать:
foo?.Bar();

Итак, для Go 2 я хотел бы предложить следующее решение:

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?

Просто мысль:

foo, err: = myFunc ()
эрр! = ноль? возвратная упаковка (ошибка)

Или

если ошибка! = ноль? возвратная упаковка (ошибка)

Если вы хотите обвести это фигурными скобками, нам не нужно ничего менять!

if err != nil { return wrap(err) }

Вы можете иметь _все_ индивидуальную обработку, которую хотите (или вообще не использовать), вы сохранили две строки кода из типичного случая, он на 100% обратно совместим (потому что нет изменений в языке), он более компактен и легкий. Это поражает многие из движущих сил gofmt ?

Я написал это до того, как прочитал предложение Карлмджонсона, которое похоже ...

Просто # перед ошибкой.

Но в реальном приложении вам все равно придется написать нормальный if err != nil { ... } чтобы вы могли регистрировать ошибки, это делает бесполезной минималистичную обработку ошибок, если только вы не можете добавить промежуточное ПО возврата через аннотации, называемые after , который запускается после возврата из функции ... (например, defer но с аргументами).

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

чище, чем:

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
}

Как насчет Swift, например, guard , за исключением того, что вместо 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}

Мне нравится ясность обработки ошибок go. Единственная беда, имхо, сколько места занимает. Я бы предложил 2 настройки:

  1. разрешить использование нулевых элементов в логическом контексте, где nil эквивалентно false , а не nil - true
  2. поддерживают однострочные условные операторы, такие как && и ||

Так

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

может стать

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

или даже короче

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

и мы можем сделать

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

или, возможно

i,ok := v.(int)
ok && s *= i

Если перегрузка && и || создает слишком много двусмысленности, возможно, можно выбрать другие символы (не более 2), например ? и # или ?? и ## или ?? и !! , что угодно. Смысл в том, чтобы поддерживать однострочный условный оператор с минимальным количеством "шумных" символов (без скобок, скобок и т. Д.). Операторы && и || хороши, потому что это использование имеет прецедент в других языках.

Это не предложение для поддержки сложных однострочных условных выражений , а только однострочных условных выражений.

Кроме того, это не предложение поддерживать полный спектр «правдивости» на некоторых других языках. Эти условные выражения будут поддерживать только nil / non-nil или логические значения.

Для этих условных операторов может быть даже целесообразно ограничиться отдельными переменными и не поддерживать выражения. Все, что более сложно, или с предложением else будет обрабатываться стандартными конструкциями if ... .

Почему бы просто не изобрести колесо и не использовать известную форму try..catch как ранее сказал

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Похоже, что нет причин различать источник обнаруженной ошибки, потому что, если вам это действительно нужно, вы всегда можете использовать старую форму if err != nil без try..catch .

Кроме того, я не совсем уверен в этом, но может быть добавлена ​​возможность "выкидывать" ошибку, если она не обрабатывается?

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 с точки зрения удобочитаемости очень важно полностью понимать поток управления. Глядя на ваш пример, нельзя сказать, какая строка может вызвать переход к блоку catch. Это похоже на скрытый оператор goto . Неудивительно, что в современных языках это делается явно и требуется, чтобы программист явно отмечал места, где поток управления может расходиться из-за возникшей ошибки. Очень похоже на return или goto но с гораздо более приятным синтаксисом.

@creker да, я полностью с вами согласен. Я думал об управлении потоком в примере выше, но не понимал, как это сделать в простой форме.

Может быть, что-то вроде:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Или другие предложения, например try a := foo() ..?

@gobwas

Когда я попадаю в блок catch, как мне узнать, какая функция в блоке try вызвала ошибку?

@urandom, если вам нужно это знать, вы, вероятно, захотите сделать if err != nil без try..catch .

@ robert-wallis: Я упоминал ранее в потоке заявление о защите Swift, но страница настолько велика, что Github больше не загружает ее по умолчанию. : -P Я все еще думаю, что это хорошая идея, и в целом я поддерживаю поиск положительных / отрицательных примеров на других языках.

@pdk

разрешить использование элементов с нулевым значением в логическом контексте, где nil эквивалентно false, а не nil - true

Я вижу, что это приводит к множеству ошибок с использованием пакета флагов, когда люди будут писать if myflag { ... } но имеют в виду написать if *myflag { ... } и компилятор не поймает это.

try / catch короче, чем if / else, когда вы пытаетесь несколько раз подряд, что более или менее все согласны с тем, что это плохо из-за проблем с потоком управления и т. д.

FWIW, метод try / catch Swift, по крайней мере, решает визуальную проблему незнания, какие операторы могут вызывать:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@ robert-wallis, у вас есть пример:

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}

При первом использовании guard это очень похоже на if err != nil { return &FooError{"Couldn't foo fails.txt", err}} , поэтому я не уверен, что это большая победа.

При втором использовании не сразу понятно, откуда взялся err . Похоже, это то, что вернулось из os.Open , что, как я полагаю, не было вашим намерением? Было бы это точнее?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

В таком случае это выглядит как ...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

Но в нем по-прежнему меньше визуального беспорядка. if err = , ; err != nil { - даже если это однострочник, для такой простой вещи все равно происходит слишком много

Договорились, что беспорядка меньше. Но значительно меньше, чтобы оправдать добавление к языку? Я не уверен, что согласен с этим.

Я думаю, что читаемость блоков try-catch в Java / C # / ... очень хорошая, поскольку вы можете следовать последовательности «счастливого пути» без какого-либо прерывания обработки ошибок. Обратной стороной является то, что у вас в основном есть скрытый механизм перехода.

В Go я начинаю вставлять пустые строки после обработчика ошибок, чтобы сделать продолжение логики «счастливого пути» более заметным. Итак, из этого примера с golang.org (9 строк)

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

я часто так делаю (11 строк)

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

Теперь вернемся к предложению, так как я уже писал что-то вроде этого, было бы неплохо (3 строки)

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}

Теперь я ясно вижу счастливый путь. Мои глаза все еще осознают, что с правой стороны есть код для обработки ошибок, но мне нужно «анализировать глазами» его только тогда, когда это действительно необходимо.

Вопрос ко всем сбоку: должен ли этот код компилироваться?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

ИМХО пользователь должен быть вынужден сказать, что он намеренно игнорирует ошибку:

func main() {
    _ := foobar()
}

Добавив мини-отчет, связанный с пунктом 3 исходного сообщения @ianlancetaylor , верните ошибку с дополнительной контекстной информацией .

При разработке библиотеки flac для Go мы хотели добавить контекстную информацию к ошибкам с помощью пакета @davecheney pkg / errors (https://github.com/mewkiz/flac/issues/22). В частности, мы обертываем ошибки, возвращаемые с помощью errors.WithStack, который аннотирует ошибки информацией трассировки стека.

Поскольку ошибка аннотирована, необходимо создать новый базовый тип для хранения этой дополнительной информации в случае ошибок. WithStack, тип - errors.withStack .

type withStack struct {
    error
    *stack
}

Теперь, чтобы получить исходную ошибку, принято использовать errors.Cause . Это позволяет сравнить исходную ошибку, например, с io.EOF .

Затем пользователь библиотеки может написать что-нибудь вроде https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78, используя errors.Cause чтобы проверить исходную ошибку. ценность:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

Это хорошо работает почти во всех случаях.

Однако при рефакторинге нашей обработки ошибок, чтобы последовательно использовать pkg / errors для добавления контекстной информации, мы столкнулись с довольно серьезной проблемой. Чтобы проверить заполнение нулями, мы реализовали io.Reader, который просто проверяет, начали давать сбой .

Проблема заключалась в том, что основным типом ошибки, возвращаемой zeros.Read теперь является errors.withStack, а не io.EOF. Это впоследствии вызвало проблемы, когда мы использовали этот ридер в сочетании с io.Copy , который специально проверяет io.EOF и не знает, как использовать errors.Cause чтобы "развернуть" ошибку, помеченную контекстная информация. Поскольку мы не можем обновить стандартную библиотеку, решением было вернуть ошибку без аннотированной информации (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

Хотя потеря аннотированной информации для интерфейсов, возвращающих конкретные значения, является потерей, с этим можно жить.

Из нашего опыта можно сделать вывод, что нам повезло, поскольку наши тестовые примеры это обнаружили. Компилятор не выдал ошибок, поскольку тип zeros прежнему реализует интерфейс io.Reader . Мы также не думали, что столкнемся с проблемой, поскольку добавленная аннотация ошибки была перезаписью, сгенерированной машиной, простое добавление контекстной информации к ошибкам не должно влиять на поведение программы в нормальном состоянии.

Но это произошло, и по этой причине мы хотим представить наш отчет об опыте на рассмотрение; когда вы думаете о том, как интегрировать добавление контекстной информации в обработку ошибок для Go 2, чтобы сравнение ошибок (используемое в интерфейсных контрактах) по-прежнему выполнялось без проблем.

Любезно,
Робин

@mewmew , давайте оставим этот вопрос об аспектах потока управления при обработке ошибок. Как лучше всего заключать и развертывать ошибки, следует обсудить в другом месте, поскольку это в значительной степени ортогонально потоку управления.

Я не знаком с вашей кодовой базой и понимаю, что вы сказали, что это был автоматический рефакторинг, но зачем вам нужно было включать контекстную информацию в EOF? Хотя EOF обрабатывается системой типов как ошибка, на самом деле это скорее значение сигнала, а не фактическая ошибка. В частности, в реализации io.Reader большую часть времени это ожидаемое значение. Разве не лучше было бы обернуть ошибку, только если бы это не было io.EOF ?

Да, я предлагаю оставить все как есть. У меня создалось впечатление, что система ошибок в Go была специально разработана таким образом, чтобы не дать разработчикам возиться с ошибками в стеке вызовов. Эти ошибки предназначены для исправления там, где они возникают, и для того, чтобы знать, когда лучше использовать панику, когда вы не можете.

Я имею в виду, что не является ли try-catch-throw по сути тем же самым поведением panic () и recovery ()?

Вздох, если мы действительно собираемся пойти по этому пути. Почему мы не можем просто сделать что-то вроде

_, ? := foo()
x?, err? := bar()

или, возможно, даже что-то вроде

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

где ? становится сокращенным псевдонимом для if var! = nil {return var}.

Мы даже можем определить другой специальный встроенный интерфейс, которому удовлетворяет метод

func ?() bool //looks funky but avoids breakage.

который мы можем использовать для существенного переопределения поведения по умолчанию нового и улучшенного условного оператора.

@mortdeus

Думаю, я согласен.
Если проблема заключается в том, чтобы иметь хороший способ представить счастливый путь, плагин для IDE мог бы сворачивать / разворачивать каждый экземпляр if err != nil { return [...] } с помощью ярлыка?

Я чувствую, что сейчас важна каждая часть. err != nil важен. return ... важно.
Писать немного хлопотно, но это нужно писать. И действительно ли это тормозит людей? На что нужно время, так это на размышления об ошибке и на то, что нужно вернуть, а не на ее написание.

Меня бы больше интересовало предложение, которое позволяет ограничить область действия переменной err .

Думаю, моя условная идея - самый изящный способ решить эту проблему. Я просто подумал о некоторых других вещах, которые сделают эту функцию достаточно достойной для включения в Go. Я собираюсь оформить свою идею в отдельном предложении.

Я не понимаю, как это могло работать:

x, y, ошибка: = baz (); return ( x? && y? ) || err?

где ? становится сокращенным псевдонимом для if var == nil {return var}.

x, y, ошибка: = baz (); return ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, ошибка: = baz (); return (x? && y?) || ошибаюсь?

становится

x, y, ошибка: = baz ();
if ((x! = nil && y! = nil) || err! = nil)) {
вернуть x, y, err
}

когда вы видите x? && y? || ошибаюсь? вы должны подумать: "действительны ли x и y? Как насчет ошибки?"

в противном случае функция возврата не выполняется. Я только что написал новое предложение по этой идее, которое развивает идею немного дальше с новым специальным встроенным типом интерфейса.

Я предлагаю Go добавить обработку ошибок по умолчанию в Go версии 2.

Если пользователь не обрабатывает ошибку, компилятор вернет ошибку, если она не равна нулю, поэтому, если пользователь напишет:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

скомпилировать преобразовать его в:

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 {
    ...
}

Если пользователь обработает ошибку или проигнорирует ее с помощью _, компилятор ничего не сделает:

_ = func1()

или

err := func1()

для нескольких возвращаемых значений это похоже:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

компилятор преобразуется в:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Если подпись Func () не возвращает ошибку, но вызывает функции, которые возвращают ошибку, компилятор сообщит об ошибке: «Пожалуйста, обработайте вашу ошибку в Func ()»
Затем пользователь может просто зарегистрировать ошибку в Func ()

И если пользователь хочет обернуть некоторую информацию для ошибки:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

или

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

Преимущество в том,

  1. программа просто терпит неудачу в точке, где происходит ошибка, пользователь не может игнорировать ошибку неявно.
  2. может значительно сократить количество строк кода.

это не так просто, потому что Go может иметь несколько возвращаемых значений, и язык не должен назначать то, что по существу приравнивается к значениям по умолчанию для возвращаемых аргументов, если разработчик явно не знает, что происходит.

Я считаю, что включение обработки ошибок в синтаксис присваивания не решает корня проблемы, которая заключается в том, что «обработка ошибок повторяется».

Использование if (err != nil) { return nil } (или аналогичного) после многих строк кода (где это имеет смысл) противоречит принципу DRY (не повторяйтесь). Думаю, поэтому нам это не нравится.

Также есть проблемы с try ... catch . Вам не нужно явно обрабатывать ошибку в той же функции, где она возникает. Я считаю, что это веская причина, по которой нам не нравится try...catch .

Я не считаю, что это взаимоисключающие; у нас может быть что-то вроде try...catch без throws .

Еще одна вещь, которая мне лично не нравится в try...catch - это произвольная необходимость ключевого слова try . Нет причин, по которым вы не можете catch после любого ограничителя области видимости, что касается рабочей грамматики. (кто-нибудь позвонит, если я ошибаюсь)

Вот что я предлагаю:

  • используя ? в качестве заполнителя для возвращаемой ошибки, где _ будет использоваться для ее игнорирования
  • вместо catch как в моем примере ниже, можно использовать error? для полной обратной совместимости

^ Если мое предположение о том, что они обратно совместимы, неверно, сообщите об этом.

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

Я придумал аргумент против этого: если вы напишете это таким образом, изменение этого блока catch может непреднамеренно повлиять на код в более глубоких областях. Это та же проблема, что и у нас с try...catch .

Я думаю, что если вы можете сделать это только в рамках одной функции, риск управляем - возможно, такой же, как и текущий риск забыть изменить строку кода обработки ошибок, когда вы намереваетесь изменить многие из них. Я вижу в этом такую ​​же разницу между последствиями повторного использования кода и последствиями несоблюдения DRY (т.е. без бесплатного обеда, как говорится)

Изменить: я забыл указать важное поведение для своего примера. В случае, если ? используется в области без каких-либо catch , я думаю, это должна быть ошибка компилятора (а не паника, о которой я, по общему признанию, подумал первым)

Изменить 2: Сумасшедшая идея: возможно, блок catch просто не повлияет на поток управления ... это было бы буквально похоже на копирование и вставку кода внутри catch { ... } в строку после ошибки ? ed (ну, не совсем - у него все равно будет своя область видимости). Это кажется странным, поскольку никто из нас к этому не привык, поэтому catch определенно не должно быть ключевым словом, если это делается таким образом, но в противном случае ... почему бы и нет?

@mewmew , давайте оставим этот вопрос об аспектах потока управления при обработке ошибок. Как лучше всего заключать и развертывать ошибки, следует обсудить в другом месте, поскольку это в значительной степени ортогонально потоку управления.

Хорошо, давайте оставим этот поток для управления потоком. Я добавил это просто потому, что это проблема, связанная с конкретным использованием пункта 3, возвращает ошибку с дополнительной контекстной информацией .

@jba Знаете ли вы о каких-либо проблемах, специально посвященных упаковке / развертыванию контекстной информации при ошибках?

Я не знаком с вашей кодовой базой и понимаю, что вы сказали, что это был автоматический рефакторинг, но зачем вам нужно было включать контекстную информацию в EOF? Хотя EOF обрабатывается системой типов как ошибка, на самом деле это скорее значение сигнала, а не фактическая ошибка. В частности, в реализации io.Reader большую часть времени это ожидаемое значение. Не лучше ли было бы обернуть ошибку, если бы это не io.EOF?

@DeedleFake Я могу немного уточнить, но, чтобы оставаться в теме, сделаю это в вышеупомянутом выпуске, посвященном упаковке / развертыванию контекстной информации для ошибок.

Чем больше я читаю все предложения (включая свое), тем меньше думаю, что у нас действительно есть проблемы с обработкой ошибок на ходу.

Я бы хотел, чтобы какое-то принуждение не игнорировало случайно возвращаемое значение ошибки, а соблюдало хотя бы
_ := returnsError()

Я знаю, что есть инструменты для поиска этих проблем, но поддержка языка на первом уровне может выявить некоторые ошибки. Не обрабатывать ошибку вообще - это все равно, что иметь неиспользованную переменную для меня, что уже является ошибкой. Это также поможет с рефакторингом, когда вы вводите в функцию тип, возвращаемый ошибкой, поскольку вы вынуждены обрабатывать ее повсюду.

Основная проблема, которую пытается решить большинство людей, - это «количество набора текста» или «количество строк». Я бы согласился с любым синтаксисом, который уменьшает количество строк, но в основном это проблема gofmt. Просто разрешите встроенные «однострочные области видимости», и все будет хорошо.

Еще одно предложение по сохранению некоторой типизации - это неявная проверка nil как с логическими значениями:

err := returnsError()
if err { return err }

или даже

if err := returnsError(); err { return err }

Это будет работать со всеми указательными типами причин.

Мне кажется, что все, что сокращает вызов функции + обработку ошибок в одну строку, приведет к менее читаемому коду и более сложному синтаксису.

менее читаемый код и более сложный синтаксис.

У нас уже есть менее читаемый код из-за подробной обработки ошибок. Добавление уже упомянутого трюка Scanner API, который должен скрыть эту многословность, только усугубляет ситуацию. Добавление более сложного синтаксиса может помочь с удобочитаемостью, для чего в конечном итоге нужен синтаксический сахар. Иначе в этом обсуждении нет смысла. На мой взгляд, шаблон всплывания ошибки и возврата нулевого значения для всего остального достаточно распространен, чтобы оправдать изменение языка.

Шаблон всплытия ошибки и возврата нулевого значения для всего

19642 сделает это проще.


Также спасибо @mewmew за отчет об опыте. Это определенно связано с этим потоком, поскольку касается опасностей в конкретных видах схем обработки ошибок. Я бы хотел увидеть больше таких.

Мне кажется, что я не очень хорошо объяснил свою идею, поэтому я сформулировал суть (и исправил многие недостатки, которые только что заметил)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

В этом суть есть несколько идей, поэтому я надеюсь, что их можно будет обсудить отдельно друг от друга.

Если оставить в стороне идею о том, что предложение здесь должно быть явно связано с обработкой ошибок, что, если Go представит что-то вроде инструкции collect ?

Оператор collect будет иметь форму collect [IDENT] [BLOCK STMT] , где идентификатор должен содержать переменную в области видимости с типом nil -able. В операторе collect доступна специальная переменная _! в качестве псевдонима для переменной, в которую выполняется сбор данных. _! нельзя использовать нигде, кроме как присвоение, то же самое, что и _ . Всякий раз, когда _! назначается, выполняется неявная проверка nil , и если _! не равно нулю, блок прекращает выполнение и продолжает выполнение остальной части кода.

Теоретически это выглядело бы примерно так:

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
}

что эквивалентно

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
}

Некоторые приятные вещи, которые могут быть доступны благодаря синтаксической функции:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

Требуются новые функции синтаксиса:

  1. ключевое слово collect
  2. специальный идентификатор _! (я играл с этим в парсере, нетрудно сделать это совпадение в качестве идентификатора, не нарушая ничего другого)

Причина, по которой я предлагаю что-то подобное, заключается в том, что аргумент «слишком повторяющаяся обработка ошибок» можно свести к «нулевым проверкам слишком часто». Go уже имеет множество функций обработки ошибок, которые работают как есть. Вы можете игнорировать ошибку с помощью _ (или просто не захватывать возвращаемые значения), вы можете вернуть ошибку без изменений с помощью if err != nil { return err } или добавить контекст и вернуться с помощью if err != nil { return wrap(err) } . Ни один из этих методов сам по себе не является слишком повторяющимся. Повторяемость ( очевидно ) возникает из-за необходимости повторять эти или аналогичные синтаксические операторы по всему коду. Я думаю, что введение способа выполнения операторов до тех пор, пока не встретится значение, отличное от nil, - хороший способ сохранить такую ​​же обработку ошибок, но уменьшить количество шаблонов, необходимых для этого.

  • Хорошая поддержка 1) игнорирования ошибки; 3) обертывание ошибки дополнительным контекстом.

проверьте, так как он остается прежним (в основном)

  • Хотя код обработки ошибок должен быть ясным, он не должен доминировать над функцией.

проверьте, так как код обработки ошибок теперь может размещаться в одном месте, если это необходимо, в то время как основная часть функции может выполняться линейно читаемым способом

  • Существующий код Go 1 должен продолжать работать, или, по крайней мере, должна существовать возможность механического перевода Go 1 в новый подход с полной надежностью.

проверьте, это дополнение, а не переделка

  • Новый подход должен побудить программистов правильно обрабатывать ошибки.

check, я думаю, поскольку механизмы обработки ошибок не отличаются - у нас был бы просто синтаксис для "сбора" первого ненулевого значения из серии выполнений и присваиваний, который можно использовать для ограничения количества места, где мы должны написать наш код обработки ошибок в функции

  • Любой новый подход должен быть короче и / или менее повторяющимся, чем текущий, но при этом оставаться ясным.

Я не уверен, что это применимо здесь, поскольку предлагаемая функция относится не только к обработке ошибок. Я действительно думаю, что он может сократить и прояснить код, который может генерировать ошибки, без загромождения нулевых проверок и досрочных возвратов.

  • Сегодня язык действительно работает, и за каждое изменение приходится платить. Это должно быть не просто стирка, это должно быть явно лучше.

Согласен, и поэтому кажется, что изменение, охват которого выходит за рамки простой обработки ошибок, может быть уместным. Я считаю, что основная проблема заключается в том, что чеки nil в go становятся повторяющимися и многословными, и так уж случилось, что error - это тип, допускающий nil .

@KernelDeimos Мы, по сути, только что придумали то же самое. Однако я пошел еще дальше и объяснил, почему способ x, ? := doSomething() самом деле не работает так хорошо на практике. Хотя приятно видеть, что я не единственный, кто думает о добавлении? оператор в язык интересным способом.

https://github.com/golang/go/issues/25582

Разве это не просто ловушка ?

Вот такой шар:

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 строк → 44

trap означает «запустить этот код в порядке стека, если переменная, помеченная как ? указанного типа, не является нулевым значением». Это похоже на defer но это может повлиять на поток управления.

Мне нравится идея trap , но синтаксис меня немного беспокоит. Что, если бы это был вид декларации? Например, trap err error {} объявляет trap именем err типа error который при назначении запускает данный код. Код даже не нужно возвращать; это просто разрешено. Это также нарушает зависимость от того, что nil является особенным.

Изменить: развернуть и привести пример, когда я не разговариваю по телефону.

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

По сути, trap работает так же, как var , за исключением того, что всякий раз, когда он назначается с помощью присоединенного к нему оператора ? выполняется блок кода. Оператор ? также предотвращает его затенение при использовании с := . Повторное объявление trap в той же области, в отличие от var , разрешено, но оно должно быть того же типа, что и существующий; это позволяет вам изменить связанный блок кода. Поскольку запущенный блок не обязательно возвращается, он также позволяет вам иметь отдельные пути для определенных вещей, например, для проверки наличия err == io.EOF .

Что мне нравится в этом подходе, так это то, что он похож на пример errWriter из « Ошибки - значения» , но в несколько более общей настройке, которая не требует объявления нового типа.

@carlmjohnson, кому ты отвечал?
Тем не менее, эта концепция trap кажется просто другим способом написать выражение defer , не так ли? Код в том виде, в котором он написан, был бы по существу таким же, если бы вы panic 'd при ошибке, отличной от нуля, а затем использовали отложенное закрытие для установки именованных возвращаемых значений и выполнения очистки. Я думаю, что здесь возникают те же проблемы, что и в моем предыдущем предложении использовать _! для автоматической паники, поскольку при этом один метод обработки ошибок накладывается на другой. FWIW Я также чувствовал, что код в том виде, в котором он был написан, был намного сложнее рассуждать, чем оригинал. Можно ли скопировать эту концепцию trap с go сегодня, даже если она менее ясна, чем наличие для нее синтаксиса? Я чувствую, что это возможно, и было бы if err != nil { panic (err) } и defer чтобы захватить и обработать это.

Это похоже на концепцию блока collect который я предложил выше, который, как я лично считаю, обеспечивает более чистый способ выразить ту же идею («если это значение не равно нулю, я хочу зафиксировать его и сделать что-нибудь. с этим"). Го любит быть прямолинейным и откровенным. trap выглядит как новый синтаксис для panic / defer но с менее понятным потоком управления.

@mccolljr , ответ мне . Из вашего сообщения я делаю вывод, что вы не видели мое предложение (теперь в «скрытых элементах», хотя и не так далеко), потому что я фактически использовал оператор отсрочки в своем предложении, расширенный с помощью recover - как функция для обработки ошибок.

Я также наблюдал, как переписывание "ловушки" упало из-за большого количества функций, которые было в моем предложении (появляются очень разные ошибки), и, кроме того, мне неясно, как исключить обработку ошибок с помощью операторов ловушки. Большая часть этого сокращения из моего предложения происходит в форме отказа от правильности обработки ошибок и, я считаю, возвращения к упрощению простого возврата ошибок напрямую, чем что-либо еще.

Возможность продолжить поток разрешена измененным примером trap который я привел выше . Я отредактировал его позже, поэтому не знаю, видели вы это или нет. Это очень похоже на collect , но я думаю, что это дает немного больше контроля над ним. В зависимости от того, как сработали правила определения объема работ, это могло быть немного спагетти, но я думаю, что можно было бы найти хороший баланс.

@thejerf А, в этом больше смысла. Я не понимал, что это был ответ на ваше предложение. Однако мне не ясно, в чем разница между erroring() и recover() , кроме того факта, что recover отвечает на panic . Похоже, мы бы просто неявно запаниковали, когда нужно было вернуть ошибку. Отсрочка также является довольно дорогостоящей операцией, поэтому я не уверен, как я отношусь к ее использованию во всех функциях, которые могут вызывать ошибки.

@DeedleFake То же самое касается trap , потому что, как я это вижу, trap по сути является макросом, который вставляет код при использовании оператора ? что представляет собой собственный набор проблем. и соображения, или это реализовано как goto ... что, что, если пользователь не вернется в блоке trap , или это просто синтаксически другой defer . Кроме того, что, если я объявлю несколько блоков ловушек в функции? Это разрешено? Если да, то какой из них будет казнен? Это добавляет сложности реализации. Го любит быть самоуверенным, и мне это нравится. Я думаю, что collect или аналогичная линейная конструкция больше соответствует идеологии Go, чем trap , которая, как мне было указано после моего первого предложения, кажется try-catch построить в костюме.

что, если пользователь не вернется в ловушку

Если trap не возвращает или иным образом не изменяет поток управления ( goto , continue , break и т. Д.), Поток управления возвращается туда, где код блок был "вызван" из. Сам блок будет работать аналогично вызову закрытия, за исключением того, что у него есть доступ к механизмам потока управления. Механизмы будут работать в том месте, где объявлен блок, а не в том месте, откуда он вызывается, поэтому

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

должно сработать.

Кроме того, что, если я объявлю несколько блоков ловушек в функции? Это разрешено? Если да, то какой из них будет казнен?

Да, это разрешено. Блоки названы ловушкой, поэтому довольно просто определить, какой из них следует вызвать. Например, в

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

блок 2 вызывается. Большой вопрос в этом случае, вероятно, будет заключаться в том, что происходит в случае n?, err? = 3, errors.New("Example") , что, вероятно, потребует указания порядка назначений, как это было в # 25609.

Я думаю, что сбор или подобная линейная конструкция больше соответствует идеологии Го, чем ловушка, которая, как мне было указано после моего первого предложения, кажется конструкцией «попытаться поймать» в костюме.

Я думаю, что и collect и trap по сути являются try-catch s в обратном порядке. Стандартный try-catch - это политика отказа по умолчанию, которая требует от вас проверки, иначе она взорвется. Это система «по умолчанию», которая позволяет вам, по сути, указать путь отказа.

Единственное, что усложняет все это, - это тот факт, что ошибки по сути не рассматриваются как сбой, а некоторые ошибки, такие как io.EOF , на самом деле вообще не указывают на сбой. Я думаю, что именно поэтому системы, которые не привязаны конкретно к ошибкам, такие как collect или trap , являются правильным решением.

"Ах, в этом больше смысла. Я не понимал, что это был ответ на ваше предложение. Однако мне не ясно, в чем будет разница между erroring () и recovery (), кроме того факта, что восстановление отвечает на паника."

Главное - не иметь большой разницы. Я пытаюсь свести к минимуму количество создаваемых новых концепций, получая от них как можно больше возможностей. Я считаю создание существующей функциональности функцией, а не ошибкой.

Один из пунктов моего предложения состоит в том, чтобы не ограничиваться только тем, «что, если мы исправим этот повторяющийся кусок из трех строк, где мы return err и заменим его на ? » на «как это повлияет на остальной язык? Какие новые шаблоны он позволяет? Какие новые «лучшие практики» он создает? Какие старые «лучшие практики» перестают быть лучшими практиками? » Я не говорю, что закончил эту работу. И даже если это будет оценено, эта идея на самом деле имеет слишком много возможностей для вкуса Go (поскольку Go не является языком, максимизирующим мощность, и даже с учетом выбора дизайна, ограничивающего его типом error он все равно, вероятно, самый мощный предложение, сделанное в этой ветке, которое я полностью имею в виду как в хорошем, так и в плохом смысле слова "мощный"), я думаю, мы могли бы исследовать вопросы о том, что новые конструкции будут делать с программами в целом, а не что они будет работать с семистрочными примерами функций, поэтому я попытался по крайней мере довести примеры до ~ 50-100 строк диапазона «реального кода». Все выглядит одинаково в 5 строках, включая обработку ошибок Go 1.0, что, возможно, является одной из причин, по которой мы все знаем по собственному опыту, что здесь существует настоящая проблема, но разговор идет по кругу, если мы говорим о в слишком маленьком масштабе, пока некоторые люди не начнут убеждать себя, что, может быть, в этом нет проблемы. (Доверяйте своему настоящему опыту программирования, а не пятистрочным образцам!)

«Похоже, мы бы просто неявно запаниковали, когда нужно вернуть ошибку».

Это не подразумевается. Это явно. Вы используете оператор pop когда он делает то, что вы хотите. Когда он не делает того, что вы хотите, вы не используете его. То, что он делает, достаточно просто уловить в одном простом предложении, хотя спецификация, вероятно, займет целый абзац, поскольку именно так работают такие вещи. Нет никакого скрытого. Кроме того, это не паника, потому что он раскручивает только один уровень стека, точно так же, как возврат; это такая же паника, как и возврат, чего нет вовсе.

Мне также все равно, если вы пишете pop как? или что-то еще. Я лично считаю, что слово выглядит немного более похожим на Go, поскольку Go в настоящее время не является языком, богатым символами, но не могу отрицать то преимущество символа, что он не конфликтует с каким-либо существующим исходным кодом. Меня интересует семантика и то, что мы можем на ней построить, и какое поведение новая семантика предоставляет программистам, как новым, так и опытным в большей степени, чем орфография.

«Отсрочка также является довольно дорогостоящей операцией, поэтому я не уверен, как я отношусь к ее использованию во всех функциях, которые могут вызывать ошибки».

Я уже признал это. Хотя я бы предположил, что в целом это не так уж и дорого, и я не слишком расстраиваюсь, говоря, что в целях оптимизации, если у вас есть горячая функция, напишите ее текущим способом. Я не ставлю перед собой цель попытаться изменить 100% всех функций обработки ошибок, но сделать 80% из них намного проще и правильнее и позволить 20% случаев (вероятно, больше похоже на 98/2, честно) оставаться такими, как они являются. Подавляющая часть кода Go не чувствительна к использованию defer, в конце концов, именно поэтому defer существует вообще.

Фактически, вы можете тривиально изменить предложение, чтобы не использовать defer и использовать какое-то ключевое слово, например trap в качестве объявления, которое запускается только один раз, независимо от того, где оно появляется, а не то, как defer на самом деле является оператором, который подталкивает обработчик в стек отложенных функций. Я намеренно решил повторно использовать defer чтобы избежать добавления новых концепций в язык ... даже для понимания ловушек, которые могут возникнуть в результате задержек в циклах, неожиданно укушающих людей. Но это все еще единственная концепция defer нужно понять.

Просто чтобы прояснить, что добавление нового ключевого слова в язык - критическое изменение.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

приводит к

prog.go:7:6: syntax error: unexpected select, expecting name or (

Это означает, что если мы попытаемся добавить try , trap , assert , любое ключевое слово в язык, мы рискуем сломать тонну кода. Код, который можно дольше поддерживать.

Вот почему я изначально предложил добавить специальный оператор ? go, который можно применять к переменным в контексте операторов. На данный момент символ ? обозначен как недопустимый для имен переменных. Это означает, что в настоящее время он не используется ни в одном текущем коде Go, и поэтому мы можем ввести его без каких-либо критических изменений.

Теперь проблема использования его в левой части присваивания заключается в том, что он не принимает во внимание, что Go допускает несколько возвращаемых аргументов.

Например, рассмотрим эту функцию

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
}

если мы используем? или попробуйте в левой части присваивания избавиться от блоков if err! = nil, мы автоматически предполагаем, что ошибки означают, что все другие значения теперь являются мусором? Что, если бы мы сделали это так

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

какие предположения мы здесь делаем? Что не должно быть вредно просто предположить, что выбросить ценность - это нормально? что, если ошибка должна быть скорее предупреждением, а значение x в порядке? Что, если единственная функция, которая вызывает ошибку, - это вызов GetZ (), а значения x, y действительно хороши? Мы можем позволить себе их вернуть. Что, если мы не будем использовать именованные возвращаемые аргументы? Что, если возвращаемые аргументы являются ссылочными типами, такими как карта или канал, должны ли мы предполагать, что возвращать nil вызывающей стороне безопасно?

TL; DR; добавление? или try к заданиям в попытке устранить

if err != nil{
    return err
}

вводит слишком много путаницы, чем льготы.

А добавление чего-то вроде предложения trap вводит возможность поломки.

Поэтому в своем предложении я сделал это отдельным выпуском. Я разрешил возможность объявлять func ?() bool для любого типа, чтобы при вызове говорилось

x, err := doSomething; return x, err?    

у вас может произойти побочный эффект ловушки, который применим к любому типу.

А подавать? работа только с операторами, как я показал, позволяет программировать операторы. В моем предложении я предлагаю разрешить специальный оператор switch, который позволяет кому-либо переключать случаи, которые являются ключевым словом +?

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

Если мы используем? для типа, который не имеет явного? объявлена ​​функция или встроенный тип, то поведение по умолчанию - проверка, если var == nil || нулевое значение {выполнить оператор} является предполагаемым намерением.

Идк, я не эксперт в дизайне языков программирования, но разве это не так?

Например, функция os.Chdir в настоящее время

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

В соответствии с этим предложением это можно было бы записать как

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

по сути то же самое, что и функции стрелок в javascript или, как Дарт определяет это, «синтаксис толстой стрелки»

например

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

из дартс-тура .

Для функций, содержащих только одно выражение, можно использовать сокращенный синтаксис:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
Синтаксис => expr является сокращением для {return expr; }. Обозначение => иногда называют синтаксисом жирной стрелки.

@mortdeus , левая часть стрелки Dart - это сигнатура функции, а syscall.Chdir(dir) - выражение. Они кажутся более или менее несвязанными.

@mortdeus Я забыл уточнить ранее, но идея, которую я здесь прокомментировал, вряд ли похожа на предложение, которое вы отметили. Мне нравится идея ? в качестве заполнителя, поэтому я скопировал это, но моя идея заключалась в повторном использовании одного блока кода для обработки ошибок, избегая при этом некоторых известных проблем с try...catch . Я был очень осторожен, придумав что-то, о чем раньше не говорили, чтобы внести новую идею.

Как насчет нового условного оператора return (или returnIf )?

return(bool expression) ...

т.е.

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

Или просто позвольте fmt форматировать однострочные функции возврата в одну строку вместо трех:

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 }

Мне приходит в голову, что я могу получить все, что хочу, просто добавив некоторого оператора, который возвращает раньше, если крайняя правая ошибка не равна нулю, если я объединю ее с именованными параметрами:

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

В настоящее время я понимаю, что консенсус Go противоречит именованным параметрам, но если возможности именных параметров меняются, то и консенсус может измениться. Конечно, если консенсус достаточно силен против этого, есть возможность включить вспомогательные средства.

Этот подход дает мне то, что я ищу (упрощая систематическую обработку ошибок в верхней части функции, возможность учитывать такой код, а также сокращение количества строк) практически с любыми другими предложениями здесь, включая исходный . И даже если сообщество Go решит, что ему это не нравится, мне не о чем беспокоиться, потому что он находится в моем коде отдельно для каждой функции и не имеет несоответствия импеданса ни в одном из направлений.

Хотя я бы предпочел предложение, которое позволяет использовать функцию сигнатуры func GetInt() (x int, err error) в коде с OtherFunc(GetInt()?, "...") (или каким бы то ни было окончательным результатом), тому, которое не может быть скомпоновано в выражение. Несмотря на меньшее раздражение постоянного повторяющегося предложения простой обработки ошибок, объем моего кода, который распаковывает функцию с арностью 2 только для того, чтобы получить первый результат, по-прежнему досадно существенен и на самом деле ничего не добавляет к ясности результирующий код.

@thejerf , мне кажется, здесь много странного поведения. Вы вызываете net.Listen , который возвращает ошибку, но не назначается. И затем вы откладываете, передавая err . Переопределяет ли каждый новый defer последний, так что annotateError никогда не вызывается? Или они складываются, так что если, скажем, toServer.Send возвращается ошибка, то дважды вызывается closeOnErr , а затем вызывается annotateError ? Вызывается ли closeOnErr только в том случае, если у предыдущего вызова есть соответствующая подпись? А что насчет этого дела?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

Читая код, он также сбивает с толку, например, почему я не могу просто сказать

client.session = communicationProtocol.FinalProtocol(conn)?

Предположительно, потому что FinalProtocol возвращает ошибку? Но это скрыто от читателя.

Наконец, что происходит, когда я хочу сообщить об ошибке и исправить ее в функции? Похоже, ваш пример предотвратит этот случай?

_Addendum_

Хорошо, я думаю, что когда вы хотите восстановиться после ошибки, по вашему примеру, вы назначаете ее, как в этой строке:

env, err := environment.GetRuntimeEnvironment()

Это нормально, потому что err затеняется, но если я изменил ...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

чтобы просто

forwardPort = env.PortToForward()

Тогда ваша отложенная обработанная ошибка не будет обнаружена, потому что вы используете err созданный в области видимости. Или я что-то упускаю?

Я считаю, что добавление к синтаксису, обозначающее, что функция может дать сбой, - хорошее начало. Я предлагаю что-то в этом роде:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

Если функция не работает (используя ключевое слово fail вместо return , она возвращает нулевое значение для каждого возвращаемого параметра.

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

Преимущество этого подхода заключается в том, что текущий код будет работать, но при этом будет задействован новый механизм для лучшей обработки ошибок (по крайней мере, в синтаксисе).

Комментарии по ключевым словам:

  • fails может быть не лучшим вариантом, но это лучшее, что я могу придумать на данный момент. Я думал об использовании err (или errs ), но то, как они используются в настоящее время, может сделать это плохим выбором из-за текущих ожиданий ( err , скорее всего, имя переменной, и errs можно считать срезом, массивом или ошибками).
  • handle может немного ввести в заблуждение. Я хотел использовать recover , но он используется для panic s ...

edit: Изменен вызов r.Read для соответствия io.Reader.Read() .

Частично причина этого предложения заключается в том, что текущий подход в Go не помогает инструментам понять, означает ли возвращаемое значение error сбой функции или оно возвращает значение ошибки как часть своей функции (например, github.com/pkg/errors ).

Я считаю, что включение функции явного выражения сбоя - это первый шаг к улучшению обработки ошибок.

@ibrasho , чем твой пример отличается от ...

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

... если мы дадим компилятору предупреждения или линт для необработанных экземпляров error ? Смена языка не требуется.

Две вещи:

  • Я думаю, что предложенный синтаксис читается и выглядит лучше. 😁
  • Моя версия требует дать функциям возможность явно указывать, что они не работают. В настоящее время в Go отсутствует то, что может позволить инструментам делать больше. Мы всегда можем рассматривать функцию, возвращающую значение error как неудачную, но это предположение. Что, если функция вернет 2 значения error ?

В моем предложении было кое-что, что я удалил, а именно автоматическое распространение:

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

Я удалил это, так как считаю, что обработка ошибок должна быть явной.

edit: Изменен вызов r.Read для соответствия io.Reader.Read() .

Итак, это будет действительная подпись или прототип?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Учитывая, что реализация io.Reader присваивает io.EOF когда больше нечего читать, и некоторая другая ошибка для условий сбоя.)

да. Но никто не должен ожидать, что err будет обозначать отказ функции, выполняющей свою работу. Ошибка чтения ошибки должна быть передана блоку дескриптора. Ошибки - это значения в Go, и значение, возвращаемое этой функцией, не должно иметь особого значения, кроме того, что эта функция возвращает (по какой-то странной причине).

Я предполагал, что в результате сбоя возвращаемые значения будут возвращены как нулевые. Текущий Reader.Read уже дает некоторые обещания, которые могут быть невозможны с этим новым подходом.

Когда Read обнаруживает ошибку или состояние конца файла после успешного чтения n> 0 байтов, он возвращает количество прочитанных байтов. Он может вернуть ошибку (отличную от nil) из того же вызова или вернуть ошибку (и n == 0) из последующего вызова. Примером этого общего случая является то, что Reader, возвращающий ненулевое количество байтов в конце входного потока, может возвращать либо err == EOF, либо err == nil. Следующее чтение должно вернуть 0, EOF.

Вызывающие должны всегда обрабатывать возвращенные n> 0 байтов, прежде чем рассматривать ошибку err. Это правильно обрабатывает ошибки ввода-вывода, которые возникают после чтения некоторых байтов, а также оба допустимых поведения EOF.

Реализациям Read не рекомендуется возвращать нулевое количество байтов с ошибкой nil, за исключением случаев, когда len (p) == 0. Вызывающие должны рассматривать возврат 0 и nil как указание на то, что ничего не произошло; в частности, он не указывает EOF.

Не все такое поведение возможно в предлагаемом в настоящее время подходе. В текущем контракте интерфейса чтения я вижу некоторые недостатки, например, как обрабатывать частичное чтение.

В общем, как должна вести себя функция, когда она частично выполняется к моменту выхода из строя? Я, честно говоря, еще не думал об этом.

Случай с io.EOF прост:

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 , мне кажется, здесь много странного поведения. Вы вызываете net.Listen, который возвращает ошибку, но не назначается.

Я использую общий оператор ? предложенный несколькими людьми, чтобы указать возврат ошибки с нулевыми значениями для других значений, если ошибка не равна нулю. Я немного предпочитаю короткое слово оператору, так как я не думаю, что Go - это тяжелый для операторов язык, но если бы ? перешел на язык, я бы все равно подсчитал свой выигрыш, а не топнул ногой. раздражение.

Переопределяет ли каждый новый defer последний, так что annotateError никогда не вызывается? Или они складываются, так что если ошибка возвращается, скажем, от toServer.Send, то closeOnErr вызывается дважды, а затем вызывается annotateError?

Он работает так же, как и сейчас defer: https://play.golang.org/p/F0xgP4h5Vxf Я ожидал, что этот пост будет упущен, и в моем запланированном ответе я хотел указать, что это _ уже_ как работает defer, и вы бы просто принижать текущее поведение Go, но ничего не получилось. Увы. Как видно из этого фрагмента, затенение не является проблемой или, по крайней мере, не является более серьезной проблемой, чем уже есть. (Это ни исправит, ни усугубит ситуацию.)

Я думаю, что один аспект, который может сбивать с толку, заключается в том, что в текущем Go уже есть случай, когда именованный параметр в конечном итоге будет «все, что действительно возвращается», поэтому вы можете сделать то, что я сделал, взять указатель на него и передать это отложенной функции и манипулировать ею, независимо от того, возвращаете ли вы напрямую такое значение, как return errors.New(...) , которое интуитивно может показаться «новой переменной», которая не является именованной переменной, но на самом деле Go в конечном итоге окажется с присвоением именованной переменной к моменту выполнения defer. Сейчас легко проигнорировать эту конкретную деталь текущего Go. Я утверждаю, что, хотя сейчас это может сбивать с толку, если бы вы работали даже с кодовой базой, в которой использовалась эта идиома (т. Е. Я не говорю, что это даже должно стать «лучшей практикой Go», подойдет всего несколько разоблачений), вы » буду разбираться в этом довольно быстро. Потому что, просто скажу это еще раз, чтобы быть предельно ясным, Go уже работает, а не предлагаемое изменение.

Вот предложение, которое, я думаю, раньше не предлагалось. На примере:

 r, !handleError := something()

Смысл этого такой же:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(где _xyzzy - это новая переменная, область действия которой распространяется только на эти две строки кода, а R может иметь несколько значений).

Преимущества этого предложения не относятся к ошибкам, не обрабатывают нулевые значения специально, и легко кратко указать, как обернуть ошибки внутри любого конкретного блока кода. Изменения синтаксиса небольшие. Учитывая простой перевод, легко понять, как работает эта функция.

Недостатки заключаются в том, что он вводит неявный возврат, нельзя написать общий обработчик, который просто возвращает ошибку (поскольку его возвращаемые значения должны быть основаны на функции, из которой он вызван), и что значение, которое передается обработчику, равно недоступно в телефонном коде.

Вот как это можно использовать:

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
}

Или вы можете использовать его в тесте для вызова t.Fatal, если ваш код инициализации не работает:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

Я бы предложил изменить сигнатуру функции только на func(error) error . Это упрощает большинство случаев, и если вам нужно дополнительно проанализировать ошибку, просто используйте текущий механизм.

Вопрос о синтаксисе: можете ли вы определить функцию встроенной?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

Меня устраивает фраза «не делай этого», но синтаксис, вероятно, должен позволять уменьшить количество особых случаев. Это также позволило бы:

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
}

Что кажется мне одним из лучших предложений для такого рода вещей, по крайней мере, с точки зрения того, как изложить это кратко. Это еще один случай, когда, IMHO, вы выдаете больше ошибок на этом этапе, чем код на основе исключений, который часто не может получить подробную информацию о природе ошибки, потому что так легко просто позволить исключениям распространяться.

Я также предлагаю по соображениям производительности, чтобы функции ошибок взрыва определялись как не вызываемые при нулевых значениях. Это позволяет минимизировать их влияние на производительность; в случае, который я только что показал, если чтение проходит нормально, это не дороже, чем текущая реализация чтения, которая уже составляет if ing при каждой ошибке и не соответствует предложению if . Если мы вызываем функцию на nil все время, это станет очень дорогостоящим, если она не может быть встроена, что в конечном итоге будет нетривиальным количеством времени. (Если ошибка активно возникает, мы, вероятно, можем оправдать и позволить вызов функции практически при любых обстоятельствах (если вы не можете вернуться к текущему методу), но на самом деле не хотим, чтобы это происходило без ошибок.) Это также означает, что функции взрыва могут принимать в своей реализации ненулевое значение, что тоже их упрощает.

@thejerf хороший, но счастливый путь сильно изменился.
много сообщений назад предлагалось иметь что-то вроде Ruby или синтаксиса - f := OpenFile(filename) or failed("couldn't open file") .

Дополнительное беспокойство - это для любого типа параметров или только для ошибок? если только для ошибок - тогда ошибка типа должна иметь особое значение для компилятора.

@thejerf хороший, но счастливый путь сильно изменился.

Я бы порекомендовал различать, вероятно, общий путь исходного предложения, где он выглядит как исходное предложение Паулханкина:

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
}

возможно, даже с учетом того, что herr где-то вычеркнут, и моих исследований того, что потребуется для его полного определения, что является необходимостью для этого разговора, и моих собственных размышлений о том, как я мог бы использовать это в своем личном коде, это просто исследование того, что еще разрешается и предоставляется предложением. Я уже сказал, что буквально встраивание функции, вероятно, в конце концов - плохая идея, но грамматика, вероятно, должна позволять ей сохранять грамматику простой. Я уже могу написать функцию Go, которая принимает три функции, и встроить их все прямо в вызов. Это не означает, что Go сломан или что Go нужно что-то сделать, чтобы предотвратить это; это означает, что я не должен этого делать, если я ценю ясность кода. Мне нравится, что Go обеспечивает ясность кода, но разработчики все еще несут некоторую неснижаемую ответственность за то, чтобы код оставался чистым.

Если вы собираетесь сказать мне, что "счастливый путь" для

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
}

запутан и труден для чтения, но счастливый путь легко прочитать с

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
}

то я предлагаю единственный возможный способ, которым вторую легче читать, - это то, что вы уже привыкли ее читать, и ваши глаза обучены переходить именно по правильному пути, чтобы увидеть счастливый путь. Я могу догадаться об этом, потому что мои тоже. Но как только вы привыкнете к альтернативному синтаксису, вы тоже научитесь этому. Вы должны анализировать синтаксис, основываясь на том, как вы себя чувствуете, когда вы к нему уже привыкли, а не на том, что вы чувствуете сейчас.

Я также отмечу, что добавленные символы новой строки во втором примере представляют то, что происходит с моим настоящим кодом. Текущая обработка ошибок Go не только добавляет в код «строчки» кода, но и добавляет множество «абзацев» к тому, что в противном случае должно было бы быть очень простой функцией. Я хочу открыть файл, прочитать несколько байтов и обработать их. Я не хочу

Откройте файл.

А затем прочтите несколько байтов, если это нормально.

А затем обработайте их, если это нормально.

Я чувствую, что есть много отрицательных голосов, которые сводятся к тому, что «это не то, к чему я привык», а не к фактическому анализу того, как они будут воспроизводиться в реальном коде, когда вы привыкнете к ним и будете использовать их свободно. .

Мне не нравится идея скрытия оператора возврата, я предпочитаю:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

В этом случае or - это нулевой условный оператор переадресации,
пересылка последнего возврата, если он не равен нулю.
В C # есть аналогичное предложение с использованием оператора ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf «счастливый путь» в вашем случае добавлен к вызову failed (...), что может быть очень длинным. также звучит как йода: rofl:

Символы @rodcorsi, введенные с помощью клавиши "shift", плохие ИМХО - (если у вас несколько раскладок клавиатуры)

Пожалуйста, не усложняйте этот путь, чем он есть сейчас. На самом деле перемещение одних и тех же кодов в одну строку (вместо 3 или более) на самом деле не является решением. Я лично не считаю ни одно из этих предложений настолько жизнеспособным. Ребята, математика очень простая. Либо примите идею «Попробуй поймать», либо оставьте вещи такими, какие они есть сейчас, что означает много «if then else» и шумов в коде, и это не совсем подходит для использования в шаблонах OO, таких как Fluent Interface.

Большое спасибо за все ваши раздачи и, возможно, несколько раздач ;-) (шучу)

@KamyarM ИМО, «использовать наиболее известную альтернативу или вообще не вносить изменений» - не очень продуктивное утверждение. Это останавливает инновации и способствует аргументам по кругу.

@KernelDeimos Я согласен с вами, но я вижу много комментариев в этой ветке, которые по сути защищали старую
Я предлагал что-то подобное раньше, и это не совсем java или C # try-catch, и я думаю, что он может поддерживать текущую обработку ошибок и библиотеки, и я использую один из примеров, приведенных выше. Таким образом, в основном компилятор проверяет наличие ошибок после каждой строки и переходит к блоку catch, если значение err не равно нулю:

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
В вашем примере, как мне узнать (во время написания кода), какой метод вернул ошибку? Как выполнить третий способ обработки ошибки («вернуть ошибку с дополнительной контекстной информацией»)?

@urandom
один из способов - использовать переключатель Go и найти тип исключения в catch. Допустим, у вас есть исключение pathError, которое, как вы знаете, вызвано OpenFile () таким образом. Другой способ, который не сильно отличается от текущей обработки ошибок if err! = Nil в GoLang, заключается в следующем:

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

Таким образом, у вас есть возможности, но вы не ограничены. Если вы действительно хотите точно знать, какая строка вызвала проблему, вы помещаете каждую строку в try catch так же, как вы прямо сейчас пишете множество if-then-elses. Если ошибка не важна для вас, и вы хотите передать ее методу вызывающего абонента, который в примерах, обсуждаемых в этом потоке, на самом деле об этом, я думаю, что предложенный мной код просто выполняет свою работу.

@KamyarM Я вижу, откуда вы сейчас идете. Я бы сказал, что если так много людей против попытки ... ловить, я рассматриваю это как доказательство того, что попытка ... ловушка не идеальна и имеет недостатки. Это простое решение проблемы, но если Go2 может улучшить обработку ошибок, чем то, что мы видели на других языках, я думаю, это было бы действительно круто.

Я думаю, что можно брать то, что хорошо в try ... catch, не принимая того, что плохого в try ... catch, что я предлагал ранее. Я согласен, что превращение трех строк в одну или две ничего не решает.

Основная проблема, на мой взгляд, заключается в том, что код обработки ошибок в функции повторяется, если частью логики является «возврат к вызывающему». Если вы хотите изменить логику в любой момент, вы должны изменить каждый экземпляр if err != nil { return nil } .

Тем не менее, мне действительно нравится идея try...catch если функции не могут ничего throw неявно.

Еще я думаю, что было бы полезно, если бы логика в catch {} требовала ключевого слова break чтобы прервать поток управления. Иногда вы хотите обработать ошибку, не прерывая поток управления. (например: «для каждого элемента этих данных сделайте что-нибудь, добавьте в список ненулевую ошибку и продолжите»)

@KernelDeimos Я полностью согласен с этим. Я видел точную такую ​​ситуацию. Вам нужно как можно чаще фиксировать ошибки, прежде чем нарушать коды. Если что-то вроде каналов можно было бы использовать в тех ситуациях в GoLang, что было бы хорошо. Тогда вы могли бы отправить все ошибки на этот канал, который ожидает catch, а затем catch мог бы обработать их одну за другой.

Я бы предпочел смешать «или вернуть» с # 19642, # 21498, чем использовать try..catch (defer / panic / recovery уже существует; бросание внутри одной функции похоже на использование нескольких операторов goto и стало беспорядочным с дополнительным переключателем типа внутри catch; позволяет забыть обработку ошибок, имея try..catch высоко в стеке (или значительно усложнить компилятор, если область видимости try..catch внутри одной функции)

@egorse
Кажется, синтаксис try-catch, который предлагает

При этом @KamyarM , почему в try есть часть определения переменной? Вы определяете переменную err , но она затеняется другими переменными err внутри самого блока. Какова его цель?

Я думаю, это указать ему, за какой переменной следует следить, что позволяет отделить ее от типа error . Вероятно, это потребует изменения правил слежки, если только вам не нужно, чтобы люди были очень осторожны с этим. Однако я не уверен в объявлении в блоке catch .

@egorse Именно то, что упомянул @DeedleFake, является целью. Это означает, что блок try наблюдает за этим объектом. Также это ограничивает его область применения. Это что-то похожее на оператор using в C #. В C # объекты, которые определены с помощью ключевого слова using, автоматически удаляются после выполнения этого блока, а область действия этих объектов ограничивается блоком «Использование».
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Использование уловки необходимо, потому что мы хотим заставить программиста решить, как правильно обрабатывать ошибку. В C # и Java отловка также обязательна. в C #, если вы не хотите обрабатывать исключение, вы вообще не используете try-catch в этой функции. Когда возникает исключение, любой метод в иерархии вызовов может обработать исключение или даже повторно выбросить (или заключить его в другое исключение) снова. Не думаю, что на Java можно сделать то же самое. В Java метод, который может вызвать исключение, должен объявить его в сигнатуре функции.

Хочу подчеркнуть, что этот блок try-catch не совсем тот. Я использовал эти ключевые слова, потому что это похоже на то, чего он хочет достичь, а также это то, с чем многие программисты знакомы и их учат на большинстве курсов по концепциям программирования.

Может быть присвоение _return on error_, которое работает, только если есть _ named error return parameter_, как в:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

Если err не равно nil вернитесь.

Я думаю, что обсуждение добавления сахара try в Rust проливает свет на участников этого обсуждения.

FWIW, старая мысль об упрощении обработки ошибок (извиняюсь, если это ерунда):

Идентификатор повышения , обозначенный символом вставки ^ , может использоваться как один из операндов в левой части присваивания. Для целей присвоения идентификатор повышения является псевдонимом для последнего возвращаемого значения содержащей функцию, независимо от того, имеет ли значение имя или нет. После завершения присваивания функция проверяет последнее возвращаемое значение на соответствие нулевому значению своего типа (nil, 0, false, ""). Если он считается нулевым, функция продолжает выполняться, в противном случае - возвращается.

Основная цель идентификатора повышения - кратко передавать ошибки от вызываемых функций обратно вызывающей стороне в данном контексте, не скрывая того факта, что это происходит.

В качестве примера рассмотрим следующий код:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

Это примерно эквивалентно:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

Программа имеет неправильный формат, если:

  • идентификатор повышения используется более одного раза в задании
  • функция не возвращает значение
  • тип последнего возвращаемого значения не имеет значимого и эффективного теста на ноль

Это предложение похоже на другие в том, что оно не решает проблему предоставления большей контекстной информации, чего бы это ни стоило.

@gboyle Вот почему последнее возвращаемое значение IMO должно быть названо и иметь тип error . Это имеет два важных вывода:

1 - названы и другие возвращаемые значения, следовательно
2 - у них уже есть значимые нулевые значения.

@ object88 Как context , для этого нужны некоторые действия со стороны основной команды, например определение встроенного типа error (просто обычного Go error ) с некоторыми общими атрибутами (сообщение? стек вызовов и т. д.).

Насколько я знаю, в Go не так много контекстных языковых конструкций. Кроме go и defer нет других, и даже эти два очень ясны и ясны (бот в синтаксисе - и на первый взгляд - и семантике).

Что насчет этого?

(скопировал реальный код, над которым я работаю):

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
}

Когда err равно нулю, он прерывается до конца оператора catch. Вы можете использовать «catch» для группировки похожих вызовов, которые обычно возвращают один и тот же тип ошибки. Даже если вызовы не были связаны между собой, вы могли впоследствии проверить типы ошибок и соответствующим образом обернуть их.

@lukescott прочитайте это сообщение в блоге @robpike https://blog.golang.org/errors-are-values

@davecheney Идея улова (без попытки) соответствует духу этого настроения. Он рассматривает ошибку как значение. Он просто прерывается (в той же функции), когда значение больше не равно нулю. Это ни в коем случае не приводит к сбою программы.

@lukescott, вы можете использовать технику Роба сегодня, вам не нужно менять язык.

Между исключениями и ошибками существует довольно большая разница:

  • ожидаются ошибки (мы можем написать для них тест),
  • исключений не ожидается (отсюда и «исключение»),

Многие языки рассматривают и то, и другое как исключения.

Между дженериками и улучшенной обработкой ошибок я бы выбрал лучшую обработку ошибок, поскольку большая часть беспорядка кода в Go возникает из-за обработки ошибок. Хотя можно сказать, что такая многословность хороша и способствует простоте, IMO также скрывает «счастливый путь» рабочего процесса до уровня неоднозначности.

Я хотел бы немного развить предложение @thejerf .

Во-первых, вместо ! вводится оператор or , который сдвигает последний возвращаемый аргумент из вызова функции слева и вызывает оператор возврата справа, выражение которого функция, которая вызывается, если сдвинутый аргумент не равен нулю (не-nill для типов ошибок), передавая ей этот аргумент. Это нормально, если люди думают, что это должно быть только для типов ошибок, хотя я считаю, что эта конструкция будет полезна для функций, которые также возвращают логическое значение в качестве последнего аргумента (что-то нормально / не нормально).

Метод Read будет выглядеть так:

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
}

Я предполагаю, что пакет ошибок предоставляет следующие удобные функции:

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

Это выглядит прекрасно читаемым, охватывая все необходимые моменты, и это также не выглядит слишком чуждым из-за знакомого оператора return.

@urandom В этом утверждении f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) как можно узнать причину? Например, это отсутствие разрешения на чтение или файл вообще не существует?

@ dc0d
Что ж, даже в приведенном выше примере включена исходная ошибка, так как сообщение, данное пользователем, просто добавлено в контекст. Как указано и вытекает из исходного предложения, or return ожидает функцию, которая получает единственный параметр смещенного типа. Это ключевой момент, который позволяет использовать не только служебные функции, которые подойдут довольно большому количеству людей, но вы можете в значительной степени написать свои собственные, если вам нужна действительно индивидуальная обработка определенных значений.

@urandom ИМО, он слишком много скрывает.

Мои 2 цента здесь, я хотел бы предложить простое правило:

"параметр ошибки неявного результата для функций"

Для любой функции параметр ошибки подразумевается в конце списка параметров результата.
если не определено явно.

Предположим, что у нас есть функция, определенная следующим образом для обсуждения:

func f () (int) {}
что идентично: func f () (int, error) {}
согласно нашему правилу ошибок неявного результата.

для присвоения вы можете всплывать, игнорировать или перехватывать ошибку следующим образом:

1) bubble up (пузыриться)

х: = f ()

если f возвращает ошибку, текущая функция немедленно вернется с ошибкой
(или создать новый стек ошибок?)
если текущая функция является основной, программа остановится.

Это эквивалентно следующему фрагменту кода:

x, ошибка: = f ()
if err! = nil {
вернуться ..., эээ
}

2) ignore (игнорировать)

х, _: = f ()

пустой идентификатор в конце списка выражений присваивания, чтобы явно сигнализировать об отказе от ошибки.

3) catch (поймать)

x, ошибка: = f ()

err нужно обрабатывать как обычно.

Я считаю, что это идиоматическое изменение соглашения о коде должно требовать только минимальных изменений в компиляторе.
или препроцессор должен сделать эту работу.

@ dc0d Можете привести пример, что он скрывает и как?

@urandom Это причина, по которой f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . Исходная ошибка, возвращаемая OpenFile() - это может быть что-то вроде отсутствия прав на чтение или отсутствие файла, а не просто «что-то не так с именем файла».

@ dc0d
Я не согласен. Это примерно так же ясно, как иметь дело с http.Handlers, где позже вы передаете их некоторому мультиплексору, и внезапно вы получаете запрос и писатель ответа. И люди привыкли к такому поведению. Как люди узнают, что делает инструкция go ? Это явно неясно при первой встрече, но все же довольно широко распространено и присутствует в языке.

Я не думаю, что мы должны выступать против любого предложения на том основании, что оно новое и никто не знает, как оно работает, потому что это верно для большинства из них.

@urandom Теперь это имеет немного больше смысла (включая пример http.Handler ).

И мы что-то обсуждаем. Я не выступаю против или за какую-либо конкретную идею. Но я поддерживаю простоту и ясность, и в то же время передаю немного здравого смысла в отношении опыта разработчиков.

@ dc0d

что может быть что-то вроде отсутствия разрешения на чтение или отсутствия файла

В этом случае вы не будете просто повторно выдавать ошибку, а проверить ее фактическое содержимое. Для меня этот выпуск касается самого популярного случая. То есть повторная выдача ошибки с добавленным контекстом. Только в более редких случаях вы конвертируете ошибку в какой-то конкретный тип и проверяете, что она на самом деле говорит. И для этого текущего синтаксиса обработки ошибок все в порядке и никуда не денется, даже если одно из предложенных здесь предложений будет принято.

@creker Ошибки не являются исключением (мой предыдущий комментарий). Ошибки - это значения, поэтому их невозможно выбросить или повторно выбросить. Для сценариев, похожих на попытку / уловку, в Go есть паника / восстановление.

@ dc0d Я не говорю об исключениях. Под повторным выбросом я подразумеваю возвращение вызывающей стороне ошибки. Предлагаемый or return errors.Contextf("opening file %s", filename) основном оборачивает и повторно выдает ошибку.

@creker Спасибо за объяснение. Он также добавляет некоторые дополнительные вызовы функций, которые влияют на планировщик, что, в свою очередь, может не привести к желаемому поведению в некоторых ситуациях.

@ dc0d - это деталь реализации, которая может измениться в будущем. И это действительно может измениться, сейчас в разработке некооперативное приоритетное обслуживание.

@creker
Я думаю, вы можете охватить даже больше случаев, чем просто возврат измененной ошибки:

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
Дополнительные вызовы функций, вероятно, будут встроены компилятором

@urandom, это выглядит очень интересно. Немного волшебно с неявным аргументом, но на самом деле он может быть достаточно общим и кратким, чтобы охватить все. Только в очень редких случаях вам придется прибегать к обычным if err != nil

@urandom , меня смущает твой пример. Почему retryReadErrHandler возвращает функцию?

@ object88
Это идея оператора or return . Он ожидает функцию, которую он вызовет в случае ненулевого последнего возвращенного аргумента с левой стороны. В этом отношении он действует точно так же, как http.Handler, оставляя фактическую логику обработки аргумента и его возврата (или запроса и его ответа в случае обработчика) обратному вызову. А чтобы использовать свои собственные данные в обратном вызове, вы создаете функцию-оболочку, которая принимает эти данные в качестве параметров и возвращает то, что ожидается.

Или, говоря более привычным языком, это похоже на то, что мы обычно делаем с обработчиками:
`` идти
func nodesHandler (репо Репо) http.Handler {
return http.HandlerFunc (func (w http.ResponseWriter, r * http.Request) {
данные, _: = json.Marshal (repo.GetNodes ())
w.Write (данные)
})
}

@urandom , вы можете избежать магии, оставив LHS таким же, как сегодня, и изменив or ... return на returnif (cond) :

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
}

Это улучшает универсальность и прозрачность значений ошибок слева и условия запуска справа.

Чем больше я вижу эти разные предложения, тем больше я склоняюсь к изменению, касающемуся только gofmt. У языка уже есть возможности, давайте просто сделаем его более читабельным. @billyh , я не returnif(cond) ... - это просто способ переписать if cond { return ...} . Почему мы не можем просто написать последнее? Мы уже знаем, что это значит.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

или даже

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

или

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

Никаких новых волшебных ключевых слов, синтаксиса или операторов.

(Это может помочь, если мы также исправим # 377, чтобы добавить гибкости в использование := .)

@ randall77 Я

@ randall77 В какой момент этот блок будет перенесен на строку?

Приведенное выше решение более приемлемо по сравнению с предлагаемыми здесь альтернативами, но я не уверен, что это лучше, чем бездействие. Gofmt должен быть максимально детерминированным.

@as , я не до конца продумал это, но, возможно, «если тело оператора if содержит единственный оператор return , тогда оператор if форматируется как одна строка ".

Возможно, требуется дополнительное ограничение на условие if , например, это должна быть логическая переменная или бинарный оператор двух переменных или констант.

@billyh
Я не вижу необходимости делать его более подробным, поскольку я не вижу ничего запутанного в этой маленькой магии в or . Я предполагаю, что, в отличие от @as , многие люди не находят ничего запутанного в том, как мы работаем с обработчиками http.

@ randall77
То, что вы предлагаете, больше соответствует предложению стиля кода, и это то, куда идти, очень самоуверенно. Это может не понравиться сообществу в целом, если вдруг появятся 2 стиля форматирования операторов if.

Не говоря уже о том, что такие лайнеры гораздо труднее читать. if != ; { } слишком много даже в несколько строк, отсюда и данное предложение. Шаблон фиксирован почти для всех случаев и может быть превращен в синтаксический сахар, который легко читать и понимать.

Проблема, с которой я сталкиваюсь с большинством этих предложений, заключается в том, что непонятно, что происходит. В вводном сообщении предлагается повторно использовать || для возврата ошибки. Мне непонятно, происходит ли там возврат. Я думаю, что если будет изобретен новый синтаксис, он должен соответствовать ожиданиям большинства людей. Когда я вижу || я не ожидаю возврата или даже остановки выполнения. Меня это раздражает.

Мне нравится высказывание Go «ошибки - это ценности», но я также согласен с тем, что if err := expression; err != nil { return err } слишком многословен, главным образом потому, что почти каждый вызов должен возвращать ошибку. Это означает, что у вас их будет много, и их легко испортить, в зависимости от того, где объявлена ​​(или затенена) ошибка. Это случилось с нашим кодом.

Поскольку Go не использует try / catch и использует panic / defer для «исключительных» обстоятельств, у нас может быть возможность повторно использовать ключевые слова try и / или catch для сокращения обработки ошибок без сбоя программы.

Вот такая мысль у меня была:

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
}

Мысль в том, что вы ставите префикс err на LHS с ключевым словом try . Если err не равно нулю, возврат происходит немедленно. Вы не должны использовать здесь уловку, если только возврат не полностью удовлетворен. Это больше соответствует ожиданиям людей о том, что «попытка прерывает выполнение», но вместо того, чтобы приводить к сбою программы, она просто возвращается.

Если возврат не полностью удовлетворен (проверка времени компиляции) или мы хотим обернуть ошибку, мы могли бы использовать catch как специальную метку только для пересылки, например так:

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

Это также позволяет вам проверять и игнорировать определенные ошибки:

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

Возможно, вы могли бы даже указать в try метку:

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

Я понимаю, что мои примеры кода - отстой (foo / bar, ack). Но, надеюсь, я проиллюстрировал, что я имею в виду, идя с / против существующих ожиданий. Я также был бы полностью согласен с сохранением ошибок в том виде, в каком они есть в Go 1. Но если изобретен новый синтаксис, нужно тщательно подумать о том, как этот синтаксис уже воспринимается, а не только в Go. Трудно изобрести новый синтаксис без того, чтобы он уже что-то значил, поэтому часто лучше руководствоваться существующими ожиданиями, чем противоречить им.

Может быть, какая-то цепочка, например, как вы можете объединить методы, но для ошибок? Я не совсем уверен, как это будет выглядеть и сработает ли это вообще, просто дикая идея.
Теперь вы можете отсортировать цепочку, чтобы уменьшить количество проверок ошибок, поддерживая какое-то значение ошибки в структуре и извлекая его в конце цепочки.

Это очень любопытная ситуация, потому что, хотя есть немного шаблонов, я не совсем уверен, как их еще больше упростить, оставив при этом смысл.

Пример кода @thejerf выглядит так с предложением @lukescott :

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
}

Он уменьшился с 59 строк до 47.

Это та же длина, но я думаю, что это немного понятнее, чем использование 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
}

Этот пример, вероятно, было бы проще выполнить с помощью deferifnotnil или чего-то в этом роде.
Но это своего рода относится ко всей одной строчке, о которой говорится во многих из этих предложений.

Немного поиграв с примером кода, я теперь против варианта try(label) name . Я думаю, что если у вас есть несколько интересных дел, просто используйте текущую систему if err != nil { ... } . Если вы в основном делаете то же самое, например, настраиваете собственное сообщение об ошибке, вы можете сделать это:

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

Если кто-то использовал Ruby, это очень похоже на их синтаксис rescue , который, на мой взгляд, читается достаточно хорошо.

Одна вещь, которую можно сделать, - сделать nil ложным значением, а для других значений получить истинное значение, в результате чего вы получите:

err := doSomething()
if err { return err }

Но я не уверен, что это действительно сработает, и это сбривает только несколько персонажей.
Я сделал много опечаток, но не думаю, что когда-либо делал опечатки в != nil .

О том, как сделать интерфейсы истинными / ложными, уже упоминалось ранее, и я сказал, что это сделает ошибки с флагами более распространенными:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , в приведенном выше примере сообщения об ошибках перемежаются кодом счастливого пути, что для меня немного странно. Если вам нужно отформатировать эти строки, вы делаете много дополнительной работы, независимо от того, идет ли что-то не так:

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 , я думаю, что анализ SSA должен иметь возможность выяснить, не используются ли определенные назначения, и изменить их так, чтобы они не выполнялись, если они не нужны (слишком оптимистично?). Если это правда, это должно быть эффективным:

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

Было бы это законно?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

Я думаю, что это должно продолжаться до тех пор, пока doThing() вернет nil, но я мог быть уверен в обратном.

@carlmjohnson

Немного поиграв с примером кода, я теперь против варианта имени try (label).

Да, я не был уверен в синтаксисе. Мне это не нравится, потому что из-за этого попытка выглядит как вызов функции. Но я мог видеть ценность указания другого ярлыка.

Было бы это законно?

Я бы сказал «да», потому что «пытаться» должно быть только вперед. Если бы вы хотели это сделать, я бы сказал, что вам нужно сделать это так:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

Или вот так:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

Одна вещь, которую можно сделать, - это сделать nil ложным значением, а для других значений - истиной, так что вы получите: err := doSomething() if err { return err }

Я думаю, что есть смысл в сокращении. Однако я не думаю, что это должно применяться к nil во всех ситуациях. Возможно, появится новый интерфейс вроде этого:

interface Truthy {
  True() bool
}

Затем любое значение, реализующее этот интерфейс, можно использовать, как вы предложили.

Это будет работать до тех пор, пока ошибка реализует интерфейс:

err := doSomething()
if err { return err }

Но это не сработает:

err := doSomething()
if err == true { return err } // err is not true

Я действительно новичок в golang, но как вы относитесь к введению условного делегатора, как показано ниже?

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
}

делегатор работает как материал, но

  • Его return означает return from its caller context . (тип возврата должен быть таким же, как у вызывающего)
  • Опционально требуется if перед { , в приведенном выше примере это if err != nil .
  • Он должен быть делегирован вызывающим с ключевым словом delegate

Можно было бы опустить delegate для делегирования, но я думаю, что это затрудняет чтение потока функции.

И, возможно, это полезно не только для обработки ошибок, хотя сейчас я не уверен.

Добавить проверку - это прекрасно, но вы можете сделать еще что-нибудь, прежде чем вернуться:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

становится

result, err := openFile(f);
check err

`` Иди
результат, ошибка: = openFile (f);
check err {
log.Println (..., ошибка)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

`` Иди
result, err: = check openFile (f) {
log.Println (..., ошибка)
}

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

Он также пытается упростить (некоторые считают утомительным) обработку ошибок (# 21161). Это стало бы:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Конечно, вы можете использовать try и другие ключевые слова вместо check , если это понятнее.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

`` Иди
результат, ошибка: = openFile (f);
try err {
// обрабатываем ошибку и возвращаем
log.Println (..., ошибка)
}

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
}

становится

check UpdateDB()

Для функции с несколькими возвращаемыми значениями вам нужно будет назначить, как мы это делаем сейчас.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

становится

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

// use a and b

2-я форма (отметьте А, Б)
check A, B оценивает A. Если nil, он ничего не делает. Если не nil, проверка действует как возврат {} *, Б.

Это для устранения ошибок. Мы по-прежнему проверяем A, но в неявном возврате используется B.

Пример

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

становится

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

Примечания
Ошибка компиляции

используйте оператор проверки для вещей, которые не оцениваются как ошибка
используйте проверку в функции с возвращаемыми значениями не в форме {type} *, ошибка
Проверка формы с двумя выражениями A, B замкнута накоротко. B не оценивается, если A равно нулю.

Примечания по практичности
Есть поддержка декорирования ошибок, но вы платите за более грубую проверку синтаксиса A, B только тогда, когда вам действительно нужно украсить ошибки.

Для шаблона if err != nil { return nil, nil, err } (который очень распространен) ошибка проверки настолько коротка, насколько это возможно без ущерба для ясности (см. Примечание к синтаксису ниже).

Примечания по синтаксису
Я бы сказал, что такой синтаксис (проверка ... в начале строки, аналогичный возврату) - хороший способ устранить шаблон проверки ошибок, не скрывая нарушения потока управления, вызываемого неявными возвратами.

Обратной стороной таких идей, как||ипойматьвыше, или a, b = foo ()? предложенный в другом потоке, заключается в том, что они скрывают модификацию потока управления таким образом, что затрудняет отслеживание потока; бывший с ||машины, добавленные в конце простой на вид строки, последняя с маленьким символом, который может появляться везде, в том числе в середине и в конце простой на вид строки кода, возможно, несколько раз.

Оператор проверки всегда будет на верхнем уровне в текущем блоке, имея такую ​​же известность, как другие операторы, которые изменяют поток управления (например, ранний возврат).

Вот еще одна мысль.

Представьте себе оператор again который определяет макрос с меткой. Оператор, который он помечает, может быть снова расширен текстовой заменой позже в функции (напоминает const / iota с оттенками goto: -]).

Например:

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
}

будет в точности эквивалентно:

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
}

Обратите внимание, что у расширения макроса нет аргументов - это означает, что должно быть меньше путаницы по поводу того факта, что это макрос, потому что компилятор не любит символы сами по себе .

Как и в случае с оператором goto, область видимости метки находится в пределах текущей функции.

Интересная идея. Мне понравилась идея метки catch, но я не думаю, что она хорошо подходит для областей видимости Go (с текущим Go вы не можете goto метку с новыми переменными, определенными в ее области действия). Идея again устраняет эту проблему, потому что метка появляется до того, как вводятся новые области действия.

И снова мега-пример:

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
}

Вот версия, более близкая к предложению Рога (мне она не очень нравится):

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 Для

Кроме того, я бы предположил, что приведенный выше пример не очень хорошо иллюстрирует его использование - в приведенном выше заявлении с повторной меткой нет ничего, что не могло бы быть выполнено в операторе defer. В примере try / catch этот код не может (например) заключить ошибку в оболочку с информацией об исходном местоположении возвращаемого сообщения об ошибке. Это также не будет работать AFAICS, если вы добавите «try» внутри одного из этих операторов if (например, чтобы проверить ошибку, возвращаемую GetRuntimeEnvironment), потому что «err», на которое ссылается оператор catch, находится в другой области, чем та объявлен внутри блока.

Я думаю, что моя единственная проблема с ключевым словом check заключается в том, что все выходы из функции должны быть return (или, по крайней мере, иметь какой-то_ коннотацию типа «Я собираюсь покинуть функцию»). Мы _могут_ получить become (за совокупную стоимость владения), по крайней мере, в become есть что-то вроде "Мы становимся другой функцией" ... но слово "проверка" действительно не похоже на это будет выход для функции.

Точка выхода функции чрезвычайно важна, и я не уверен, действительно ли check ощущается как «точка выхода». Помимо этого, мне очень нравится идея того, что делает check , он позволяет гораздо более компактно обрабатывать ошибки, но все же позволяет обрабатывать каждую ошибку по-разному или оборачивать ошибку так, как вы хотите.

Могу я также добавить предложение?
А как насчет этого:

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
}

Назначение ошибки может иметь любую форму, даже что-то глупое вроде

f := Open(name) =: e

Или вернуть другой набор значений в случае ошибки, а не только ошибок, а также блок try catch был бы неплохим.

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 Я лично считаю, что очень хорошо, если ошибки в Go не обрабатываются специальным образом. Это просто ценности, и я считаю, что они должны оставаться такими.

Кроме того, try-catch (и подобные конструкции) - это просто плохая конструкция, которая поощряет плохие методы. Каждая ошибка должна обрабатываться отдельно, а не обрабатываться каким-то «улавливающим все» обработчиком ошибок.

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
это слишком сложно.

моя идея: |err| означает ошибку проверки: 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 и все будущие комментаторы: см. https://go.googlesource.com/proposal/+/master/design/go2draft.md .

Я подозреваю, что сейчас эта ветка устарела, и нет смысла продолжать здесь. Страница обратной связи Wiki - это то место, где, вероятно, следует развиваться в будущем.

Мега-пример с использованием предложения Go 2:

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
}

Я думаю, это настолько чисто, насколько мы можем надеяться. Блок handle обладает хорошими качествами метки again или ключевого слова Ruby rescue . Единственный вопрос, который у меня в голове, - это использовать ли знаки препинания или ключевое слово (я думаю, ключевое слово) и разрешить ли получение ошибки без ее возврата.

Я пытаюсь понять предложение - похоже, что для каждой функции существует Это похоже на настоящую слабость.

Мне также интересно, не упускаем ли мы из виду критическую необходимость разработки средств тестирования в наших системах. Рассмотрение того, как мы собираемся использовать пути ошибок во время тестов, должно быть частью обсуждения, но я тоже этого не вижу,

@sdwarwick Я не думаю, что это лучшее место для обсуждения проекта проекта, описанного на https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . Лучше всего добавить ссылку на запись на вики-странице https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

Тем не менее, этот проект допускает использование нескольких блоков дескрипторов в функции.

Этот вопрос начался как конкретное предложение. Мы не собираемся принимать это предложение. По этому вопросу было много серьезных дискуссий, и я надеюсь, что люди выделят хорошие идеи в отдельные предложения и обсудят последний проект проекта. Я собираюсь закрыть этот вопрос. Спасибо за обсуждение.

Если говорить в совокупности этих примеров:

r, err := os.Open(src)
    if err != nil {
        return err
    }

То, что я хотел бы написать одной строчкой примерно так:

r, err := os.Open(src) try ("blah-blah: %v", err)

Вместо «попробовать» поставьте любое красивое и подходящее слово.

С таким синтаксисом ошибка вернется, а остальные будут значениями по умолчанию в зависимости от типа. Если мне нужно вернуться вместе с ошибкой и чем-то еще конкретным, а не по умолчанию, тогда никто не отменяет классический более многострочный вариант.

Еще короче (без добавления какой-то обработки ошибок):

r, err := os.Open(src) try

)
PS Простите за мой английский))

Мой вариант:

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

Привет,

У меня простая идея, основанная на том, как обработка ошибок работает в оболочке, как и в первоначальном предложении. В оболочке ошибки сообщаются возвращаемыми значениями, которые не равны нулю. Возвращаемое значение последней команды / вызова сохраняется в $? в оболочке. В дополнение к имени переменной, заданному пользователем, мы могли бы автоматически сохранить значение ошибки последнего вызова в предопределенной переменной и сделать так, чтобы его можно было проверить с помощью предопределенного синтаксиса. Я выбрал? как синтаксис для ссылки на последнее значение ошибки, которое было возвращено из вызова функции в текущей области. Я выбрал! как сокращение для if? ! = ноль {}. Выбор для? зависит от оболочки, но также потому, что это кажется разумным. Если возникает ошибка, вас, естественно, интересует, что произошло. Возникает вопрос. ? является общим признаком поднятого вопроса, и поэтому мы используем его для ссылки на последнее значение ошибки, которое было сгенерировано в той же области.
! используется как сокращение для if? ! = nil, потому что это означает, что нужно обратить внимание на случай, если что-то пойдет не так. ! означает: если что-то пошло не так, сделайте это. ? ссылается на последнее значение ошибки. Как обычно стоимость? равно нулю, если ошибки не было.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

Чтобы сделать синтаксис более привлекательным, я бы позволил разместить! линия сразу после вызова, а также без фигурных скобок.
С помощью этого предложения вы также можете обрабатывать ошибки без использования идентификатора, определенного программистом.

Это было бы разрешено:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

Это было бы разрешено

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

Согласно этому предложению вам не нужно возвращаться из функции при возникновении ошибки.
и вместо этого вы можете попытаться исправить ошибку.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

В рамках этого предложения вы можете использовать! в цикле for для нескольких подобных попыток.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

Я в курсе! в основном используется для отрицания выражений, поэтому предложенный синтаксис может вызвать путаницу у непосвященных. Идея такая! сам по себе расширяется до? ! = nil, когда он используется в условном выражении в случае, подобном показанному в верхнем примере, где он не привязан к какому-либо конкретному выражению. Верхняя строка for не может быть скомпилирована с текущим ходом, потому что это не имеет смысла без контекста. Под это предложение! само по себе истинно, когда ошибка произошла в самом последнем вызове функции, которая может вернуть ошибку.

Оператор return для возврата ошибки сохраняется, потому что, как здесь отмечали другие, желательно сразу увидеть, где возвращается ваша функция. Вы можете использовать этот синтаксис в сценарии, когда ошибка не требует выхода из функции.

Это предложение проще, чем некоторые другие предложения, поскольку нет никаких усилий для создания варианта блока try and catch, подобного синтаксису, известному из других языков. Он сохраняет текущую философию обработки ошибок непосредственно там, где они возникают, и делает ее более лаконичной.

@tobimensch, пожалуйста, вики-странице обратной связи по обработке ошибок Go 2 . Сообщения по этому закрытому вопросу могут быть пропущены.

Если вы его не видели, возможно, вам стоит прочитать черновик проекта обработки ошибок Go 2 .

И вас могут заинтересовать Требования, которые следует учитывать при обработке ошибок Go 2 .

Может быть, уже слишком поздно указывать на это, но меня беспокоит все, что похоже на магию javascript. Я говорю об операторе || который каким-то волшебным образом должен работать с error intedface. Мне это не нравится.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги