Интерфейсы в Go

Теория: Nil в интерфейсах

В Go значение nil обозначает отсутствие данных. Оно встречается у указателей, срезов, мап, каналов, функций и интерфейсов. Но именно с интерфейсами nil ведёт себя необычно.

Интерфейс в Go устроен как пара: тип и значение. Он считается пустым только тогда, когда отсутствуют оба. Если задан хотя бы один компонент — интерфейс уже не nil. На практике это создаёт нетривиальные ситуации при сравнении.

Частый случай — в интерфейс передаётся nil-указатель. Значения нет, но тип остаётся. Визуально всё выглядит как nil, но сравнение == nil возвращает false. И это вполне корректно с точки зрения Go.

Такое поведение может сломать логику. Программа может начать обрабатывать ошибку, которой не было, или — наоборот — проигнорировать настоящую. Компилятор и рантайм молчат. Ошибка становится видна только при выполнении, и то не сразу.

Особенно часто эта ситуация возникает с типом error. Посмотрим на пример:

type MyError struct{}

func (e *MyError) Error() string {
	return "ошибка"
}

func getError() error {
	var err *MyError = nil
	return err
}

Кажется, что getError() возвращает nil, но:

err := getError()
fmt.Println(err == nil) // false

Интерфейс err не пуст. Значения в нём нет, но тип *MyError сохранён. Такая ошибка даже получила собственное имя — interface value is not nil.

Go сравнивает и тип, и значение. Если задан хотя бы один из них — результат == nil будет ложным. Проверка не сработает, а код пойдёт по неверной ветке.

Поведение касается всех интерфейсов. Например:

var p *int = nil
handle(p)

func handle(v interface{}) {
	fmt.Println(v == nil) // false
}

Хотя pnil, переменная v уже содержит тип *int. Интерфейс непустой. Сравнение v == nil даёт false, как и в случае с error.

Чтобы такое не застало врасплох, важно помнить: интерфейс считается пустым только если в нём нет ни типа, ни значения. Всё остальное — уже не nil.

Как быть

Если функция должна вернуть пустой интерфейс — возвращай nil, а не nil-указатель. Это касается и ошибок. Простой return nil безопаснее, чем return (*MyError)(nil).

Если всё же приходится возвращать nil-указатель заранее известного типа, нужно понимать: интерфейс уже будет непустым, и проверка == nil не сработает. Такой случай должен обрабатываться отдельно.

При необходимости можно проверить содержимое интерфейса через reflect:

if reflect.ValueOf(v).IsNil() {
	// значение действительно nil
}

Во время отладки помогают fmt.Printf("%#v\n", err) и reflect.TypeOf(err) — они показывают, есть ли внутри тип и значение.

Но лучшее решение — писать код так, чтобы такие проверки вообще не понадобились. Если значение отсутствует — возвращай nil. Если оно есть — пусть будет полноценным. nil-указатели внутри интерфейсов — частый источник багов, которых можно избежать одной строчкой.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845