У Голанга много преимуществ, этим объясняется его популярность. Но обработка ошибок в Go 1 не очень эффективна, нам приходится писать много подробных неудобных кодов в повседневной разработке.

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

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

Вывод первый: я предпочитаю github.com/pkg/errors. Причина будет подробно объяснена ниже.

Проблема

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

import (
   "database/sql"
   "fmt"
)

func foo() error {
   return sql.ErrNoRows
}

func bar() error {
   return foo()
}

func main() {
   err := bar()
   if err != nil {
      fmt.Printf("got err, %+v\n", err)
   }
}
//Outputs:
// got err, sql: no rows in result set

Иногда нам нужно выполнить разную обработку в зависимости от разных типов ошибок:

import (
   "database/sql"
   "fmt"
)

func foo() error {
   return sql.ErrNoRows
}

func bar() error {
   return foo()
}

func main() {
   err := bar()
   if err == sql.ErrNoRows {
      fmt.Printf("data not found, %+v\n", err)
      return
   }
   if err != nil {
      // Unknown error
   }
}
//Outputs:
// data not found, sql: no rows in result set

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

func foo() error {
   return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}

Тогда условие err == sql.ErrNoRows станет ложным. Кроме того, стек вызовов сбрасывается, когда возвращается ошибка, что является наиболее важной диагностической информацией. Нам нужен более гибкий способ решения таких проблем.

Решения

Есть несколько решений проблемы. Ошибка упаковки в эти библиотеки сохранит доступность корневой ошибки и полного стека вызовов.

1. github.com/pkg/errors

от Dave Cheney , в этой библиотеке есть 3 ключевых метода:

  1. Wrap используется для обертывания основной ошибки, добавления контекстной текстовой информации и присоединения стека вызовов. Обычно он используется для обертывания вызовов API от других людей (стандартная библиотека или сторонняя библиотека).
  2. WithMessage используется для добавления контекстной текстовой информации к основной ошибке без присоединения стека вызовов. Применяйте этот метод только для «обернутой ошибки». Примечание: не повторяйте Wrap, это будет записывать стеки вызовов избыточности.
  3. Cause метод предназначен для определения основной ошибки

Перепишите приведенный выше пример с github.com/pkg/errors:

import (
   "database/sql"
   "fmt"

   "github.com/pkg/errors"
)

func foo() error {
   return errors.Wrap(sql.ErrNoRows, "foo failed")
}

func bar() error {
   return errors.WithMessage(foo(), "bar failed")
}

func main() {
   err := bar()
   if errors.Cause(err) == sql.ErrNoRows {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/*Output:
data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo
    /usr/three/main.go:11
main.bar
    /usr/three/main.go:15
main.main
    /usr/three/main.go:19
runtime.main
    ...
*/

Если мы используем %v в качестве параметра формата, мы получим однострочную строку вывода, содержащую весь контекстный текст в порядке стека вызовов. Если изменить параметр формата на %+v ,, мы получим полный стек вызовов.

Если вы хотите просто обернуть ошибку стеком вызовов присоединения, дополнительный контекстный текст не требуется, используйте WithStack

func foo() error {
   return errors.WithStack(sql.ErrNoRows)
}

Примечание. При использовании Wrap, WithMessage или WithStack, если параметр err равен nil, будет возвращен nil, что означает, что нам не нужно проверять err != nil условие перед вызовом метода. Сохранение кода простым

2. golang.org/x/xerrors

Выслушав отзывы сообщества, команда Go опубликовала предложение по упрощению обработки ошибок в Go 2. Член основной группы Go Расс Кокс частично реализовал это предложение в golang.org/x/xerrors. Он решает ту же проблему с помощью подхода, аналогичного github.com/pkg/errors, вводит команду форматирования : %w и использует метод Is для определения основной ошибки.

import (
   "database/sql"
   "fmt"

   "golang.org/x/xerrors"
)

func bar() error {
   if err := foo(); err != nil {
      return xerrors.Errorf("bar failed: %w", foo())
   }
   return nil
}

func foo() error {
   return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {
   err := bar()
   if xerrors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs:
data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
    main.bar
        /usr/four/main.go:12
  - foo failed:
    main.foo
        /usr/four/main.go:18
  - sql: no rows in result set
*/

По сравнению с github.com/pkg/errors, имеет ряд недостатков:

  1. Замените Wrap параметром формата : %w. Кажется, упрощает код, но этот подход теряет проверку во время компиляции. Если : %w не является концом строки формата (e.g., "foo : %w bar"), или двоеточие отсутствует (e.g., "foo %w"), или пробел между двоеточием и знаком процента отсутствует (e.g., "foo:%w"), упаковка завершится неудачно без какого-либо предупреждения.
  2. Что еще более серьезно, вы должны проверить условие err != nil перед вызовом xerrors.Errorf. Это на самом деле нисколько не упрощает работу разработчика.

3. Встроенная поддержка переноса ошибок в Go 1.13.

Начиная с Go 1.13, некоторые (не все) функции xerrors были интегрированы в стандартную библиотеку. Он наследует все недостатки xerrors и вносит один дополнительный☹️. Поэтому я рекомендую не использовать его в данный момент.

import (
   "database/sql"
   "errors"
   "fmt"
)

func bar() error {
   if err := foo(); err != nil {
      return fmt.Errorf("bar failed: %w", foo())
   }
   return nil
}

func foo() error {
   return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {
   err := bar()
   if errors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found,  %+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs:
data not found,  bar failed: foo failed: sql: no rows in result set
*/

Аналогично версии xerrors. Однако он не поддерживает вывод стека вызовов. И согласно официальному заявлению, графика для этого нет. Так что github.com/pkg/errors - лучший выбор на данный момент

Подведение итогов

Проведя сравнение выше, я считаю, что вы сделали свой выбор. Позвольте мне пояснить. Мой порядок выбора: 1 ›2› 3

  1. Если вы используете github.com/pkg/errors, оставьте его. На данный момент нет лучшего решения, чем это
  2. Если вы уже много использовали golang.org/x/xerrors, не переключайтесь на встроенное решение в спешке, оно того не заслуживает.

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

Не говоря уже о if err != nil, на которую широко распространяются жалобы, даже дорожная карта по его усовершенствованию весьма противоречива. Фактически команда Го скорректировала предложение из-за непрекращающихся возражений.

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

Последняя заметка

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

Почему? Когда пользователь начинает использовать errors.Cause(err, sql.ErrNoRows) или xerrors.Is (err, sql.ErrNoRows), это означает, что sql.ErrNoRows, как деталь реализации, открывается извне и становится частью API.

Если вы занимаетесь разработкой приложений с использованием некоторых библиотек, это приемлемо.

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

См. Также: Мастеринг проводов