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 handlers.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.(*handlers.ResponseError); ok && e == nil {
return s.NotifyError(err)
}
return nil
}
Да, такой прием работает.
Но будем честны, мы не можем позволить себе проверить все типы ошибок которые мы будем передавать.
Это могут быть все ошибки из драйвера базы данных, все наши внутренние ошибки и прочий мусор.
Какой наиболее рациональный вариант я вижу:
func (s *NotificationService) NotifyIfError(w *ResponseWriter) error {
var err error
...
var res handlers.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.
Отмечу, что приведенные мной выше пляски не про это.
Мои заметки позволяют не упасть с паникой когда вы знаете, что хотите вернуть интерефйс, а в случае ошибок — вы всегда хотите возвращать интерфейс.
Но если вы можете себе позволить вернуть определенный тип — возвращайте его.
Ссылки
Я
https://www.linkedin.com/in/%D0%B4%D0%B5%D0%BD%D0%B8%D1%81-%D0%B4%D0%B2%D0%BE%D1%80%D0%BD%D0%B8%D0%BA%D0%BE%D0%B2-33399812b/
https://t.me/it_patifon
https://twitter.com/denny_penta/followers
https://github.com/dennypenta
Автор: candybooberr