источник:Энтерит Ningsun.com/09-09-2019/…
Эта статья взята из моей недавней конференции в Токио, Япония.ГоКон Веснавыступления на конференциях.
Errors are just values
Я провел много времени, думая о лучшим подходе к обработке ошибок в программе GO. Я действительно желаю, чтобы там был один способ обработки ошибок, которые могут быть преподаваться всем программистым программистам Rote, так же, как математика или английский алфавит.
Однако я пришел к выводу, что единого способа обработки ошибок не существует. Вместо этого я думаю, что обработку ошибок в Go можно разбить на три основные стратегии.
Sentinel errors
Первый тип обработки ошибок я называю _sentinel errors_.
if err == ErrSomething { … }
Название происходит от практики компьютерного программирования использования определенных значений для указания того, что дальнейшая обработка невозможна. Поэтому для Go мы используем определенные значения для представления ошибок.
Примеры включаютio.EOF
Значения класса или низкоуровневые ошибки, такие какsyscall
часто в упаковкеsyscall.ENOENT
.
и дажеsentinel errors
Указывает, что произошла ошибка _no_, например.go/build.NoGoError
, иpath/filepath.Walk
изpath/filepath.SkipDir
.
использоватьsentinel
Значение — это наименее гибкая стратегия обработки ошибок, поскольку вызывающая сторона должна использовать оператор равенства для сравнения результата с предварительно объявленным значением. Проблема возникает, когда вы хотите предоставить больше контекста, потому что возврат другой ошибки нарушит проверку на равенство.
даже при использовании с добрыми намерениямиfmt.Errorf
Добавление некоторого контекста к ошибке приведет к тому, что проверка на равенство вызывающего объекта не пройдёт. Вместо этого звонящий вынужден смотреть наerror
изError
вывод метода, чтобы увидеть, соответствует ли он определенной строке.
Never inspect the output of error.Error
Также я думаю, что это никогда не следует проверятьerror.Error
вывод метода.error
на интерфейсеError
Методы предназначены для людей, а не для кода.
Содержимое этой строки относится к файлу журнала или отображается на экране. Не следует пытаться изменить поведение программы, проверяя ее.
Я знаю, что иногда это невозможно, и, как кто-то указал в твиттере, этот совет не относится к написанию тестов. Что еще более важно, на мой взгляд, сравнение неправильной строковой формы — это запах кода, и вы должны стараться избегать этого.
Sentinel errors become part of your public API
Если ваша общедоступная функция или метод возвращает ошибку с определенным значением, то это значение должно быть общедоступным и, конечно же, задокументированным. Это увеличивает площадь API.
Если ваш API определяет интерфейс, который возвращает конкретную ошибку, все реализации этого интерфейса будут ограничены возвратом только этой ошибки, даже если они могут предоставить более описательную ошибку.
пройти черезio.Reader
видеть это. рисунокio.Copy
Такая функция требует, чтобы реализация читателя возвращала точноio.EOF
, чтобы сообщить вызывающей стороне, что данных больше нет, но это не ошибка.
Sentinel errors create a dependency between two packages
уже,sentinel error values
Самая большая проблема заключается в том, что они создают зависимости исходного кода между двумя пакетами. Например, чтобы проверить, равна ли ошибкаio.EOF
, ваш код должен импортироватьio
Сумка.
Этот конкретный пример звучит не так уж плохо, поскольку он распространен, но представьте, когда многие пакеты в проекте экспортируютсяerror values
, связь существует, когда другие пакеты в проекте должны быть импортированы для проверки определенных условий ошибки.
Поработав над большим проектом, в котором использовался этот паттерн, я могу сказать вам, что призрак плохого дизайна в виде циклов импорта никогда не покидает нас.
Conclusion: avoid sentinel errors
Итак, мой совет - избегать использования в коде, который вы пишетеsentinel error values
. В некоторых случаях они используются в стандартной библиотеке, но подражать этому шаблону не следует.
Если кто-то попросит вас экспортировать неверное значение из пакета, вам следует вежливо отказаться и вместо этого предложить альтернативу, например ту, которую я рассмотрю ниже.
Error types
Error types
— это вторая форма обработки ошибок в Go, которую я хочу обсудить.
if err, ok := err.(SomeType); ok { … }
Тип ошибки — это создаваемый вами тип, который реализует интерфейс ошибки. В этом примереMyError
Введите файлы и строки трассировки, а также сообщения, объясняющие, что произошло.
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d: %s”, e.File, e.Line, e.Msg)
}
return &MyError{"Something happened", “server.go", 42}
так какMyError error
является типом, поэтому вызывающие объекты могут использовать утверждения типа для извлечения дополнительного контекста из ошибок.
err := something()
switch err := err.(type) {
case nil:
// call succeeded, nothing to do
case *MyError:
fmt.Println(“error occurred on line:”, err.Line)
default:
// unknown error
}
error types
относительноerror values
Большим улучшением является то, что они могут обернуть основную ошибку, чтобы предоставить больше контекста.
Хороший примерos.PathError
тип, который аннотирует основную ошибку тем, что он пытается сделать, и файлом, который он пытается использовать.
// PathError records an error and the operation
// and file path that caused it.
type PathError struct {
Op string
Path string
Err error // the cause
}
func (e *PathError) Error() string
Problems with error types
Вызывающий может использовать утверждение типа или переключатель типа,error types
Должен быть публичным.
Если ваш код реализует интерфейс, контракт которого требует определенного типа ошибки, все разработчики этого интерфейса должны зависеть от пакета, который определяет тип ошибки.
Глубокое знание типов пакетов создает сильную связь с вызывающей стороной, что приводит к хрупкости API.
Conclusion: avoid error types
Несмотря на то чтоerror types
Сравниватьsentinel error values
лучше, потому что они охватывают больше контекста ошибки, типы ошибок также имеют многоerror values
Проблема.
Так что мой совет - избегатьerror types
или, по крайней мере, не делать их частью общедоступного API.
Opaque errors
Теперь давайте рассмотрим третий тип обработки ошибок. На мой взгляд, это наиболее гибкая стратегия обработки ошибок, поскольку она требует наименьшей связи между кодом и вызывающим кодом.
Я называю этот подход непрозрачной обработкой ошибок, потому что, хотя вы знаете, что произошла ошибка, вы не можете заглянуть внутрь ошибки. Как вызывающая сторона, все, что вы знаете о результате операции, действительно или нет.
Это непрозрачная обработка ошибок — просто возвращайте ошибку, не делая предположений о ее содержании. Таким образом, обработка ошибок может быть очень полезной в качестве средства отладки.
import “github.com/quux/bar”
func fn() error {
x, err := bar.Foo()
if err != nil {
return err
}
// use x
}
Например,Foo
Контракт не гарантирует, что он вернет в неправильном контексте. Передавая ошибку с дополнительным контекстом,Foo
Авторы s теперь могут аннотировать ошибки, не нарушая контракт с вызывающей стороной.
Assert errors for behaviour, not type
В некоторых случаях использования дихотомии (ошибка или нет) для обработки ошибок недостаточно.
Например, взаимодействие со службами вне процесса (например, сетевая активность) требует от вызывающей стороны рассмотрения характера ошибки, чтобы определить, целесообразно ли повторить операцию.
В этом случае вместо того, чтобы утверждать, что ошибка имеет определенный тип или значение, мы можем утверждать, что ошибка реализует определенное поведение. Рассмотрим этот пример:
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
Любые ошибки могут быть переданыIsTemporary
чтобы определить, можно ли повторить ошибку.
Если ошибка не реализованаtemporary
интерфейс, то есть неTemporary
способ, то ошибка не временная.
Если ошибка материализуетсяTemporary
, то еслиtrue
Возвращая значение true, вызывающий объект может повторить операцию.
Ключевым моментом здесь является то, что эта логика может работать без импорта неправильно определенного пакета или прямого знания чего-либо оerr
реализуется в случае базового типа — нас просто интересует его поведение.
Не просто проверяйте ошибки, обрабатывайте их изящно
Это подводит меня ко второй пословице Go, о которой я хочу поговорить: не просто проверяйте наличие ошибок, а обрабатывайте их изящно. Можете ли вы задать несколько вопросов по следующему коду?
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
Очевидное предложение состоит в том, что пять строк функции можно заменить на:
return authenticate(r.User)
Но это простой вопрос, который каждый должен найти в обзорах кода. Более фундаментальная проблема с этим кодом заключается в невозможности определить, откуда возникла исходная ошибка.
еслиauthenticate
возвращает ошибку, тоAuthenticateRequest
Ошибка будет возвращена вызывающей стороне, которая может сделать то же самое, и так далее. В верхней части программы тело программы выводит ошибки на экран или в файл журнала, и все, что будет напечатано, будет:No such file or directory
.
Нет информации о неправильном создании файла и строки. Нет стека вызовов, вызвавшего ошибкуstack trace
. Авторы этого кода будут вынуждены провести долгую сессию, разделив свой код пополам, чтобы выяснить, какой путь кода вызывает ошибку «файл не найден».
_Язык программирования Go_ от Донована и Кернигана рекомендует использоватьfmt.Errorf
Добавить контекст к путям ошибок
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return **fmt.Errorf("authenticate failed: %v", err)**
}
return nil
}
Но, как мы видели ранее, этот шаблон связан с использованиемsentinel error values
или утверждение типа несовместимо, поскольку значение ошибки преобразуется в строку, объединяется с другой строкой и затем используетсяfmt.Errorf
Преобразование его обратно в ошибку нарушает равенство, полностью разрушая контекст исходной ошибки.
Annotating errors
Я хотел бы предложить способ добавления контекста к ошибкам, и для этого я представлю простой пакет. Код находится вgithub.com/pkg/errors
поставка. Пакет ошибок имеет две основные функции:
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
Первая функцияWrap
, который получает ошибку и сообщение и создает новую ошибку.
// Cause unwraps an annotated error.
func Cause(err error) error
Вторая функцияCause
, который берет ошибку, которая могла быть упакована, и разворачивает ее, чтобы восстановить исходную ошибку.
Используя эти две функции, теперь мы можем аннотировать любые ошибки и восстанавливать основные ошибки, когда нам нужно их проверить. Рассмотрим пример функции, которая считывает содержимое файла в память.
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, **errors.Wrap(err, "open failed")**
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, **errors.Wrap(err, "read failed")**
}
return buf, nil
}
Мы будем использовать эту функцию, чтобы написать функцию для чтения файла конфигурации, а затем изmain
назови это.
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, **errors.Wrap(err, "could not read config")**
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
еслиReadConfig
Путь кода не удался, потому что мы использовалиerrors.Wrap
, получаем красивую ошибку комментария в стиле K&D.
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
так какerrors.Wrap
Генерируется ошибка стека, поэтому мы можем изучить стек для получения дополнительной отладочной информации. Вот тот же пример снова, но на этот раз мы используемfmt.Println
заменятьerrors.Print
func main() {
_, err := ReadConfig()
if err != nil {
errors.Print(err)
os.Exit(1)
}
}
Мы получим следующую информацию:
readfile.go:27: could not read config
readfile.go:14: open failed
open /Users/dfc/.settings.xml: no such file or directory
Первая строка исходит изReadConfig
, вторая строка изReadFile
изos.Open
часть, остальное происходит отos
Сама посылка не несет информации о местоположении.
Теперь, когда мы представили концепцию упаковки стеков генерации ошибок, нам нужно обсудить обратную операцию — их развертывание. Этоerrors.Cause
домен функции.
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := **errors.Cause(err)**.(temporary)
return ok && te.Temporary()
}
В работе всякий раз, когда вам нужно проверить, соответствует ли ошибка определенному значению или типу, вы должны сначала использоватьerrors.Cause
Функция восстанавливает исходную ошибку.
Only handle errors once
Наконец, я хочу упомянуть: вы должны обрабатывать ошибки только один раз. Обработка ошибок означает проверку значения ошибки и принятие решения.
func Write(w io.Writer, buf []byte) {
w.Write(buf)
}
Если решение не принято, ошибка игнорируется. Как мы видим здесь,w.Write
ошибки сбрасываются.
Однако существуют также проблемы с принятием нескольких решений по одной ошибке.
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
// annotated error goes to log file
log.Println("unable to write:", err)
// unannotated error returned to caller
return err
}
return nil
}
In this example if an error occurs during Write
, a line will be written to a log file, noting the file and line that the error occurred, and the error is also returned to the caller, who possibly will log it, and return it, all the way back up to the top of the program.
So you get a stack of duplicate lines in your log file, but at the top of the program you get the original error without any context. Java anyone?
В этом примере, еслиWrite
Во время ошибки в файл журнала записывается строка, обратите внимание на файл и строку, в которой произошла ошибка, и ошибка также возвращается вызывающему, который может зарегистрировать и вернуть ее, вплоть до начала программы. .
Таким образом, вы получаете стопку повторяющихся строк в файле журнала, но в верхней части программы вы получаете любой контекст без исходной ошибки. Кто-нибудь использует Java?
func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return **errors.Wrap(err, "write failed")**
}
использоватьerrors
пакет, который позволяет добавлять контекст к значениям ошибок таким образом, чтобы его могли проверить как люди, так и машины.
Conclusion
В заключение, ошибки являются частью общедоступного API пакета и относятся к ним так же тщательно, как и к любой другой части общедоступного API.
Для максимальной гибкости я рекомендую вам попытаться рассматривать все ошибки как непрозрачные. В тех случаях, когда это невозможно сделать, утверждение ведет себя неправильно, а не ошибка типа или значения.
свернуто в программеsentinel error values
, и используйте его сразу же при возникновении ошибкиerrors.Wrap
Оберните его так, чтобы ошибки превратились в непрозрачные ошибки.
Наконец, если вам нужно проверить, используйтеerrors.Cause
Восстановите основную ошибку.