Перейти к содержанию
Educora
Средний16 мин8 / 10

Обработка ошибок

Пойми, почему в Go ошибки — обычные значения, и научись работать с `errors.New`, `fmt.Errorf` и `%w`, `errors.Is` и `errors.As`, а также с `panic` и `recover`.

Проверь себя
В этом уроке ты узнаешь
  • Писать функции, возвращающие ошибки, и проверять их через if err != nil
  • Оборачивать ошибки через %w и проверять их с помощью errors.Is и errors.As
  • Объяснять, когда используются panic и recover

Файла может не оказаться, интернет может пропасть, пользователь может ввести буквы вместо числа. Многие языки в таких случаях выбрасывают исключение и ловят его через try/catch. Go выбрал другой путь: ошибка — это обычное значение, которое возвращает функция. Код становится чуть длиннее, зато ты ясно видишь, где может возникнуть каждая ошибка и как она обрабатывается.

Ошибка — обычное значение

В Go error — простой интерфейс: любой тип с методом Error() string является ошибкой. Правило такое: функция, которая может завершиться неудачей, возвращает ошибку последним результатом, а при успехе — nil. Вызывающий код сразу проверяет: if err != nil { ... }. Простейшую ошибку создаёт errors.New("текст").

Go
package main

import (
	"errors"
	"fmt"
)

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 4)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", result)

	if _, err := divide(1, 0); err != nil {
		fmt.Println("error:", err)
	}
}
Ожидаемый результат
result: 2.5
error: division by zero

Обрати внимание: при ошибке в качестве результата возвращается 0, но вызывающий код на него не смотрит — сначала проверяется err. Основной код остаётся у левого края, а ошибка обрабатывается сразу, с быстрым выходом.

Обёртывание ошибок: %w и errors.Is

Когда ошибка передаётся наверх, полезно добавить контекст: чем мы занимались, когда она возникла? fmt.Errorf("load profile %d: %w", id, err) создаёт новую ошибку и оборачивает (wrap) в неё исходную. Спецификатор %w важен: именно он сохраняет исходную ошибку. Затем errors.Is(err, ErrNotFound) проходит всю цепочку и сообщает, есть ли внутри искомая ошибка. Заранее объявленные ошибки такого рода называют сигнальными (sentinel errors), а их имена обычно начинаются с Err.

Go
package main

import (
	"errors"
	"fmt"
)

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

func findStudent(id int) (string, error) {
	students := map[int]string{1: "Aysel", 2: "Murad"}
	name, ok := students[id]
	if !ok {
		return "", ErrNotFound
	}
	return name, nil
}

func loadProfile(id int) (string, error) {
	name, err := findStudent(id)
	if err != nil {
		return "", fmt.Errorf("load profile %d: %w", id, err)
	}
	return "Profile of " + name, nil
}

func main() {
	p, err := loadProfile(2)
	fmt.Println(p, err)
	_, err = loadProfile(7)
	fmt.Println(err)
	fmt.Println(errors.Is(err, ErrNotFound), err == ErrNotFound)
}
Ожидаемый результат
Profile of Murad <nil>
load profile 7: not found
true false

Посмотри на последнюю строку: обёрнутая ошибка уже не равна ErrNotFound (== даёт false), но errors.Is находит её внутри цепочки. Поэтому ошибки всегда проверяй через errors.Is.

Хрупко: сравнение текста
if err != nil && err.Error() == "not found" {
	fmt.Println("no such student")
}
Надёжно: errors.Is
if errors.Is(err, ErrNotFound) {
	fmt.Println("no such student")
}
Как только добавляется контекст, текст меняется (load profile 7: not found), и первый вариант перестаёт работать. А errors.Is распознаёт и обёрнутые ошибки.

Свои типы ошибок и errors.As

Иногда об ошибке нужно знать больше, чем её текст: какое поле неверно, какое значение пришло? Тогда создай собственный тип ошибки — структуру с методом Error() string. Чтобы достать такую ошибку из цепочки обёрток, используют errors.As: если в цепочке найдётся ошибка подходящего типа, функция запишет её в твою переменную и вернёт true.

Go
package main

import (
	"errors"
	"fmt"
)

type ValidationError struct {
	Field string
	Value int
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("invalid %s: %d", e.Field, e.Value)
}

func setAge(age int) error {
	if age < 0 || age > 120 {
		return &ValidationError{Field: "age", Value: age}
	}
	return nil
}

func main() {
	err := fmt.Errorf("register user: %w", setAge(-3))
	fmt.Println(err)
	var ve *ValidationError
	if errors.As(err, &ve) {
		fmt.Println("field:", ve.Field, "value:", ve.Value)
	}
	fmt.Println(setAge(30) == nil)
}
Ожидаемый результат
register user: invalid age: -3
field: age value: -3
true
ИнструментЧто делает
errors.New("...")создаёт простую ошибку
fmt.Errorf("ctx: %w", err)добавляет контекст и оборачивает ошибку
errors.Is(err, ErrX)есть ли в цепочке конкретная ошибка?
errors.As(err, &target)достаёт из цепочки ошибку заданного типа

panic и recover

panic — аварийная ситуация, которая прерывает нормальную работу программы: выход за границы массива, nil-указатель, целочисленное деление на ноль. Во время паники функции покидаются одна за другой, но их отложенные (defer) вызовы выполняются. Если отложенная функция вызовет recover(), паника прекращается, и программа продолжает работу.

Go
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	result = a / b
	return result, nil
}

func main() {
	fmt.Println(safeDivide(10, 2))
	fmt.Println(safeDivide(1, 0))
	fmt.Println("program continues")
}
Ожидаемый результат
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues

safeDivide(1, 0) вызвала панику, но отложенная функция перехватила её через recover и записала обычную ошибку в именованный результат err. Именованный результат здесь важен: именно благодаря ему отложенная функция может изменить возвращаемое значение.

Главное

  • Ошибка — значение с методом Error() string; функция возвращает её последним результатом, а при успехе — nil.
  • Сразу проверяй if err != nil и передавай ошибку наверх с контекстом: fmt.Errorf("...: %w", err).
  • Обёрнутые ошибки проверяй через errors.Is (конкретная ошибка) и errors.As (тип ошибки), а не через == или сравнение текста.
  • Собственный тип ошибки может нести дополнительные сведения, например поле и значение.
  • panic — только для аварийных ситуаций; recover может перехватить её лишь внутри отложенной функции.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Как функция в Go, которая может завершиться неудачей, обычно сообщает об ошибке?