Golang проверить тип ошибки

To check the TYPE of the error, use errors.As

As finds the first error in err’s chain that matches target […] ​An error matches target if the error’s concrete value is assignable to the value pointed to by target

Of course type identity is a condition for assignability.

So it would look like:

target := &model.ModelMissingError{} 
if errors.As(err, &target) {
    fmt.Println(target) // no model found for id
}

Pay attention to the two uses of & in the example above. This is because:

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

In your case, you declared Error() string method on the pointer receiver, therefore «a pointer to the type that implements the error interface» to satisfy As is **ModelMissingError. So you need to address twice.


The other method errors.Is checks for value equality.

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.

This is useful for example in case of fixed error values, e.g. errors declared as var or const like the standard library io.EOF. As an example:

var ErrModelMissing = errors.New("no model found for id")

func foo() {
    err := bar()
    if errors.Is(err, ErrModelMissing) {
        fmt.Println(err) // no model found for id
    }
}

Consider that the usefulness of Go 1.13 errors.As and errors.Is lies in error unwrapping. If you inspect the error at the top of an arbitrarily long call stack, you must remember that the original error may become wrapped into other errors while being bubbled up. Then directly checking for equality or type assignability is not enough.

    err1 := fmt.Errorf("wrapped: %w", &ModelMissingError{})
    target := &ModelMissingError{}
    fmt.Println(errors.As(err1, &target)) // true
    
    err2 := fmt.Errorf("wrapped: %w", FixedError)
    fmt.Println(errors.Is(err2, FixedError)) // true
    fmt.Println(err2 == FixedError) // false

Additionally, the package github.com/pkg/errors is compatible with errors.As and errors.Is:

// import pkgerr "github.com/pkg/errors"

err3 := pkgerr.Wrap(pkgerr.Wrap(&ModelMissingError{}, "wrapped 1"), "wrapped 2")
fmt.Println(errors.As(err3, &target)) // true

Playground: https://play.golang.org/p/FEzggdBLCqq


Naturally, if you know for sure that the error is not wrapped, a good old type assertion works just as fine:

if myerr, ok := err.(*model.ModelMissingError); ok {
    // handle myerr
}

Рассказываем и показываем на практических примерах, как обрабатывать различные ошибки в языке программирования Go.

Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.

Выглядит это так:

			type error interface {  
    Error() string
}
		

В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.

  1. Как обрабатывать ошибки в Go?
  2. Создание ошибок
  3. Оборачивание ошибок
  4. Проверка типов с Is и As
  5. Сторонние пакеты по работе с ошибками в Go
  6. Defer, panic and recover
  7. После изложенного

Как обрабатывать ошибки в Go?

Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error и проверить её на nil:

			if err != nil {
	return err
}
		

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

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

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

Создание ошибок

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

  • errors.New
  • fmt.Errorf

Метод errors.New() создаёт ошибку, принимая в качестве параметра текстовое сообщение.

			package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.New("emit macho dwarf: elf header corrupted")
	fmt.Print(err)
}
		

С помощью метода fmt.Errorf можно добавить дополнительную информацию об ошибке. Данные будут храниться внутри одной конкретной строки.

			package main

import (
	"fmt"
)

func main() {
	const name, id = "bueller", 17
	err := fmt.Errorf("user %q (id %d) not found", name, id)
	fmt.Print(err)
}
		

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

Оборачивание ошибок

Поскольку Error — это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.

			package main

import (
  "fmt"
)

type NotFoundError struct {
  UserId int
}

func (err NotFoundError) Error() string {
  return fmt.Sprintf("user with id %d not found", err.UserId)
}

func SearchUser(id int) error {
  // some logic for search
  // ...
  // if not found
  var err NotFoundError
  err.UserId = id
  return err
}

func main() {
  const id = 17
  err := SearchUser(id)
  if err != nil {
     fmt.Println(err)
     //type error checking
     notFoundErr, ok := err.(NotFoundError)
     if ok {
        fmt.Println(notFoundErr.UserId)
     }
  }
}
		

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

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

Также для оборачивания ошибок в fmt.Errorf есть плейсхолдер %w, который и позволяет произвести такую упаковку.:

			package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	err := openFile("non-existing")
	if err != nil {
		fmt.Println(err.Error())
		// get internal error
		fmt.Println(errors.Unwrap(err))
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}
	return nil
}
		

Проверка типов с Is и As

В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:

  • errors.Is
  • errors.As

Метод errors.Is, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:

			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)
		}
	}
}
		

Если это будет та же самая ошибка, то функция вернёт true, если нет — false.

errors.As проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):

			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)
		}
	}
}
		

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

Сторонние пакеты по работе с ошибками в Go

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

В качестве примера можно посмотреть на пакет pkg/errors. Одной из его способностей является логирование stack trace:

			package main

import (
	"fmt"
	"github.com/pkg/errors"
)

func main() {
	err := errors.Errorf("whoops: %s", "foo")
	fmt.Printf("%+v", err)
}
	// Example output:
	// whoops: foo
	// github.com/pkg/errors_test.ExampleErrorf
	//         /home/dfc/src/github.com/pkg/errors/example_test.go:101
	// testing.runExample
	//         /home/dfc/go/src/testing/example.go:114
	// testing.RunExamples
	//         /home/dfc/go/src/testing/example.go:38
	// testing.(*M).Run
	//         /home/dfc/go/src/testing/testing.go:744
	// main.main
	//         /github.com/pkg/errors/_test/_testmain.go:102
	// runtime.main
	//         /home/dfc/go/src/runtime/proc.go:183
	// runtime.goexit
	//         /home/dfc/go/src/runtime/asm_amd64.s:2059
		

Defer, panic and recover

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

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

Для работы с такими ошибками существует механизм «defer, panic, recover»

Defer

Defer помещает все вызовы функции в стек приложения. При этом отложенные функции выполняются в обратном порядке — независимо от того, вызвана паника или нет. Это бывает полезно при очистке ресурсов:

			package main

import (
    "fmt"
    "os"
)

func main() {
    f := createFile("/tmp/defer.txt")
    defer closeFile(f)
    writeFile(f)
}

func createFile(p string) *os.File {
    fmt.Println("creating")
    f, err := os.Create(p)
    if err != nil {
        panic(err)
    }
    return f
}

func writeFile(f *os.File) {
    fmt.Println("writing")
    fmt.Fprintln(f, "data")
}

func closeFile(f *os.File) {
    fmt.Println("closing")
    err := f.Close()
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        os.Exit(1)
    }
}
		

Panic

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

Например, Golang будет «паниковать», когда число делится на ноль:

			panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
		

Также панику можно вызвать явно с помощью метода panic(). Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.

Recover

Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.

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

			package main

import (
	"errors"
	"fmt"
)

func A() {
	defer fmt.Println("Then we can't save the earth!")
	defer func() {
		if x := recover(); x != nil {
			fmt.Printf("Panic: %+v\n", x)
		}
	}()
	B()
}

func B() {
	defer fmt.Println("And if it keeps getting hotter...")
	C()
}

func C() {
	defer fmt.Println("Turn on the air conditioner...")
	Break()
}

func Break() {
	defer fmt.Println("If it's more than 30 degrees...")
	panic(errors.New("Global Warming!!!"))
}

func main() {
	A()
}
		

После изложенного

Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.

Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.

Время на прочтение
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 мы экспериментируем, упрощаем и отгружаем. И теперь, отгрузив эти изменения, мы принимаемся за новые эксперименты.

The Go Blog

Introduction

If you have written any Go code you have probably encountered the built-in error type.
Go code uses error values to indicate an abnormal state.
For example, the os.Open function returns a non-nil error value when
it fails to open a file.

func Open(name string) (file *File, err error)

The following code uses os.Open to open a file.
If an error occurs it calls log.Fatal to print the error message and stop.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

You can get a lot done in Go knowing just this about the error type,
but in this article we’ll take a closer look at error and discuss some
good practices for error handling in Go.

The error type

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 error type, as with all built in types,
is predeclared
in the universe block.

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
}

You can construct one of these values with the errors.New function.
It takes a string that it converts to an errors.errorString and returns
as an error value.

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

Here’s how you might use errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

A caller passing a negative argument to Sqrt receives a non-nil error
value (whose concrete representation is an errors.errorString value).
The caller can access the error string (“math:
square root of…”) by calling the error’s Error method,
or by just printing it:

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

The fmt package formats an error value by calling its Error() string method.

It is the error implementation’s responsibility to summarize the context.
The error returned by os.Open formats as “open /etc/passwd:
permission denied,” not just “permission denied.” The error returned by
our Sqrt is missing information about the invalid argument.

To add that information, a useful function is the fmt package’s Errorf.
It formats a string according to Printf’s rules and returns it as an error
created by errors.New.

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

In many cases fmt.Errorf is good enough,
but since error is an interface, you can use arbitrary data structures as error values,
to allow callers to inspect the details of the error.

For instance, our hypothetical callers might want to recover the invalid
argument passed to Sqrt.
We can enable that by defining a new error implementation instead of using
errors.errorString:

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

A sophisticated caller can then use a type assertion
to check for a NegativeSqrtError and handle it specially,
while callers that just pass the error to fmt.Println or log.Fatal will
see no change in behavior.

As another example, the json
package specifies a SyntaxError type that the json.Decode function returns
when it encounters a syntax error parsing a JSON blob.

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

The Offset field isn’t even shown in the default formatting of the error,
but callers can use it to add file and line information to their error messages:

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

(This is a slightly simplified version of some actual code
from the Camlistore project.)

The error interface requires only a Error method;
specific error implementations might have additional methods.
For instance, the net package returns errors of type error,
following the usual convention, but some of the error implementations have
additional methods defined by the net.Error interface:

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

Client code can test for a net.Error with a type assertion and then distinguish
transient network errors from permanent ones.
For instance, a web crawler might sleep and retry when it encounters a temporary
error and give up otherwise.

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Simplifying repetitive error handling

In Go, error handling is important. The language’s design and conventions
encourage you to explicitly check for errors where they occur (as distinct
from the convention in other languages of throwing exceptions and sometimes catching them).
In some cases this makes Go code verbose,
but fortunately there are some techniques you can use to minimize repetitive error handling.

Consider an App Engine
application with an HTTP handler that retrieves a record from the datastore
and formats it with a template.

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

This function handles errors returned by the datastore.Get function and
viewTemplate’s Execute method.
In both cases, it presents a simple error message to the user with the HTTP
status code 500 (“Internal Server Error”).
This looks like a manageable amount of code,
but add some more HTTP handlers and you quickly end up with many copies
of identical error handling code.

To reduce the repetition we can define our own HTTP appHandler type that includes an error return value:

type appHandler func(http.ResponseWriter, *http.Request) error

Then we can change our viewRecord function to return errors:

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

This is simpler than the original version,
but the http package doesn’t understand
functions that return error.
To fix this we can implement the http.Handler interface’s ServeHTTP
method on appHandler:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

The ServeHTTP method calls the appHandler function and displays the
returned error (if any) to the user.
Notice that the method’s receiver, fn, is a function.
(Go can do that!) The method invokes the function by calling the receiver
in the expression fn(w, r).

Now when registering viewRecord with the http package we use the Handle
function (instead of HandleFunc) as appHandler is an http.Handler
(not an http.HandlerFunc).

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

With this basic error handling infrastructure in place,
we can make it more user friendly.
Rather than just displaying the error string,
it would be better to give the user a simple error message with an appropriate HTTP status code,
while logging the full error to the App Engine developer console for debugging purposes.

To do this we create an appError struct containing an error and some other fields:

type appError struct {
    Error   error
    Message string
    Code    int
}

Next we modify the appHandler type to return *appError values:

type appHandler func(http.ResponseWriter, *http.Request) *appError

(It’s usually a mistake to pass back the concrete type of an error rather than error,
for reasons discussed in the Go FAQ,
but it’s the right thing to do here because ServeHTTP is the only place
that sees the value and uses its contents.)

And make appHandler’s ServeHTTP method display the appError’s Message
to the user with the correct HTTP status Code and log the full Error
to the developer console:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

Finally, we update viewRecord to the new function signature and have it
return more context when it encounters an error:

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

This version of viewRecord is the same length as the original,
but now each of those lines has specific meaning and we are providing a
friendlier user experience.

It doesn’t end there; we can further improve the error handling in our application. Some ideas:

  • give the error handler a pretty HTML template,

  • make debugging easier by writing the stack trace to the HTTP response when the user is an administrator,

  • write a constructor function for appError that stores the stack trace for easier debugging,

  • recover from panics inside the appHandler,
    logging the error to the console as “Critical,” while telling the user “a
    serious error has occurred.” This is a nice touch to avoid exposing the
    user to inscrutable error messages caused by programming errors.
    See the Defer, Panic, and Recover
    article for more details.

Conclusion

Proper error handling is an essential requirement of good software.
By employing the techniques described in this post you should be able to
write more reliable and succinct Go code.

Since Go 1.13, we have new helpful ways to find out the type of error, even if we use error wrapping. If we want to check if a given error matches another specific error, we need to use Is() function from the errors package. If we are interested in whether the error is of a given type, we should call the As() function.

Is() function

In the example below, we can see that the function validateInput returns an error for badInput. This error is ErrBadInput wrapped in an error created by fmt.Errorf(). Using the Is(err, target error) bool function, we can detect the ErrBadInput even if it is wrapped since this function checks if any error in the chain of wrapped errors matches the target. Therefore, this form should be preferable to comparison if err == ErrBadInput.

package main

import (
    "errors"
    "fmt"
)

const badInput = "abc"

var ErrBadInput = errors.New("bad input")

func validateInput(input string) error {
    if input == badInput {
        return fmt.Errorf("validateInput: %w", ErrBadInput)
    }
    return nil
}

func main() {
    input := badInput

    err := validateInput(input)
    if errors.Is(err, ErrBadInput) {
        fmt.Println("bad input error")
    }
}

Output:

As() function

Similar to Is(), the As(err error, target interface{}) bool checks if any error in the chain of wrapped errors matches the target. The difference is that this function checks whether the error has a specific type, unlike the Is(), which examines if it is a particular error object. Because As considers the whole chain of errors, it should be preferable to the type assertion if e, ok := err.(*BadInputError); ok.

target argument of the As(err error, target interface{}) bool function should be a pointer to the error type, which in this case is *BadInputError

package main

import (
    "errors"
    "fmt"
)

const badInput = "abc"

type BadInputError struct {
    input string
}

func (e *BadInputError) Error() string {
    return fmt.Sprintf("bad input: %s", e.input)
}

func validateInput(input string) error {
    if input == badInput {
        return fmt.Errorf("validateInput: %w", &BadInputError{input: input})
    }
    return nil
}

func main() {
    input := badInput

    err := validateInput(input)
    var badInputErr *BadInputError
    if errors.As(err, &badInputErr) {
        fmt.Printf("bad input error occured: %s\n", badInputErr)
    }
}

Output:

bad input error occured: bad input: abc

Понравилась статья? Поделить с друзьями:
  • Goldcard jgd4s g ошибка e 1003
  • Godex ez 2350i ошибка проверьте этикет ленту
  • Golang тестирование ошибок
  • Godex g500 коды ошибок
  • Godex g300 ошибки