Рассмотрим интересную задачу, которая часто встречается на собеседованиях Go-разработчиков. На первый взгляд, код выглядит совершенно корректно, но при запуске происходит паника. Почему? Потому что Go — строго типизированный язык, который хранит не только значение, но и его тип.
Давайте посмотрим, что нужно исправить в функции, чтобы исправить ошибку, не меняя ее сигнатуру.
Условие задачи
Перед нами функция printNumber, которая принимает пустой интерфейс (interface{}). Запускаем код и ловим панику:
package main
import "fmt"
func printNumber(ptrToNumber interface{}) {
if ptrToNumber != nil {
fmt.Println(*ptrToNumber.(*int))
} else {
fmt.Println("nil")
}
}
func main() {
v := 10
printNumber(&v) // Ожидаем: 10
var pv *int
printNumber(pv) // Ожидаем: nil
pv = &v
printNumber(pv) // Ожидаем: 10
}
В чем подвох?
Проблема кроется в строке:
fmt.Println(*ptrToNumber.(*int))
Здесь используем type assertion (.(*int)), который говорит компилятору: «Я уверен, что внутри интерфейса лежит именно *int, дай доступ к нему».
Но давайте посмотрим, что реально хранится в интерфейсе в каждом вызове:
printNumber(&v)— внутри *int ✅ (указатель на int)printNumber(pv)— внутри nil (но это не просто nil, это *int со значением nil) ❌printNumber(pv)— снова *int ✅
Второй вызов приводит к панике, потому что ptrToNumber.(*int) выполняется успешно (тип-то совпадает!), но когда пытаемся разыменовать указатель (*), Go получает nil-указатель и паникует.
Диагностика
Чтобы увидеть проблему своими глазами, добавим немного отладочной печати:
func printNumber(ptrToNumber interface{}) {
if ptrToNumber != nil {
fmt.Printf("Тип: %T, значение: %v\n", ptrToNumber, ptrToNumber)
fmt.Println(*ptrToNumber.(*int))
} else {
fmt.Println("nil")
}
}
Вывод покажет:
Тип: *int, значение: 0xc00008e0a8 10 Тип: *int, значение: <nil> // Вот он, nil-указатель! panic: runtime error: invalid memory address or nil pointer dereferenced
Интерфейс не равен nil (потому что он хранит информацию о типе), но само значение внутри — nil.
Решение
Как исправить функцию, не меняя сигнатуру? Нужно проверить два условия:
- Интерфейс не пустой
- Внутри не nil-указатель
Вот элегантное решение:
func printNumber(ptrToNumber interface{}) {
// Проверяем, что интерфейс не пустой
if ptrToNumber == nil {
fmt.Println("nil")
return
}
// Пробуем получить значение как *int
if val, ok := ptrToNumber.(*int); ok {
if val != nil {
fmt.Println(*val)
} else {
fmt.Println("nil")
}
} else {
fmt.Println("ожидался *int")
}
}
Но можно сделать еще лаконичнее:
func printNumber(ptrToNumber interface{}) {
if ptrToNumber == nil {
fmt.Println("nil")
return
}
if val, ok := ptrToNumber.(*int); ok && val != nil {
fmt.Println(*val)
} else {
fmt.Println("nil")
}
}
Объяснение работы
ptrToNumber == nil— проверяем, что интерфейс вообще пустой (в нем нет ни типа, ни значения)val, ok := ptrToNumber.(*int)— безопасный type assertion, который не паникуетval != nil— проверяем, что сам указатель не нулевой
Теперь функция корректно обрабатывает все случаи:
printNumber(&v)→ 10printNumber(nil)→ nil (пустой интерфейс)printNumber(pv), где pv = nil → nilprintNumber(pv), где pv = &v → 10
Ключевой вывод
В Go интерфейс, содержащий nil-указатель, не равен nil! Это одна из самых частых ловушек для новичков. Интерфейс считается nil только когда и тип, и значение равны nil.
Сталкивались ли вы на практике с подобным поведением интерфейсов? Может быть, у вас есть свои истории отладки, связанные с type assertion? Поделитесь в комментариях — интересно почитать ваш опыт.