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


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



картинка отсюда


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


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


  • Открытые типы ошибок увеличивают "площадь контакта" с API пакета.
  • Новые реализации должны возвращать только типы, указанные в объявлении интерфейса, даже если они плохо подходят.
  • Тип ошибки, после добавления в код, не может быть изменен или объявлен устаревшим без нарушения совместимости, делая API хрупким.

Подтверждайте ожидаемое поведение, а не тип ошибок


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


Это предложение соответствует природе неявных интерфейсов Go, а не является [подтипом] природы языков, основанных на наследовании. Рассмотрим этот пример:


func isTimeout(err error) bool {
        type timeout interface {
                Timeout() bool
        }
        te, ok := err.(timeout)
        return ok && te.Timeout()
}

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


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


Это может показаться неразрешимой проблемой, но на практике существует довольно мало общепринятых методов интерфейса, поэтому Timeout() bool и Temporary() bool будут охватывать большой набор вариантов использования.


В заключение


Подтверждайте ожидаемое поведение, а не тип ошибок


Для авторов пакетов


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


Для пользователей пакета


Если вам нужно проверить ошибку, используйте интерфейсы для подтверждения ожидаемого поведения, а не типа ошибки. Не спрашивайте авторов пакетов об открытых типах ошибок; попросите их привести свои типы в соответствие с общими интерфейсами, указав методы Timeout() или Temporary() в зависимости от ситуации.


Об авторе


Автор данной статьи, Дейв Чини, является автором многих популярных пакетов для Go, например https://github.com/pkg/errors и https://github.com/davecheney/httpstat.


От переводчика


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


Правки в текст для повышения понятности материала приветствуются!

Комментарии (2)


  1. Color
    19.02.2019 15:21

    Ну фактически автор пересказывает суть исключений.


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


    А то, что предлагает автор, еще хуже, чем проверять тип ошибок или ставнивать с глобальными переменными пакета. Го заведомо не дает вызывающему никакой информации кроме того, была ошибка, или нет. Любая надстройка над этой концепцией добавляет вам неявных зависимостей. Мне из имеющегося пока ближе глобальные переменные, реализующие error, в которые можно передать текст при return-е. Это, с одной стороны, описывает "ожидаемое поведение" (одна переменная вида ErrWrongInput — одно поведение -> один вид обработки), при этом пользовательский текст можно передать любой, подробно описывающий что произошло для лога или сообщения пользователю.
    Поможет ли это сделать контракт явным — нет, нифига.


  1. Forked
    20.02.2019 01:39

    У автора оригинала (Дэйва Чини) очень тяжелый язык. И ход его мысли бывает совсем нелегко уловить. Но этот перевод только усугубляет ситуацию. Начиная с «контракта для функций» и лишних запятых.