- Ошибки: встроенный тип error
- Проверка ошибок в тестах
- Паники: аварийные ситуации
- Проверка паник в тестах
- Когда ошибка, а когда паника?
Ошибки и паники — это два разных уровня поломок в программе. Ошибка — это ожидаемая ситуация, которую можно обработать. Пользователь ввёл кривые данные, файл не открылся, база недоступна — всё это нормально, функция вернула ошибку, а вызывающий код решает, что с ней делать.
Паника — это авария. Программа оказалась в состоянии, где продолжать работу уже нельзя. Например, деление на ноль в функции, которая по контракту должна «всегда делить», или сломанное внутреннее состояние. В Go паника выбивается с помощью panic
.
В тестах важно проверять и то, и другое: иногда функция должна вернуть ошибку, а иногда она обязана упасть.
Ошибки: встроенный тип error
В Go ошибки — это обычные значения. Есть встроенный интерфейс:
type error interface {
Error() string
}
То есть всё, что умеет возвращать строку через метод Error()
, считается ошибкой. Чаще всего используют errors.New("...")
или fmt.Errorf("...")
, чтобы создать ошибку.
Функции обычно возвращают результат и ошибку:
package calc
import "errors"
// Divide делит одно число на другое.
// Если знаменатель равен нулю — возвращаем ошибку.
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Если всё хорошо — ошибка равна nil
. Если что-то пошло не так — вторая переменная хранит ошибку с описанием.
Проверка ошибок в тестах
Когда мы пишем тест, важно проверить не только «счастливый путь», но и то, как функция реагирует на неправильный ввод.
package calc_test
import (
"calc"
"testing"
)
func TestDivide_Error(t *testing.T) {
_, err := calc.Divide(10, 0)
// Проверка №1: ошибка должна быть.
// Если её нет — дальше проверять нечего.
if err == nil {
t.Fatal("ожидали ошибку, но получили nil")
}
// Проверка №2: если ошибка есть, проверяем её текст.
// Сравнение через err.Error().
want := "division by zero"
if err.Error() != want {
t.Errorf("получили %q, а хотели %q", err.Error(), want)
}
}
Почему t.Fatal и t.Errorf разные?
t.Fatal
(иt.Fatalf
) — это экстренный стоп. Тест немедленно прерывается. Мы используем его там, где без этого дальше нет смысла. В примере выше — если ошибки вообще нет, то сравнивать её текст бессмысленно.t.Error
(иt.Errorf
) — это мягкий сигнал: «что-то не так, но давай посмотрим дальше». Тест помечается как упавший, но выполнение продолжается. Это удобно, если ошибка есть, но сообщение отличается — тогда мы хотя бы увидим, что ещё делал тест.
Нужно запомнить правило: Fatal — для критичных проверок, Error — для уточняющих.
Паники: аварийные ситуации
Иногда функция должна не вернуть ошибку, а «завалиться» с паникой. Это используется редко, но бывают такие места, где программа не имеет смысла без аварийной остановки.
package calc
// MustDivide работает строго: если знаменатель 0 — сразу паника.
func MustDivide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
Проверка паник в тестах
Если такую функцию вызвать напрямую, весь тест упадёт. Чтобы проверить панику, нужно её поймать. Для этого есть связка defer
+ recover
.
func TestMustDivide_Panic(t *testing.T) {
defer func() {
// recover ловит панику, если она была.
if r := recover(); r == nil {
t.Fatal("ожидали панику, но её не было")
} else if r != "division by zero" {
t.Errorf("неожиданное сообщение паники: %v", r)
}
}()
// Этот вызов должен вызвать панику.
// Если паники не будет, defer не сработает как мы хотим.
_ = calc.MustDivide(10, 0)
}
Схема работы простая:
- Ставим
defer
с анонимной функцией. - Внутри неё вызываем
recover()
. - Если
recover()
вернулnil
→ паники не было → тест провален. - Если вернул строку → проверяем, что она совпадает с ожидаемой.
Когда ошибка, а когда паника?
Ошибки — это часть нормального хода программы. Мы можем их обработать и продолжить работу:
- Пользователь не ввёл пароль → показали сообщение.
- Файл не найден → создали новый.
- Подключение к БД не вышло → повторили через секунду.
Паники — это аварии. Тут уже нельзя «показать подсказку и продолжить». Надо валить выполнение или хотя бы остановить конкретный кусок.
- Нарушены внутренние инварианты.
- В функцию передали данные, с которыми она категорически не умеет работать.
- Ошибка в инициализации, без которой приложение бессмысленно.
В тестах важно фиксировать и такие сценарии: что код не просто «как-то себя ведёт», а строго выдаёт ошибку или строго падает.
Ошибки в Go — это встроенный механизм, с которым мы сталкиваемся постоянно. Паники — более редкий инструмент, но их тоже нужно уметь проверять.
Ошибки проверяются через if err != nil
и err.Error()
. Паники — через defer
и recover
. В тестах t.Fatal
используют для критичных проверок, когда без этого тест теряет смысл, а t.Errorf
— для уточняющих сравнений.
Так мы получаем тесты, которые покрывают весь спектр: и удачные сценарии, и ошибки, и аварийные ситуации.
Самостоятельная работа
Ниже две функции с разным поведением: одна возвращает ошибку, другая — паникует. Ваша задача — реализовать их и написать тесты.
1) Функция, которая валидирует имя и возвращает ошибку для пустой строки: ```go package validate
import "errors"
var ErrEmptyName = errors.New("empty name")
func ValidateName(name string) error { if name == "" { return ErrEmptyName } return nil } ```
2) Функция, которая извлекает элемент по индексу и паникует при выходе за границы: ```go package safe
func MustAtT any T { if i < 0 || i >= len(xs) { panic("index out of range") } return xs[i] } ```
Что проверить в тестах:
- ValidateName: ошибка на пустой строке, отсутствие ошибки на непустой.
- MustAt: корректное значение для валидного индекса; паника при индексе <0 и индексе >= len(xs).
Подсказка: для проверки паник используйте конструкцию с defer
и recover()
.
Показать решение
package validate
import "testing"
func TestValidateName_Empty(t *testing.T) {
if err := ValidateName(""); err == nil {
t.Fatalf("expected error, got nil")
} else if err != ErrEmptyName {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidateName_NonEmpty(t *testing.T) {
if err := ValidateName("Hexlet"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// файл safe/mustat_test.go
package safe
import "testing"
func TestMustAt_Valid(t *testing.T) {
xs := []int{10, 20, 30}
got := MustAt(xs, 1)
if got != 20 {
t.Fatalf("got %d, want %d", got, 20)
}
}
func TestMustAt_Panic_NegativeIndex(t *testing.T) {
xs := []int{1}
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic, got none")
}
}()
_ = MustAt(xs, -1)
}
func TestMustAt_Panic_OutOfRange(t *testing.T) {
xs := []int{1, 2}
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic, got none")
}
}()
_ = MustAt(xs, 2)
}
Дополнительные материалы
- errors — стандартный пакет
- Defer, panic и recover (статья)
- Error handling and Go (статья)
- testing — документация
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.