Сравнение ошибок golang

The error type is an interface type. An error variable represents any value that can describe itself as a string. Here is the interface’s declaration:

type error interface {
    Error() string
}

The most commonly-used error implementation is the errors package’s unexported errorString type:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

See this working code output (The Go Playground):

package main

import (
    "errors"
    "fmt"
    "io"
)

func main() {
    err1 := fmt.Errorf("Error")
    err2 := errors.New("Error")
    err3 := io.EOF

    fmt.Println(err1)         //Error
    fmt.Printf("%#v\n", err1) // &errors.errorString{s:"Error"}
    fmt.Printf("%#v\n", err2) // &errors.errorString{s:"Error"}
    fmt.Printf("%#v\n", err3) // &errors.errorString{s:"EOF"}
}

output:

Error
&errors.errorString{s:"Error"}
&errors.errorString{s:"Error"}
&errors.errorString{s:"EOF"}

Also see: Comparison operators

Comparison operators compare two operands and yield an untyped boolean
value. In any comparison, the first operand must be assignable to the
type of the second operand, or vice versa.

The equality operators == and != apply to operands that are
comparable.

Pointer values are comparable. Two pointer values are equal if they
point to the same variable or if both have value nil. Pointers to
distinct zero-size variables may or may not be equal.

Interface values are comparable. Two interface values are equal if
they have identical dynamic types and equal dynamic values or if both
have value nil.

A value x of non-interface type X and a value t of interface type T
are comparable when values of type X are comparable and X implements
T. They are equal if t’s dynamic type is identical to X and t’s
dynamic value is equal to x.

Struct values are comparable if all their fields are comparable. Two
struct values are equal if their corresponding non-blank fields are
equal.


So:

1- You may use Error(), like this working code (The Go Playground):

package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("Token is expired")
    err2 := errors.New("Token is expired")
    if err1.Error() == err2.Error() {
        fmt.Println(err1.Error() == err2.Error()) // true
    }
}

output:

true

2- Also you may compare it with nil, like this working code (The Go Playground):

package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("Token is expired")
    err2 := errors.New("Token is expired")
    if err1 != nil {
        fmt.Println(err1 == err2) // false
    }
}

output:

false

3- Also you may compare it with exact same error, like this working code
(The Go Playground):

package main

import (
    "fmt"
    "io"
)

func main() {
    err1 := io.EOF
    if err1 == io.EOF {
        fmt.Println("err1 is : ", err1)
    }
}

output:

err1 is :  EOF

ref: https://blog.golang.org/error-handling-and-go

Время на прочтение
7 мин

Количество просмотров 33K

В последнее десятилетие мы успешно пользовались тем, что Go обрабатывает ошибки как значения. Хотя в стандартной библиотеке была минимальная поддержка ошибок: лишь функции errors.New и fmt.Errorf, которые генерируют ошибку, содержащую только сообщение — встроенный интерфейс позволяет Go-программистам добавлять любую информацию. Нужен лишь тип, реализующий метод Error:

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

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

Паттерн, когда одна ошибка содержит другую, встречается в Go столь часто, что после жаркой дискуссии в Go 1.13 была добавлена его явная поддержка. В этой статье мы рассмотрим дополнения к стандартной библиотеке, обеспечивающие упомянутую поддержку: три новые функции в пакете errors и новая форматирующая команда для fmt.Errorf.

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

Ошибки до Go 1.13

Исследование ошибок

Ошибки в Go являются значениями. Программы принимают решения на основе этих значений разными способами. Чаще всего ошибка сравнивается с nil, чтобы понять, не было ли сбоя операции.

if err != nil {
    // something went wrong
}

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

var ErrNotFound = errors.New("not found")

if err == ErrNotFound {
    // something wasn't found
}

Значение-ошибка может быть любого типа, который удовлетворяет определённому в языке интерфейсу ошибок. Программа может использовать утверждение типа или переключатель типа для просмотра значения-ошибки более специфического типа.

type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string { return e.Name + ": not found" }

if e, ok := err.(*NotFoundError); ok {
    // e.Name wasn't found
}

Добавление информации

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

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

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

type QueryError struct {
    Query string
    Err   error
}

Программы могут заглянуть внутрь значения *QueryError и принять решение на основе исходной ошибки. Иногда это называется «распаковкой» (unwrapping) ошибки.

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

Тип os.PathError из стандартной библиотеки — ещё пример того, как одна ошибка содержит другую.

Ошибки в Go 1.13

Метод Unwrap

В Go 1.13 в пакетах стандартной библиотеки errors и fmt упрощена работа с ошибками, которые содержат другие ошибки. Самым важным является соглашение, а не изменение: ошибка, содержащая другую ошибку, может реализовать метод Unwrap, который возвращает исходную ошибку. Если e1.Unwrap() возвращает e2, то мы говорим, что e1 упаковывает e2 и можно распаковать e1 для получения e2.

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

func (e *QueryError) Unwrap() error { return e.Err }

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

Исследование ошибок с помощью Is и As

В Go 1.13 пакет errors содержит две новые функции для исследования ошибок: Is и As.

Функция errors.Is сравнивает ошибку со значением.

// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // something wasn't found
}

Функция As проверяет, относится ли ошибка к конкретному типу.

// Similar to:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
    // err is a *QueryError, and e is set to the error's value
}

В простейшем случае функция errors.Is ведёт себя как сравнение с контрольной ошибкой, а функция errors.As ведёт себя как утверждение типа. Однако работая с упакованными ошибками, эти функции оценивают все ошибки в цепочке. Давайте посмотрим на вышеприведённый пример распаковки QueryError для исследования исходной ошибки:

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

С помощью функции errors.Is можно записать так:

if errors.Is(err, ErrPermission) {
    // err, or some error that it wraps, is a permission problem
}

Пакет errors также содержит новую функцию Unwrap, которая возвращает результат вызова метода Unwrap ошибки, или возвращает nil, если у ошибки нет метода Unwrap. Обычно лучше использовать errors.Is или errors.As, поскольку они позволяют исследовать всю цепочку одним вызовом.

Упаковка ошибок с помощью %w

Как я упоминал, нормальной практикой является использование функции fmt.Errorf для добавления к ошибке дополнительной информации.

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

В Go 1.13 функция fmt.Errorf поддерживает новая команда %w. Если она есть, то ошибка, возвращаемая fmt.Errorf, будет содержать метод Unwrap, возвращающий аргумент %w, который должен быть ошибкой. Во всех остальных случаях %w идентична %v.

if err != nil {
    // Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

Упаковка ошибки с помощью %w делает её доступной для errors.Is и errors.As:

err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...

Когда стоит упаковывать?

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

Например, представьте функцию Parse, которая считывает из io.Reader сложную структуру данных. Если возникает ошибка, нам захочется узнать номер строки и столбца, где она произошла. Если ошибка возникла при чтении из io.Reader, нам нужно будет упаковать её, чтобы выяснить причину. Поскольку вызывающий был предоставлен функции io.Reader, имеет смысл показать сгенерированную им ошибку.

Другой случай: функция, которая делает несколько вызовов базы данных, вероятно, не должна возвращать ошибку, в которой упакован результат одного из этих вызовов. Если БД, которая использовалась этой функцией, является частью реализации, то раскрытие этих ошибок нарушит абстракцию. К примеру, если функция LookupUser из пакета pkg использует пакет Go database/sql, то она может столкнуться с ошибкой sql.ErrNoRows. Если вернуть ошибку с помощью fmt.Errorf("accessing DB: %v", err), тогда вызывающий не может заглянуть внутрь и найти sql.ErrNoRows. Но если функция вернёт fmt.Errorf("accessing DB: %w", err), тогда вызывающий мог бы написать:

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …

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

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

Настройка тестирования ошибок с помощью методов Is и As

Функция errors.Is проверяет каждую ошибку в цепочке на соответствие целевому значению. По умолчанию ошибка соответствует этому значению, если они эквивалентны. Кроме того, ошибка в цепочке может объявлять о своём соответствии целевому значению с помощью реализации метода Is.

Рассмотрим ошибку, вызванную пакетом Upspin, которая сравнивает ошибку с шаблоном и оценивает только ненулевые поля:

type Error struct {
    Path string
    User string
}

func (e *Error) Is(target error) bool {
    t, ok := target.(*Error)
    if !ok {
        return false
    }
    return (e.Path == t.Path || t.Path == "") &&
           (e.User == t.User || t.User == "")
}

if errors.Is(err, &Error{User: "someuser"}) {
    // err's User field is "someuser".
}

Функция errors.As также консультирует метод As при его наличии.

Ошибки и API пакетов

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

Самое простое: говорить, была ли операция успешной, возвращая, соответственно, значение nil или не-nil. Во многих случаях другой информации не требуется.

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

var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
    if itemNotFound(name) {
        return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
    }
    // ...
}

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

В любом случае, не раскрывайте пользователю внутренние подробности. Как упоминалось в главе «Когда стоит упаковывать?», если возвращаете ошибку из другого пакета, то преобразуйте её, чтобы не раскрывать исходную ошибку, если только не собираетесь брать на себя обязательство в будущем вернуть эту конкретную ошибку.

f, err := os.Open(filename)
if err != nil {
    // The *os.PathError returned by os.Open is an internal detail.
    // To avoid exposing it to the caller, repackage it as a new
    // error with the same text. We use the %v formatting verb, since
    // %w would permit the caller to unwrap the original *os.PathError.
    return fmt.Errorf("%v", err)
}

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

var ErrPermission = errors.New("permission denied")

// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() {
    if !userHasPermission() {
        // If we return ErrPermission directly, callers might come
        // to depend on the exact error value, writing code like this:
        //
        //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
        //
        // This will cause problems if we want to add additional
        // context to the error in the future. To avoid this, we
        // return an error wrapping the sentinel so that users must
        // always unwrap it:
        //
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

Заключение

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

Как сказал Расс Кокс (Russ Cox) в своём выступлении на GopherCon 2019, на пути к Go 2 мы экспериментируем, упрощаем и отгружаем. И теперь, отгрузив эти изменения, мы принимаемся за новые эксперименты.

Overview

First of all, what is meant by equality of the error?  As you already know that error is represented by the error interface in go. In go, two interfaces are equal if

  • Both refer to the same underlying type
  • The underlying value is equal (or both nil)

So the above two points apply for comparing the error as well. There are two ways of checking if given errors are equal

  • Using the equality operator (==)
  • Using the Is function of errors package – https://golang.org/pkg/errors/ .  Using Is function is preferable to using the equality operator because it checks for equality by unwrapping the first error sequentially and matches it with the target error at each step of unwrap. We will see an example, later on, to fully understand why it is preferable. Below is the syntax of Is function.
func Is(err, target error) bool

Code

Let’s see an example

package main
import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happended"
}
func main() {
    var err1 errorOne
    err2 := do()
    if err1 == err2 {
        fmt.Println("Equality Operator: Both errors are equal")
    }
    if errors.Is(err1, err2) {
        fmt.Println("Is function: Both errors are equal")
    }
}
func do() error {
    return errorOne{}
}

Output

Equality Operator: Both errors are equal
Is function: Both errors are equal

In the above program, we created errorOne struct which defines the Error method hence implement the error interface.  We created err1  variable which is an instance of errorOne struct. We also created a do() function which raises an error of type errorOne and that is captured in err2 variable in the main function

Then we are comparing two errors using

  • Using equality operator
err1 == err2
  • Using the Is function of errors package
errors.Is(err1, err2)

Both the method correctly output that the error is equal as both err1 and err2

  • Refer to same underlying type which is errorOne
  • Have the same underlying value

We mentioned above that using the Is function preferable to using the equality operator because it checks for equality by unwrapping the first error sequentially and matches it with the target error at each step of unwrap. Let’s see an example of that

package main

import (
	"errors"
	"fmt"
)

type errorOne struct{}

func (e errorOne) Error() string {
	return "Error One happended"
}

func main() {
	err1 := errorOne{}

	err2 := do()

	if err1 == err2 {
		fmt.Println("Equality Operator: Both errors are equal")
	} else {
		fmt.Println("Equality Operator: Both errors are not equal")
	}

	if errors.Is(err2, err1) {
		fmt.Println("Is function: Both errors are equal")
	}
}

func do() error {
	return fmt.Errorf("E2: %w", errorOne{})
}

Output

Equality Operator: Both errors are not equal
Is function: Both errors are equal

The above program is almost same as the previous program just the difference being that in the do() function we are wrapping the error as well

return fmt.Errorf("E2: %w", errorOne{})
  • The equality operator outputs
Equality Operator: Both errors are not equal
  • While Is function outputs
Is function: Both errors are equal

This is because the err2 returned wraps an instance of  errorOne which is not catched by the equality operator but is catched by Is function

  • go
  • golang
  • I believe you should never inspect the output of the Error method. The Error method on the error interface exists for humans, not code. The contents of that string belong in a log file, or displayed on screen.

    Comparing the string form of an error is, in my opinion, a code smell, and you should try to avoid it.
    —Dave Cheney, “Don’t just check errors, handle them gracefully”

    This is Part 2 of a series about errors and error testing in Go:

    1. Testing errors in Go
    2. Comparing Go error values
    3. Error wrapping in Go

    In Part 1 we talked about when to ignore error values in tests (answer: never), and also about how to write tests for the behaviour when errors are expected.

    To do that, we need the function under test to return the appropriate error. Sometimes that’s easy, and sometimes it’s not. Let’s look at a couple of examples.

    Simulating errors

    If our function is supposed to return an error for invalid input, as in the format.Data example from Part 1 of this series, that’s fairly easy to test. We can just give it invalid input and see what it does.

    In other cases it can be a little more difficult to arrange for the specified error to occur. For example, consider a function like this:

    func ReadAll(r io.Reader) ([]byte, error) {
        data, err := io.ReadAll(r)
        if err != nil {
            return nil, err
        }
        return data, nil
    }

    (Listing reader/1)

    If the function encounters a read error, it returns it. Now, we probably wouldn’t bother to test this behaviour in practice, because it’s so straightforward. But let’s try, just for the fun of it.

    How could we arrange for a read error to happen in a test? That’s not so easy if we use something like a strings.Reader:

    func TestReadAll_ReturnsAnyReadError(t *testing.T) {
        input := strings.NewReader("any old data")
        _, err := reader.ReadAll(input)
        if err == nil {
            t.Error("want error for broken reader, got nil")
        }
    }

    (Listing reader/1)

    This will always fail, even when the function is correct, because reading from a strings.Reader never does produce an error.

    How can we make the strings.Reader return an error when someone calls its Read method? We can’t. But we can implement our own very trivial io.Reader to do just that:

    type errReader struct{}
    
    func (errReader) Read([]byte) (int, error) {
        return 0, io.ErrUnexpectedEOF
    }

    (Listing reader/2)

    This is manifestly useless as a reader, since it never reads anything, always just returning a fixed error instead. But that makes it just the thing to use in our test:

    func TestReadAll_ReturnsAnyReadError(t *testing.T) {
        input := errReader{} // always returns error
        _, err := reader.ReadAll(input)
        if err == nil {
            t.Error("want error for broken reader, got nil")
        }
    }

    (Listing reader/2)

    Because io.Reader is such a small interface, it’s very easy to implement it with whatever behaviour we need in a test. You could imagine more sophisticated kinds of fake reader, such as one that errors after a certain number of bytes have been read, or a ReadCloser that fails to close itself properly, and so on.

    In fact, we don’t even need to implement this erroneous reader ourselves. It’s provided in the standard library iotest package as ErrReader, along with some other useful test readers such as TimeoutReader and HalfReader.

    Testing that an error is not nil

    There’s another kind of mistake it’s easy to make when testing error results, and it comes from a perfectly understandable thought process. “My function returns something and error,” says the programmer, “and tests are about comparing want and got. So I should compare both results against what I expect them to be.”

    In other words, they’ll compare the “something” result with some expected value, which is fine. And they’ll also try to compare the “error” result with some expected value, which is where they run into a problem.

    Let’s see an example. Remember our store.Open example from Part 1? It returns a *Store, if it can successfully open the store, or nil and some error if it can’t.

    Suppose we’re writing a “want error” test for this behaviour. In other words, we’ll deliberately try to open some store that can’t be opened, and check that we get an error.

    This is straightforward, because all we need to do is check that the err value is not nil. That’s all the “contract” promises, so that’s what we check:

    func TestOpenGivesNonNilErrorForBogusFile(t *testing.T) {
        t.Parallel()
        _, err := store.Open("bogus file")
        if err == nil {
            t.Error("want error opening bogus store file")
        }
    }

    (Listing store/1)

    As with the format.Data example, since we expect an error, we just ignore the other result value. We aren’t interested in it for this test.

    Indeed, if we received it and assigned it to got, then the compiler would complain that we don’t then do anything with that variable:

    got declared and not used

    It would be easy to feel bullied by this into doing something with got, like comparing it against a want value. But we know that’s wrong.

    What the compiler is really saying, in its gnomic way, is “Hey programmer, since you don’t seem to care about the value of got, you should ignore it using _.” Quite right, and that’s what we’ll do.

    But supposing we (wrongly) think that we need to test that Open returns some specific error. How would we even do that? Well, we can try the want-and-got pattern:

    func TestOpenGivesSpecificErrorForBogusFile(t *testing.T) {
        t.Parallel()
        want := errors.New("open bogus: no such file or directory")
        _, got := store.Open("bogus")
        if got != want {
            t.Errorf("wrong error: %v", got)
        }
    }

    (Listing store/1)

    This seems plausible, I think you’ll agree. Yet, surprisingly, the test fails:

    wrong error: open bogus: no such file or directory

    Wait, what? That is the error we were expecting. So why are these two values not comparing equal?

    Let’s make the problem even more explicit, by using errors.New to construct both values, and then comparing them:

    func TestOneErrorValueEqualsAnother(t *testing.T) {
        t.Parallel()
        want := errors.New("Go home, Go, you're drunk")
        got := errors.New("Go home, Go, you're drunk")
        if got != want {
            t.Errorf("wrong error: %v", got)
        }
    }

    (Listing store/1)

    Surely this can’t fail? On the contrary:

    wrong error: Go home, Go, you're drunk

    We might justifiably feel a little puzzled at this. We have two values constructed exactly the same way, and yet they don’t compare equal. Why not?

    This is somewhat non-obvious behaviour, I admit, but Go is doing exactly what it’s supposed to. We can work out what’s happening, if we think it through.

    What type does errors.New return? We know it implements the interface type error, but what concrete type is it? Let’s look at the source code of errors.New to see what it does:

    func New(text string) error {
        return &errorString{text}
    }

    I bet you didn’t know the unexported struct type errorString existed, yet you’ve probably used it every day of your life as a Go programmer. But that’s not the important part.

    The important part here is that errors.New returns a pointer. So when we construct error values by calling errors.New, the results we get are pointers.

    Can you see now why they don’t compare equal? In fact, Go pointers never compare equal unless they point to the same object. That is, unless they’re literally the same memory address.

    And that won’t be the case here, because they point to two distinct instances of the errorString struct that merely happen to contain the same message. This is why it doesn’t make sense to compare error values in Go using the != operator: they’ll never be equal.

    What the programmer really wanted to know in this case was not “Do these two values point to the same piece of memory?”, but “Do these two values represent the same error?”

    How can we answer that question?

    String matching on errors is fragile

    Here’s another attempt to answer the question of whether two values represent the same error:

    func TestOpenGivesSpecificErrorStringForBogusFile(t *testing.T) {
        t.Parallel()
        want := errors.New("open bogus: no such file or directory")
        _, got := store.Open("bogus")
        if got.Error() != want.Error() {
            t.Errorf("wrong error: %v", got)
        }
    }

    (Listing store/1)

    Instead of comparing the error values directly, we compare the strings produced by each error’s Error method.

    But although this works, more or less, there’s something about it that doesn’t feel quite right. The whole point of error being an interface, after all, is that we shouldn’t have to care about what its string value is.

    So if we construct our expected error using errors.New and some fixed string, and compare it with the error result from the function, that’s really just the same as comparing two strings.

    That works great, right up until some well-meaning programmer makes a minor change to their error text, such as adding a comma. Bang! Broken tests all over the place. By doing this kind of comparison, we’ve made the test brittle.

    There are no bad ideas, as they say, but let’s keep thinking.

    Sentinel errors lose useful information

    If string matching errors makes tests fragile, then maybe we could define some named error value for store.Open to return.

    In other words, something like this:

    var ErrUnopenable = errors.New("can't open store file")

    This is called a sentinel error, and there are several examples in the standard library, such as io.EOF. It’s an exported identifier, so people using your package can compare your errors with that value.

    This makes the test quite straightforward, because we can go back to our nice, simple scheme of comparing error values directly:

    func TestOpenGivesErrUnopenableForBogusFile(t *testing.T) {
        t.Parallel()
        _, err := store.Open("bogus")
        if err != store.ErrUnopenable {
            t.Errorf("wrong error: %v", err)
        }
    }

    (Listing store/2)

    This works, provided Open does indeed return this exact value in the error case. Well, we can arrange that:

    func Open(path string) (*Store, error) {
        f, err := os.Open(path)
        if err != nil {
            return nil, ErrUnopenable // losing information about 'err'
        }
        // ...
    }

    (Listing store/2)

    This looks okay, but it’s still not ideal. Actually, we lost some information here that might have been useful: specifically, why we couldn’t open the file. We had that information in the err variable, but then we threw it away and returned the fixed value ErrUnopenable instead.

    So what do users eventually see? Just the value of ErrUnopenable, which is fixed:

    can't open store file

    This is very unhelpful. What store file? Why couldn’t it be opened? What was the error? What action could fix the problem? The user could be forgiven for feeling somewhat let down.

    DR EVIL: Right, okay, people, you have to tell me these things, all right? I’ve been frozen for thirty years, okay? Throw me a frickin’ bone here! I’m the boss. Need the info.

    —“Austin Powers: International Man of Mystery”

    Actually, it would have been more helpful simply to return the err value directly, because it already contains everything the user needs to know:

    open store.bin: permission denied

    Much more informative! Unlike ErrUnopenable, this tells us not only which specific file couldn’t be opened, but also why not.

    So a sentinel error value ErrUnopenable makes it possible to detect that kind of error programmatically, but at the expense of making the error message itself nearly useless. But did we even need to make this trade-off in the first place?

    For example, do programs that use store.Open really need to distinguish “unopenable file” errors from other kinds of errors opening a store? Or is all they care about simply that there was some error?

    Most of the time in Go programs, all we care about is that err is not nil. In other words, that there was some error. What it is specifically usually doesn’t matter, because the program isn’t going to take different actions for different errors. It’s probably just going to print the error message and then exit.

    Getting back to the store.Open test, the user-facing behaviour that we care about is that, if the store can’t be opened, Open returns some error.

    And that’s easy to detect, in a test or elsewhere. We can just compare it with nil, which is where we started:

    func TestOpenGivesNonNilErrorForBogusFile(t *testing.T) {
        t.Parallel()
        _, err := store.Open("bogus file")
        if err == nil {
            t.Error("want error opening bogus store file")
        }
    }

    (Listing store/1)

    In other words, if an error is intended to signal something to the user, then a sentinel error probably won’t be that helpful, because its value is fixed. We can’t use it to convey any dynamic information that might help the user solve the problem.

    And if the system doesn’t need to know anything about the error except that it’s not nil, then we don’t need a sentinel error at all.


    That’s it for Part 2. In Part 3, we’ll discuss error wrapping, distinguishing errors using errors.Is and errors.As, and implementing custom error types. Stay tuned!

    Package errors implements functions to manipulate errors.

    The New function creates errors whose only content is a text message.

    An error e wraps another error if e’s type has one of the methods

    Unwrap() error
    Unwrap() []error
    

    If e.Unwrap() returns a non-nil error w or a slice containing w,
    then we say that e wraps w. A nil error returned from e.Unwrap()
    indicates that e does not wrap any error. It is invalid for an
    Unwrap method to return an []error containing a nil error value.

    An easy way to create wrapped errors is to call fmt.Errorf and apply
    the %w verb to the error argument:

    wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
    

    Successive unwrapping of an error creates a tree. The Is and As
    functions inspect an error’s tree by examining first the error
    itself followed by the tree of each of its children in turn
    (pre-order, depth-first traversal).

    Is examines the tree of its first argument looking for an error that
    matches the second. It reports whether it finds a match. It should be
    used in preference to simple equality checks:

    if errors.Is(err, fs.ErrExist)
    

    is preferable to

    if err == fs.ErrExist
    

    because the former will succeed if err wraps io/fs.ErrExist.

    As examines the tree of its first argument looking for an error that can be
    assigned to its second argument, which must be a pointer. If it succeeds, it
    performs the assignment and returns true. Otherwise, it returns false. The form

    var perr *fs.PathError
    if errors.As(err, &perr) {
    	fmt.Println(perr.Path)
    }
    

    is preferable to

    if perr, ok := err.(*fs.PathError); ok {
    	fmt.Println(perr.Path)
    }
    

    because the former will succeed if err wraps an *io/fs.PathError.

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    // MyError is an error implementation that includes a time and message.
    type MyError struct {
    	When time.Time
    	What string
    }
    
    func (e MyError) Error() string {
    	return fmt.Sprintf("%v: %v", e.When, e.What)
    }
    
    func oops() error {
    	return MyError{
    		time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
    		"the file system has gone away",
    	}
    }
    
    func main() {
    	if err := oops(); err != nil {
    		fmt.Println(err)
    	}
    }
    
    Output:
    
    1989-03-15 22:30:00 +0000 UTC: the file system has gone away
    
    • Variables
    • func As(err error, target any) bool
    • func Is(err, target error) bool
    • func Join(errs …error) error
    • func New(text string) error
    • func Unwrap(err error) error
    • Package
    • As
    • Is
    • Join
    • New
    • New (Errorf)
    • Unwrap

    This section is empty.

    View Source

    var ErrUnsupported = New("unsupported operation")

    ErrUnsupported indicates that a requested operation cannot be performed,
    because it is unsupported. For example, a call to os.Link when using a
    file system that does not support hard links.

    Functions and methods should not return this error but should instead
    return an error including appropriate context that satisfies

    errors.Is(err, errors.ErrUnsupported)
    

    either by directly wrapping ErrUnsupported or by implementing an Is method.

    Functions and methods should document the cases in which an error
    wrapping this will be returned.

    As finds the first error in err’s tree that matches target, and if one is found, sets
    target to that error value and returns true. Otherwise, it returns false.

    The tree consists of err itself, followed by the errors obtained by repeatedly
    calling Unwrap. When err wraps multiple errors, As examines err followed by a
    depth-first traversal of its children.

    An error matches target if the error’s concrete value is assignable to the value
    pointed to by target, or if the error has a method As(interface{}) bool such that
    As(target) returns true. In the latter case, the As method is responsible for
    setting target.

    An error type might provide an As method so it can be treated as if it were a
    different error type.

    As panics if target is not a non-nil pointer to either a type that implements
    error, or to any interface type.

    package main
    
    import (
    	"errors"
    	"fmt"
    	"io/fs"
    	"os"
    )
    
    func main() {
    	if _, err := os.Open("non-existing"); err != nil {
    		var pathError *fs.PathError
    		if errors.As(err, &pathError) {
    			fmt.Println("Failed at path:", pathError.Path)
    		} else {
    			fmt.Println(err)
    		}
    	}
    
    }
    
    Output:
    
    Failed at path: non-existing
    

    Is reports whether any error in err’s tree matches target.

    The tree consists of err itself, followed by the errors obtained by repeatedly
    calling Unwrap. When err wraps multiple errors, Is examines err followed by a
    depth-first traversal of its children.

    An error is considered to match a target if it is equal to that target or if
    it implements a method Is(error) bool such that Is(target) returns true.

    An error type might provide an Is method so it can be treated as equivalent
    to an existing error. For example, if MyError defines

    func (m MyError) Is(target error) bool { return target == fs.ErrExist }
    

    then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
    an example in the standard library. An Is method should only shallowly
    compare err and the target and not call Unwrap on either.

    package main
    
    import (
    	"errors"
    	"fmt"
    	"io/fs"
    	"os"
    )
    
    func main() {
    	if _, err := os.Open("non-existing"); err != nil {
    		if errors.Is(err, fs.ErrNotExist) {
    			fmt.Println("file does not exist")
    		} else {
    			fmt.Println(err)
    		}
    	}
    
    }
    
    Output:
    
    file does not exist
    

    Join returns an error that wraps the given errors.
    Any nil error values are discarded.
    Join returns nil if every value in errs is nil.
    The error formats as the concatenation of the strings obtained
    by calling the Error method of each element of errs, with a newline
    between each string.

    A non-nil error returned by Join implements the Unwrap() []error method.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func main() {
    	err1 := errors.New("err1")
    	err2 := errors.New("err2")
    	err := errors.Join(err1, err2)
    	fmt.Println(err)
    	if errors.Is(err, err1) {
    		fmt.Println("err is err1")
    	}
    	if errors.Is(err, err2) {
    		fmt.Println("err is err2")
    	}
    }
    
    Output:
    
    err1
    err2
    err is err1
    err is err2
    

    New returns an error that formats as the given text.
    Each call to New returns a distinct error value even if the text is identical.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func main() {
    	err := errors.New("emit macho dwarf: elf header corrupted")
    	if err != nil {
    		fmt.Print(err)
    	}
    }
    
    Output:
    
    emit macho dwarf: elf header corrupted
    

    The fmt package’s Errorf function lets us use the package’s formatting
    features to create descriptive error messages.

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	const name, id = "bimmler", 17
    	err := fmt.Errorf("user %q (id %d) not found", name, id)
    	if err != nil {
    		fmt.Print(err)
    	}
    }
    
    Output:
    
    user "bimmler" (id 17) not found
    

    Unwrap returns the result of calling the Unwrap method on err, if err’s
    type contains an Unwrap method returning error.
    Otherwise, Unwrap returns nil.

    Unwrap only calls a method of the form «Unwrap() error».
    In particular Unwrap does not unwrap errors returned by Join.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func main() {
    	err1 := errors.New("error1")
    	err2 := fmt.Errorf("error2: [%w]", err1)
    	fmt.Println(err2)
    	fmt.Println(errors.Unwrap(err2))
    	// Output
    	// error2: [error1]
    	// error1
    }
    
    Output:
    
    

    This section is empty.

    Понравилась статья? Поделить с друзьями:
  • Справки бк ошибка при печати handled exception
  • Сработала подушка безопасности как убрать ошибку
  • Спринтер ошибка 2521 08
  • Справки бк ошибка печать невозможна
  • Сработал предохранительный прессостат ошибка е03