Описание пакета error языка программирования Go
Функция New
создает ошибки, содержащие только текстовое сообщение.
Ошибка e
оборачивает другую ошибку, если тип e
имеет один из методов:
Unwrap() error
Unwrap() []error
Если e.Unwrap()
возвращает не nil
ошибку w
или срез, содержащий w
, то говорится, что e
оборачивает w
. Возвращение nil
ошибки из e.Unwrap()
указывает на то, что e
не оборачивает никакую ошибку. Недопустимо, чтобы метод Unwrap()
возвращал срез, содержащий nil
значение ошибки.
Легкий способ создать обернутые ошибки — вызвать fmt.Errorf
и применить шаблон %w
к аргументу ошибки:
wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
Последовательное разворачивание ошибки создает дерево. Функции Is
и As
исследуют дерево ошибки, проверяя сначала саму ошибку, а затем дерево каждого из ее потомков по очереди (префиксный, глубинный обход).
См. https://go.dev/blog/go1.13-errors для более глубокого обсуждения философии оборачивания и когда следует оборачивать.
Функция Is
исследует дерево своего первого аргумента в поисках ошибки, совпадающей со вторым. Она сообщает, найдено ли совпадение. Ее следует использовать вместо простых проверок на равенство:
if errors.Is(err, fs.ErrExist)
лучше, чем
if err == fs.ErrExist
потому что первая будет успешной, если err
оборачивает io/fs.ErrExist
.
Функция As
исследует дерево своего первого аргумента в поисках ошибки, которую можно присвоить второму аргументу, который должен быть указателем. Если она успешна, она выполняет присваивание и возвращает true
. В противном случае она возвращает false
. Форма
var perr *fs.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path)
}
лучше, чем
if perr, ok := err.(*fs.PathError); ok {
fmt.Println(perr.Path)
}
потому что первая будет успешной, если err оборачивает *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
Переменные
var ErrUnsupported = New("unsupported operation")
ErrUnsupported
указывает на то, что запрашиваемая операция не может быть выполнена, потому что она не поддерживается. Например, вызов os.Link
при использовании файловой системы, которая не поддерживает жесткие ссылки.
Функции и методы не должны возвращать эту ошибку, а вместо этого должны возвращать ошибку, включающую соответствующий контекст, который удовлетворяет
errors.Is(err, errors.ErrUnsupported)
либо путем прямого оборачивания ErrUnsupported
, либо путем реализации метода Is
.
Функции и методы должны документировать случаи, в которых будет возвращена ошибка, оборачивающая эту.
Функции
func As
Объяснение As
📌 Пример 1: Простая кастомная ошибка
Допустим, у нас есть своя ошибка с дополнительными полями:
type NetworkError struct {
Code int
Message string
}
// Реализуем метод Error(), чтобы NetworkError соответствовала интерфейсу error
func (e *NetworkError) Error() string {
return fmt.Sprintf("Network error %d: %s", e.Code, e.Message)
}
Теперь создадим ошибку и проверим её тип с помощью As
:
func main() {
err := &NetworkError{Code: 404, Message: "Not Found"} // создаём ошибку
var netErr *NetworkError
if errors.As(err, &netErr) { // проверяем тип и извлекаем
fmt.Printf("Ошибка сети! Код: %d, Текст: %s\n", netErr.Code, netErr.Message)
} else {
fmt.Println("Это не ошибка сети")
}
}
Вывод:
Ошибка сети! Код: 404, Текст: Not Found
👉 Что произошло?
errors.As
проверила, чтоerr
имеет тип*NetworkError
.- Если да — сохранила её в
netErr
, и мы можем использовать её поля (Code
,Message
).
📌 Пример 2: Ошибка обёрнута в другую ошибку
Часто ошибки “заворачивают” в другие ошибки с помощью fmt.Errorf
и %w
. As
умеет “пробираться” через такие обёртки.
func fetchData() error {
return &NetworkError{Code: 500, Message: "Server Error"}
}
func main() {
err := fetchData()
wrappedErr := fmt.Errorf("не удалось получить данные: %w", err) // оборачиваем
var netErr *NetworkError
if errors.As(wrappedErr, &netErr) { // всё равно находит NetworkError внутри!
fmt.Printf("Ошибка сети: %d - %s\n", netErr.Code, netErr.Message)
}
}
Вывод:
Ошибка сети: 500 - Server Error
👉 Почему это полезно?
Даже если ошибка была обёрнута (%w
), As
“достанет” её исходный тип.
📌 Пример 3: Проверка нескольких типов ошибок
Иногда ошибка может быть одного из нескольких типов. As
позволяет это проверить.
type TimeoutError struct {
TimeoutSec int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("Timeout after %d sec", e.TimeoutSec)
}
func main() {
err := &TimeoutError{TimeoutSec: 30} // допустим, это наша ошибка
// Пробуем разные типы:
var timeoutErr *TimeoutError
var networkErr *NetworkError
if errors.As(err, &timeoutErr) {
fmt.Println("Это TimeoutError:", timeoutErr.TimeoutSec)
}
if errors.As(err, &networkErr) {
fmt.Println("Это NetworkError:", networkErr.Code)
} else {
fmt.Println("Это не NetworkError") // сработает это
}
}
Вывод:
Это TimeoutError: 30
Это не NetworkError
👉 Вывод:
As
проверяет по очереди каждый тип. Если ошибка не соответствует типу — просто возвращает false
.
📌 Пример 4: Работа со стандартными ошибками
As
можно использовать и со встроенными типами ошибок, например, с *os.PathError
.
func main() {
_, err := os.Open("non-existent-file.txt") // вернёт PathError
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Ошибка файла: %s (операция: %s)\n", pathErr.Path, pathErr.Op)
}
}
Вывод (примерно):
Ошибка файла: non-existent-file.txt (операция: open)
🎯 Итог: Когда использовать As
?
- Если нужно проверить тип ошибки (например,
MyCustomError
). - Если нужно достать поля ошибки (например,
err.Code
,err.TimeoutSec
). - Если ошибка может быть обёрнута (
%w
), но тебе нужна исходная.
❌ As
vs Is
Is
→ проверяет конкретное значение ошибки (err == io.EOF
).As
→ проверяет тип ошибки (err
— это*MyError
?).
func As(err error, target any) bool
As
находит первую ошибку в дереве err
, которая совпадает с target
, и если такая найдена, устанавливает target
в значение этой ошибки и возвращает true
. В противном случае возвращает false
.
Дерево состоит из err
самой по себе, за которой следуют ошибки, полученные путем многократного вызова ее метода Unwrap() error
или Unwrap() []error
. Когда err
оборачивает несколько ошибок, As
проверяет err
, за которой следует глубинный обход ее потомков.
Ошибка совпадает с target
, если конкретное значение ошибки может быть присвоено значению, на которое указывает target
, или если у ошибки есть метод As(any) bool
, такой что As(target)
возвращает true
. В последнем случае метод As
отвечает за установку target
.
Тип ошибки может предоставить метод As
, чтобы его можно было рассматривать как если бы он был другим типом ошибки.
As
вызывает панику, если target
не является не nil указателем на тип, реализующий error
, или на любой интерфейсный тип.
Пример
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
func Is
Объяснение Is и As
В Go пакет errors
предоставляет функции Is
и As
для работы с ошибками. Они помогают сравнивать ошибки и проверять их тип. Давай разберём их простым языком.
errors.Is
– проверка на конкретную ошибку
Что делает?
Проверяет, содержит ли цепочка ошибок (error
) конкретную ошибку.
Аналог из жизни:
Представь, что у тебя есть коробка с вложенными коробками (ошибка, которая обёрнута в другие ошибки). Ты ищешь конкретную вещь (например, ключ). errors.Is
рекурсивно открывает все коробки и проверяет, есть ли там нужный ключ.
Пример:
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("ошибка: %w", ErrNotFound) // оборачиваем ошибку
if errors.Is(err, ErrNotFound) {
fmt.Println("Ошибка 'not found' найдена!")
}
Вывод:
Здесь errors.Is
“распаковала” err
и обнаружила внутри ErrNotFound
.
errors.As
– проверка типа ошибки
Что делает?
Проверяет, можно ли привести ошибку к определённому типу (например, к твоей кастомной структуре-ошибке). Если да – сохраняет её в переменную.
Аналог из жизни:
У тебя есть коробка, в которой может лежать либо книга, либо ручка. Ты говоришь: “Если там книга, положи её на стол”. errors.As
проверяет тип и “кладёт” ошибку в переменную, если тип подходит.
Пример:
type MyError struct {
Code int
Msg string
}
err := &MyError{Code: 404, Msg: "Not Found"} // создаём ошибку типа *MyError
var myErr *MyError
if errors.As(err, &myErr) { // передаём указатель на переменную
fmt.Printf("Код: %d, Сообщение: %s\n", myErr.Code, myErr.Msg)
}
Вывод:
Здесь errors.As
проверила, что err
имеет тип *MyError
, и сохранила её в myErr
.
Разница между Is
и As
Функция | Для чего? | Работает с |
---|---|---|
Is |
Проверить, что ошибка именно X |
Конкретными ошибками (== ) |
As |
Проверить, что ошибка типа X |
Кастомными типами ошибок (структуры) |
Когда использовать?
Is
– если нужно проверить, что ошибка равна конкретному значению (например,io.EOF
).As
– если нужно проверить, что ошибка имеет определённый тип, и достать её поля.
Обе функции поддерживают обёрнутые ошибки (с %w
в fmt.Errorf
), поэтому они умеют “заглядывать” внутрь цепочек ошибок. 🚀
func Is(err, target error) bool
Is
сообщает, совпадает ли какая-либо ошибка в дереве err
с target
.
Дерево состоит из err
самой по себе, за которой следуют ошибки, полученные путем многократного вызова ее метода Unwrap() error
или Unwrap() []error
. Когда err
оборачивает несколько ошибок, Is
проверяет err
, за которой следует глубинный обход ее потомков.
Ошибка считается совпадающей с целевой, если она равна этой целевой или если она реализует метод Is(error) bool
, такой что Is(target)
возвращает true
.
Тип ошибки может предоставить метод Is
, чтобы его можно было рассматривать как эквивалентный существующей ошибке. Например, если MyError
определяет
func (m MyError) Is(target error) bool { return target == fs.ErrExist }
то Is(MyError{}, fs.ErrExist)
возвращает true
. См. syscall.Errno.Is
для примера в стандартной библиотеке. Метод Is
должен только поверхностно сравнивать err
и цель и не вызывать Unwrap
для обоих.
Пример
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
func Join
func Join(errs ...error) error
Join
возвращает ошибку, которая оборачивает переданные ошибки. Любые значения nil
ошибки отбрасываются. Join
возвращает nil
, если все значения в errs
равны nil
. Ошибка форматируется как конкатенация строк, полученных путем вызова метода Error
каждого элемента errs
, с новой строкой между каждой строкой.
Не nil
ошибка, возвращенная Join
, реализует метод Unwrap() []error
.
Пример:
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("first error")
err2 := errors.New("second error")
err3 := errors.New("third error")
combinedErr := errors.Join(err1, err2, err3)
fmt.Println(combinedErr)
}
func New
func New(text string) error
New
возвращает ошибку, которая форматируется как заданный текст. Каждый вызов New
возвращает уникальное значение ошибки, даже если текст идентичен.
Пример:
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("something went wrong")
fmt.Println(err)
}
Пример
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
func Unwrap
func Unwrap(err error) error
Unwrap
возвращает основную ошибку, обернутую в err
, если она существует. Если err
не оборачивает никакую ошибку, Unwrap
возвращает nil
.
Пример
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