Избегать проверки, является ли ошибка нулевым повторением?

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

a, err := doA()
if err != nil {
  return nil, err
}
b, err := doB(a)
if err != nil {
  return nil, err
}
c, err := doC(b)
if err != nil {
  return nil, err
}
... and so on ...

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

ОБНОВЛЕНИЕ: Спасибо за все ответы. Обратите внимание, что в моем примере doB зависит от a, doC зависит от b и так далее. Поэтому большинство предлагаемых рефакторингов не работают в этом случае. Любое другое предложение?

6 ответов

Решение

Это частая жалоба, и есть несколько ответов на нее.

Вот несколько распространенных:

1 - это не так плохо

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

2 - Это на самом деле хорошая вещь

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

3 - Используйте панику / восстановить

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

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

4 - немного реорганизовать код

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

В качестве тривиального примера это:

err := doA()
if err != nil {
    return err
}
err := doB()
if err != nil {
    return err
}
return nil

также может быть организован как:

err := doA()
if err != nil {
    return err
}
return doB()

5 - использовать именованные результаты

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

6 - Используйте оператор перед условием if

Как хорошо напомнил Том Уайльд в комментарии ниже, if операторы в Go принимают простое утверждение перед условием. Так что вы можете сделать это:

if err := doA(); err != nil {
    return err
}

Это хорошая идиома Go, и используется часто.

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

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

func validError(errs ...error) error {
    for i, _ := range errs {
        if errs[i] != nil {
            return errs[i]
        }
    }
    return nil
}

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

Пример использования ( полная версия в игре):

x, err1 := doSomething(2)
y, err2 := doSomething(3)

if e := validError(err1, err2); e != nil {
    return e
}

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

Вы можете использовать именованные возвращаемые параметры, чтобы немного сократить

Ссылка на игровую площадку

func doStuff() (result string, err error) {
    a, err := doA()
    if err != nil {
        return
    }
    b, err := doB(a)
    if err != nil {
        return
    }
    result, err = doC(b)
    if err != nil {
        return
    }
    return
}

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

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

type Type1 struct {
    a int
    b int
    c int

    err error
}

func (t *Type1) doA() {
    if t.err != nil {
        return
    }

    // do something
    if err := do(); err != nil {
        t.err = err
    }
}

func (t *Type1) doB() {
    if t.err != nil {
        return
    }

    // do something
    b, err := t.doWithA(a)
    if err != nil {
        t.err = err
        return
    }

    t.b = b
}

func (t *Type1) doC() {
    if t.err != nil {
        return
    }

    // do something
    c, err := do()
    if err != nil {
        t.err = err
        return
    }

    t.c = c
}

func main() {

    t := Type1{}
    t.doA()
    t.doB()
    t.doC()

    if t.err != nil {
        // handle error in t
    }

}

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

Это имеет некоторые преимущества, хотя.

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

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

Вы можете передать ошибку в качестве аргумента функции

func doA() (A, error) {
...
}
func doB(a A, err error)  (B, error) {
...
} 

c, err := doB(doA())

Я заметил, что некоторые методы в пакете "html/template" делают это, например

func Must(t *Template, err error) *Template {
    if err != nil {
        panic(err)
    }
    return t
}
Другие вопросы по тегам