Одна из самых коварных ловушек в Go связана с проверкой ошибок на nil. Казалось бы, что может быть проще: передаем nil в функцию, которая проверяет ошибку, и ожидаем вывода «ok». Однако на практике такой код может привести к панике. Рассмотрим конкретный пример из мира тестирования.
- Постановка задачи
- Детальный разбор
- Природа интерфейсов в Go
- Что происходит в коде?
- Создание переменной
- Передача в функцию
- Проверка внутри функции
- Вывод ошибки
- Почему это проблема в тестировании
- Как исправить
- Вариант 1. Возвращать nil-интерфейс
- Вариант 2. Проверять через приведение типа
- Вариант 3. Использовать интерфейс для проверки
- Ключевые выводы
Постановка задачи
Дан следующий код:
type MyError struct{}
func (e *MyError) Error() string {
return "my error"
}
func CheckError(err error) {
if err != nil {
panic(err)
}
fmt.Println("ok")
}
func TestCollect(t *testing.T) {
var err *MyError
CheckError(err)
}
Вопрос: что произойдет при выполнении теста TestCollect?
panic: my error
Несмотря на то, что переменная err явно инициализирована как nil, функция CheckError обрабатывает ее как ненулевую ошибку.
Детальный разбор
Природа интерфейсов в Go
Чтобы понять этот парадокс, необходимо вспомнить, как устроены интерфейсы в Go. Интерфейс — это структура данных, состоящая из двух полей:
- type (тип) — указатель на информацию о типе данных
- value (значение) — указатель на само значение
Интерфейс считается равным nil только в одном случае: когда и тип, и значение равны nil.
Что происходит в коде?
Создание переменной
var err *MyError
Переменная err имеет конкретный тип *MyError и значение nil. Это обычный nil-указатель.
Передача в функцию
CheckError(err)
Параметр функции err имеет тип error — это интерфейс. При передаче аргумента происходит неявное приведение типов. Создается интерфейсная переменная, которая содержит:
- type: *MyError
- value: nil
Важно: интерфейс получил информацию о типе! Значит, он больше не пустой!
Проверка внутри функции
if err != nil {
panic(err)
}
Проверка err != nil смотрит на интерфейс целиком. Поскольку в интерфейсе указан тип (*MyError), он не равен nil, даже если значение внутри — nil. Условие срабатывает, и вызывается panic(err).
Вывод ошибки
При панике вызывается Error() string, который возвращает "my error".
Почему это проблема в тестировании
В тестах такая ситуация особенно опасна, потому что часто встречается паттерн:
var err *MyError
result, err := someFunction()
if err != nil {
t.Fatal(err)
}
Если в someFunction() возвращает nil указатель на MyError, то err != nil будет true, хотя фактически ошибки нет. Тест упадет ложно.
Как исправить
Вариант 1. Возвращать nil-интерфейс
Функции, возвращающие ошибки, всегда должны возвращать nil интерфейс, а не nil-указатель на конкретный тип:
func someFunction() error {
var err *MyError
// ...
return err // ПЛОХО: вернет интерфейс с типом *MyError
}
func someFunctionFixed() error {
var err *MyError
// ...
if err != nil {
return err // ХОРОШО: возвращает только если ошибка не nil
}
return nil // ХОРОШО: возвращает чистый nil
}
Вариант 2. Проверять через приведение типа
Если вы не можете изменить возвращаемое значение, проверяйте через type assertion:
func CheckError(err error) {
if err == nil {
fmt.Println("ok")
return
}
// Проверяем, не является ли ошибка nil-указателем
if _, ok := err.(*MyError); ok {
// Это наш тип, нужно проверить значение
fmt.Println("ok") // или обработать по-другому
return
}
panic(err)
}
Вариант 3. Использовать интерфейс для проверки
Более общее решение — проверять, является ли ошибка nil-значением:
func IsNilError(err error) bool {
if err == nil {
return true
}
// Получаем reflect.Value и проверяем, не nil ли значение
v := reflect.ValueOf(err)
if v.Kind() == reflect.Ptr && v.IsNil() {
return true
}
return false
}
func CheckError(err error) {
if !IsNilError(err) {
panic(err)
}
fmt.Println("ok")
}
Ключевые выводы
- Интерфейс в Go — это структура, содержащая тип и значение.
- Интерфейс равен
nilтолько когда оба поля равныnil. - Nil-указатель конкретного типа, переданный в интерфейс, создает ненулевой интерфейс.
- Функции, возвращающие error, должны возвращать именно
nil, а не nil-указатель на свой тип ошибки. - В тестах эта проблема может привести к ложным срабатываниям и трудноуловимым багам.
Понимание внутреннего устройства интерфейсов в Go критически важно для корректной обработки ошибок. Паттерн «возвращайте nil интерфейс, а не nil-указатель» должен стать привычкой для каждого Go-разработчика. Игнорирование этого правила приводит к неожиданным паникам, особенно в тестах, где кажущийся безопасным код внезапно начинает валиться.
А вы сталкивались с такой проблемой в своих проектах? Как вы отлаживали подобные случаи, когда err != nil оказывался true для фактически пустой ошибки? Поделитесь опытом в комментариях.