Как стать автором
Обновить

Nil не всегда nil

Время на прочтение3 мин
Количество просмотров14K

Nil не всегда nil


"Что? Что вообще здесь написано?" спросите вы. Сейчас все разложу.


Когда начинал изучать язык — не думал что зайду в этот узкий случай. Это также не рационально как и изменять итерируемую коллекцию.


На примере:

func Foo() error {
    var err *os.PathError = nil
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)           // <nil>
    fmt.Println(err == nil) // false
}

WAT!


Что представляет собой интерфейс

Переходим в файл пакета go runtime/runtime2.go и видим:


type itab struct { // 40 bytes on a 64bit arch
    inter *interfacetype
    _type *_type
    ...
}

Интерфейс хранит в себе тип интерфейса и тип самого значение.


Значение любого интерфейса, не только error, является nil в случае когда И значение И тип являются nil.


Функция Foo возвращает nil типа *os.PathError, результат мы сравниваем с nil типа nil, откуда и следует их неравенство.


Возможно об этом многие знали, но мало кто думает как попасть в это на практике.


Мой пример

type Response struct {
    Result ResponseResult    `json:"result,omitempty"`
    Error  *ResponseError    `json:"error,omitempty"`
}

type ResponseError struct {
    Message string `json:"message"`
}

func (e *ResponseError) Error() string {
    return e.Message
}
...
func (s *NotificationService) NotifyIfError(w *ResponseWriter) error {
    ...
    var res handlers.Response
    _ = json.Unmarshal(body, &res)

    if res.Error == nil {
        return
    }

    return s.NotifyError(err)
}

Response всегда имеет результат или ошибку.


При наличии ошибки — отправляем ее куда необходимо через сервис нотификаций.
Внутри сервиса вызывается метод Error, а так как наше значение nil — получаем панику.


Что делать?

Возвращать интерфейс строго типа интерфейса.


В случае ошибки — типа ошибки.


  • Добавим обьявление типа error

func (s *NotificationService) NotifyIfError(w *ResponseWriter) error {
    ...
    var res Response
    _ = json.Unmarshal(body, &res)

        var err error = res.Error
    return s.NotifyError(err)
}

Этот прием тоже, к моему удивлению, не работает.


Получается так, что при присвоении значение в переменную err мы также передаем ей исходную информацию о типе, который не есть nil.


  • Попробуем получить из типа интерфейса наш исходный тип и проверить его значение.

func (s *NotificationService) NotifyIfError(w *ResponseWriter) error {
    ...
        if e, ok := err.(*ResponseError); ok && e == nil {
        return s.NotifyError(err)
    }

        return nil
}

Да, такой прием работает.


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


Это могут быть все ошибки из драйвера базы данных, все наши внутренние ошибки и прочий мусор.


Какой наиболее рациональный вариант я вижу:

func (s *NotificationService) NotifyIfError(w *ResponseWriter) error {
    var err error
        ...
        var res Response
        _ = json.Unmarshal(body, &res)
        if res.Error != nil {
            return s.NotifyError(err)
        }

    return nil
}

Вначале у нас объявленa переменная типа error, как оказывается со значением и типом nil.
И прежде чем передать наш тип и значение этой переменной — проверим наш тип и его значение на nil.


Это позволит нам не упасть с паникой.


Напоследок

Можно пойти еще дальше и реализовать "опциональную" ошибку у типа Response, OptionalError или ErrorOrNil, вроде такого:


func (r *Response) ErrorOrNil() error {
    if r.Error == nil {
        return nil
    }

    return r.Error
}

На заметку

В заметках Go wiki код ревью заметка в топике об интерфейса: Instead return a concrete type and let the consumer mock the producer implementation.


Отмечу, что приведенные мной выше пляски не про это.


Мои заметки позволяют не упасть с паникой когда вы знаете, что хотите вернуть интерефйс, а в случае ошибок — вы всегда хотите возвращать интерфейс.


Но если вы можете себе позволить вернуть определенный тип — возвращайте его.


Ссылки

go-internals


Я

LinkedIn
Telegram
Twitter
Github

Теги:
Хабы:
Всего голосов 28: ↑20 и ↓8+12
Комментарии4

Публикации

Истории

Работа

Go разработчик
147 вакансий

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань