Первоначально опубликовано на https://badgerbadgerbadgerbadger.dev 13 сентября 2017 г.

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

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

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

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

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

Вот почему.

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

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

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

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

package contrived

import(
    "thirdparty/lib"
    "errors"
)

func HandleRequest(req Request, res Response) {
    result, err := ControllerCall(req.Id)
    if err != nil {
        log(err.Error())
        res.BadResponse("...")
        return
    }
    
    res.GoodResponse("...")
}

func ControllerCall(id string) (*string, error) {
    result, err := DbCall(id)
    if err != nil {
        return nil, errors.New("controller failed to blah blah blah "+err.Error())
    }
    
    return result, nil
}

func DbCall(id string) (*string, error) {
    result, err := lib.DoSomething(id)
    if err != nil {
        return nil, errors.New("db call failed for id: "+id+" "+err.Error())
    }
    
    return result, nil
}

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

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

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

Давайте начнем

  • Всегда проверяйте наличие ошибок, если функция их возвращает. Я знаю, это звучит очевидно, но вы будете удивлены, узнав, сколько кода, который я видел, не делает этого.
  • Создавайте собственные типы ошибок для тех ошибок, с которыми вы знаете столкнетесь (неверный ввод, повторяющийся запрос и т. Д.). Верните их, завернутые в errors пакет, когда столкнетесь с ними.
  • При работе с ошибками, возвращаемыми из библиотеки, заключите их в errors.Wrap или errors.Wrapf и вернитесь.
  • При работе с ошибками, возвращаемыми из вашего собственного кода, если:
    - он не требует дополнительного контекста, просто верните его.
    - если он требует некоторого дополнительного контекста (возможно, точек данных, захваченных в этой функции), используйте Wrap или Wrapf.
  • Обрабатывайте ошибки без дальнейшего распространения в точке входа вашей программы (команды в cli или обработчики на веб-сервере). Разверните его с помощью errors.Cause и проверьте его:
    - Если это пользовательский тип, который вы определили для своего проекта, обработайте его соответствующим образом (вы можете захотеть регистрировать их на уровне журнала ошибка или предупреждение, поскольку это было ожидаемым). Если это конечная точка, с которой сталкиваются пользователи, вы можете заключить договор с клиентом о том, какие коды возвращать, и действовать соответствующим образом (вы даже можете в конечном итоге вернуть общее сообщение об ошибке). Если это внутренняя конечная точка (мыслящие микросервисы), вы можете вернуть соответствующий код состояния и сообщение, регистрируя то же самое.
    - Если это тип неизвестно, вы можете захотеть зарегистрировать фатальный. Это ошибка, которой никогда не должно было случиться. После этого все зависит от вас.

Я использую эти рекомендации, чтобы убедиться, что во время отладки у меня достаточно информации, чтобы понять, что происходит не так. Мне все еще приходится делать error != nil везде, но я думаю, что это не так уж плохо, как я думал вначале. В отличие от распространения исключений, этот способ работы заставляет меня останавливаться и думать на каждом этапе, хочу ли я позволить ошибке распространяться самостоятельно или добавить дополнительный контекст.

Наконец

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

Первоначально опубликовано на https://badgerbadgerbadgerbadger.dev 13 сентября 2017 г.