Материал проходит вычитку — возможны неточности.
Урок 44 из 118

Typed nil

Ночью упавший S3-клиент прошёл все проверки и уронил обработчик загрузки. Утром команда разбирала инцидент и нашла причину: пустой клиент влез в интерфейс, а тот считал его настоящим.

Главная мысль

интерфейс, внутри которого лежит nil-указатель конкретного типа, не равен nil — и проверка if err != nil на нём срабатывает на пустом месте.

Nil-указатель в интерфейсе

var client *S3Client
var st Storage = client // st не nil!
fmt.Println(st == nil) // false

Интерфейс — пара «тип + значение» из прошлого урока (в памяти — два слова). st хранит: тип *S3Client и данные nil. Сам интерфейс при этом не nil: он знает, какого типа значение внутри, просто значение — пустое. Именно так упавший клиент хранилища прошёл все проверки в проде — а потом уронил обработчик, когда тот обратился к пустым данным.

Классическая ловушка с ошибками

error — это интерфейс с одним методом Error string (полный разговор — в). Чтобы ловушка сработала, нужен тип, который его реализует:

// отдельный тип ошибки, а не запись о файле как ошибка
type fileErr struct{}
func (*fileErr) Error() string { return "файл не найден" }

func findFile(name string) error {
    var err *fileErr
    if something {
        err = &fileErr{}
    }
    return err // вернули типизированный nil как ошибку
}

err := findFile("report.pdf")
if err != nil {
    // сработает, хотя реальной ошибки нет!
}

return err упаковывает (*fileErr)(nil) в error: тип есть, данные пустые. Наверху err != nil — истина, и вызывающий обрабатывает ошибку, которой не было. Это одна из самых известных ловушек Go.

Как лечить

Лекарство — возвращать голый nil, а не типизированный:

func findFile(name string) error {
    var err *fileErr
    if something {
        err = &fileErr{}
    }
    if err == nil {
        return nil // голый nil — интерфейс станет по-настоящему nil
    }
    return err
}

Правило простое: функция, возвращающая интерфейс, должна возвращать nil как голый nil, а не nil-указатель конкретного типа. Если ты видишь if err != nil, срабатывающее «на пустом месте» — ищи функцию, которая вернула типизированный nil.

В следующем уроке

Вы узнаете:

  • почему fmt.Println на typed nil печатает <nil>
  • почему компилятор не защищает от typed nil
<!-- wp:course-hub/quiz {"title":"Проверьте себя","questions":[{"text":"var p *Person; var s Speaker = p. Чему равно s == nil?","options":[{"text":"false — динамический тип *Person заполнен, данные nil","correct":true,"comment":"Интерфейс знает, какого типа значение внутри, просто значение пустое — сам интерфейс не nil."},{"text":"true","correct":false},{"text":"Ошибка компиляции","correct":false},{"text":"Паника","correct":false}]},{"text":"fmt.Println(err) на typed nil (метод Error паникует на nil-получателе). Что напечатается?","options":[{"text":" — fmt вызывает Error(), тот паникует, fmt перехватывает панику и печатает ","correct":true,"comment":"%T на typed nil покажет конкретный тип, а на голом nil — ; так их и различают."},{"text":"Паника уронит процесс","correct":false},{"text":"user not found","correct":false},{"text":"Пустая строка","correct":false}]},{"text":"var u *User; var g Greeter = u; метод Greet с проверкой u == nil. Что вернёт g.Greet()?","options":[{"text":"«никого» — метод легально вызывается на nil-получателе, а проверка внутри защищает","correct":true,"comment":"Интерфейс знает тип *User, данные nil; без проверки было бы обращение к nil и паника."},{"text":"Панику — нельзя вызывать метод на nil","correct":false},{"text":"Пустую строку","correct":false},{"text":"Ошибку компиляции","correct":false}]}]} /-->
Итоги

Интерфейс с nil-указателем внутри ≠ nil: тип заполнен, данные пустые. Лечится возвратом голого nil.

Наш сайт собирает куки. Чтобы тебе было удобнее им пользоваться. Нажимая на кнопку, ты даёшь разрешение собирать куки