RSS Telegram YouTube Apple Яндекс Spotify Google Amazon Почта

28. Обработка ошибок в Go

03.12.2023

Скачать

К списку выпусков

Ссылки выпуска:

Ошибка в Go - это интерфейс

В Go ошибка - это builtin-интерфейс с единственным методом Error() string. Собственно это все что нужно знать. Можно создавать свои ошибки с контекстом (здесь я не имею ввиду context пакет) необходимой "толщины", а можно воспользоваться готовой простой имплементацией в SDK - пакетом errors.

Когда мы создаем свои ошибки, с помощью вызовов errors.New() или fmt.Errorf(), мы создаем обычную переменную. Под капотом это просто указатель на структуру с единственным полем, строкой.

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

Пакет errors предлагает единственную готовую exported ошибку: ErrUnsupported. Эту ошибку не рекомендуется возвращать напрямую, но обернув. Возвращать эту ошибку нужно в случаях, если запрошенная процедура не поддерживается, например, создание hard link в файловых системах, где это невозможно.

Использование errors пакета: всегда оборачиваем ошибки

Слово "всегда" следует интерпретировать как "взять за правило об этом думать". Wrapping ошибок позволяет добавить дополнительную информацию, которую скорее стоит добавить. Ошибки происходят редко и лучше сопроводить каждую из них информативным контекстом (context пакет тут ни при чем). На каждом уровне стека вызова есть какой-то контекст, в котором возникла эта ошибка. Стоит рассмотреть возможность добавления этого контекста на каждом уровне. Тогда в вызывающем коде, когда ошибка получена и на руках у неё есть достаточно подробный след (трейс) при каких обстоятельствах эта ошибка возникла.

Модели работы с ошибками: исключения

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

Ошибка или указатель на ошибку?

Если у нас ошибка с "богатым" контекстом, то создаем структуру и возвращаем указатель на эту структуру. Для референса можно глянуть на *fs.PathError и метод os.Open, который возвращает эту ошибку.

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

fs.PathError

Разворачиваем ошибки: As или Is?

Если вы используете пакет errors для работы с ошибками, то возможно обратили внимание на рекомендацию использовать функции As и Is вместо сравнений/type assertions, но все же последние встречается чаще.

Как запомнить As и Is

Ключевая проверка в методе As: reflectlite.TypeOf(err).AssignableTo(targetType), т.е. проверяется можно ли присвоить второму аргументы значение типа первого аргумента. Итак, As - разворачиваем до возможности присвоить. Английский предлог As (в роли) используется для уточнения отношения. Например, "She works as a software developer". Можно опустить "works": "She ... as a software developer". Аналогичная конструкция применима и к ошибкам. Ошибка X в роли Y - это ок (As возвращает bool)?

Is-сравнение ошибок проще: сравниваются конкретные типы после проверки reflectlite.TypeOf(target).Comparable()

Что если нужно по-разному обрабтывать ошибки?

Самый простой способ:

  1. Создаем переменные-ошибки через конструктор errors.New.
  2. В вызывающем методе сравниваем ошибки с объявленными в пункет 1.
Этот способ и следующий возможно плохи тем, что у вас меньше контроля над тем, что вы возвращаете пользователю: текст ошибки может содержать информацию, которую вам бы не хотелось ему показывать.

Самый простой способ + немного контекста:

  1. Создаем переменные-ошибки через конструктор errors.New.
  2. Оборачиваем такую ошибку, добавляя контекст.
  3. В вызывающем методе проверяем ошибку функцией Is.

Много контекста:

  1. Создаем свои структуры для каждой ошибки с необходимыми полями, для описания контекста ошибки.
  2. Чтобы эти структуры стали ошибками, имплементируем у каждой метод Error() string.
  3. В вызывающем методе проверяем ошибку функцией As и получаем нужный контекст.

Сторонние решения для работы с ошибками

ztrue/tracerr

Красиво отображает трейс. Может даже показать соседние строчки кода. Как это работает? По фреймам трейса определяет файл и строчку, читает в нем строчки рядом с проблемной строкой и отображает. Работать это может только в среде разработки. В production у меня остается только бинарник, да и некому будет смотреть на красивые ошибки. Вердикт: я скорее не буду им пользоваться.

К списку выпусков