Go вернул ошибку, которая выглядит как nil: проверяем типизированный указатель
Проверка конфигурации прошла успешно, но вызывающий код попал в ветку if err != nil. При печати ошибка выглядит пустой, а проверка указателя внутри функции показывала true для сравнения с nil. Такая ситуация особенно неприятна в HTTP-сервисе: корректный запрос превращается в серверную ошибку из-за способа возврата значения.
Причина связана с устройством интерфейса. Переменная типа error хранит сведения о конкретном типе и его значении. Указатель *ConfigError может быть нулевым, но после помещения в интерфейс конкретный тип уже известен. В результате сам интерфейс не равен nil. Для успешного результата нужно вернуть именно пустой интерфейс ошибки, у которого нет ни значения, ни конкретного типа.
Воспроизводим сбой без сервера
Пусть функция проверяет наличие обязательного параметра подключения. При отсутствии создаётся *ConfigError, а при успехе локальная переменная указателя остаётся нулевой. Ошибочная версия возвращает её в обоих случаях. Исправленная версия возвращает указатель только при реальной ошибке, а для успеха выполняет явный return nil.
В примере метод Error умеет обработать нулевой получатель. Это позволяет безопасно напечатать проблему и увидеть её устройство. Такое поведение метода само по себе ничего не меняет в сравнении интерфейса: даже если строка ошибки пустая, наличие конкретного типа остаётся. Убирать сбой через сравнение текста ошибки поэтому бессмысленно.
package main
import (
"errors"
"fmt"
)
type ConfigError struct { Field string }
func (e *ConfigError) Error() string {
if e == nil { return "empty ConfigError pointer" }
return "missing field: " + e.Field
}
func broken(ok bool) error {
var problem *ConfigError
if !ok { problem = &ConfigError{Field: "dsn"} }
return problem
}
func validate(ok bool) error {
if !ok { return &ConfigError{Field: "dsn"} }
return nil
}
func main() {
var pointer *ConfigError
err := broken(true)
if pointer != nil || err == nil { panic("fixture did not reproduce typed nil") }
fmt.Printf("pointer nil=%v; interface nil=%v; type=%T\n", pointer == nil, err == nil, err)
if validate(true) != nil { panic("valid configuration rejected") }
failure := validate(false)
var detail *ConfigError
if !errors.As(failure, &detail) || detail == nil || detail.Field != "dsn" {
panic("missing typed failure")
}
fmt.Println("valid: nil; invalid:", failure)
}
Запустите файл командой go run main.go. Строка с типом показывает *main.ConfigError, хотя исходный указатель пуст. Две следующие проверки подтверждают нужный договор исправленной функции: корректная конфигурация даёт nil, некорректная — настоящую ошибку с доступным полем. Проверка через errors.As извлекает типизированную причину и позволяет вызывающему коду принять решение без разбора строк.
Исправляем место возникновения
Ищите функцию, которая возвращает конкретный указатель как интерфейс. Частый вариант — накопление ошибки в переменной var err *MyError с общим возвратом в конце. Проверьте все успешные пути. Если в конце должна отсутствовать ошибка, верните nil явно или храните промежуточный результат в переменной типа error, присваивая ей только реальные ошибки.
При диагностике полезнее печатать %T и %#v, чем только текст. Первый формат показывает конкретный тип, второй помогает заметить нулевой указатель. После нахождения источника уберите временную печать, особенно если структура ошибки содержит параметры подключения или другие чувствительные значения. Само сравнение err != nil в вызывающем коде оставьте: оно соответствует обычному договору Go.
Не стоит заставлять каждый HTTP-обработчик отражением проверять, не спрятан ли внутри интерфейса нулевой указатель. Это переносит ошибку производителя на всех потребителей и усложняет поддержку новых типов. В FAQ Go о nil в error описан именно этот случай. Устойчивое решение — сделать возвращаемое значение однозначным в функции, которая создаёт ошибку.
Как проверить изменение в сервисе
Добавьте тест успешного пути рядом с тестом отказа. В нём важно проверить саму ошибку на nil, а не только значение результата. Затем проверьте HTTP-ответ для тех же входных данных: если обработчик ранее превращал типизированный nil в ответ 500, корректный запрос должен снова проходить. Отдельно проверьте отказ, чтобы исправление не стало безусловным подавлением ошибок.
Когда ошибка действительно существует, её можно оборачивать с сохранением причины и распознавать дальше. Связанный материал об обёртках ошибок и errors.Is продолжает эту тему. Сначала добейтесь правильного значения на успешном пути, затем стройте классификацию реальных отказов.
Та же особенность относится к другим интерфейсам, например к возвращаемому читателю данных. Поэтому полезно сформулировать правило в команде: отсутствие результата на интерфейсной границе передаётся явным nil. Проверять его нужно до присваивания конкретного нулевого указателя интерфейсу. При ревью обращайте внимание на общий возврат типизированной переменной в конце функции, особенно если часть веток оставляет её незаполненной. Маленький тест успешного пути обычно делает такую ошибку сразу заметной.
В платном курсе «Микросервисы на Go» — откройте программу обработка ошибок связывается с HTTP- и gRPC-вызовами, дедлайнами и проверками сценариев отказа. Следующая полезная задача — определить, какой отказ можно показать клиенту, какой записать в диагностику и когда повтор запроса способен создать дубль. Это превращает знание об интерфейсах в понятное поведение нескольких взаимодействующих сервисов.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
