Описание пакета 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-existingfunc 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' найдена!")
}
Вывод:
Ошибка '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)
}
Вывод:
Код: 404, Сообщение: Not Found
Здесь 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 existfunc 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