Первый очерк из цикла приключений в мире сусликов.
С этой статьи начинается серия небольших рассказов о необычных подводных камнях, которые можно встретить в начале разработки на Go. В статьях будут примеры кода, будьте с ними аккуратнее - не все из них будут компилироваться и работать, читайте внимательно комментарии, везде будет указано, на какой строке происходит ошибка. Также в блоках кода везде табуляция заменена на пробелы - это сделано намеренно, чтобы статьи выглядели у всех одинаково.
Начинать рассказывать я буду издалека, с самых основ, но это необходимо для полного понимания. В конце же рассказа, будет самое интересное, но всё равно стоит читать его с самого начала.
Пара слов обо мне. Я всю жизнь занимаюсь разработкой программного обеспечения, в основном в сфере WEB, успел познакомиться с многими языками программирования и поработать в разных крупных компаниях. Сейчас руковожу разработкой в компании NUT.Tech, мы там делаем классные и интересные вещи. В данный момент в основном разработка в отделе построена вокруг Go, поэтому о нём я вам и расскажу.
Статьи серии:
Интерфейсы в Go - как красиво выстрелить себе в ногу
Нарезаем массивы правильно в Go
...
Пожалуй, начнём. Вы когда-нибудь задумывались, что такое interface
? Ну, то есть, не ключевое слово синтаксиса, а что это такое в рантайме? Как выглядит его проинстанцированный объект? А, главное, каким свойством обладает при сравнении с nil
? Нет? Тогда устраивайтесь поудобнее, я сейчас вам всё расскажу.
Начнём с определения интерфейса из спецификации языка:
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is
nil
.
Мой вольный перевод:
Тип интерфейса определяет множество методов, называющееся интерфейс. Переменная типа интерфейса может хранить значение любого типа с набором методов, который является любым надмножеством интерфейса. Говорят, что такой тип реализует интерфейс. Значение неинициализированной переменной типа интерфейса равно
nil
.
На мой взгляд, хорошее абстрактное описание, которое не очень сильно помогает осознать интерфейсы на практике, но в целом даёт общее понимание. Очень советую пойти в документации в раздел Interface_types - там много примеров и более подробное описание.
Но этого поста бы не было, будь там вся необходимая информация ;) Допустим, мы уже понимаем, что переменная с интерфейсом в качестве типа это некий объект, который с точки зрения типизации обязан обладать всеми перечисленными в указанном интерфейсе методами.
Необходимо также понимать, что переменные с интерфейсом в качестве типа бессмысленны сами по себе, они не создаются и не используются в отрыве от обычных типов. И именно переменные обычных типов, превращаются в переменные интерфейсного типа. А с точки зрения компилятора в переменную интерфейсного типа может превратится любая переменная обычного типа, у которой её тип содержит все заявленные в интерфейсе методы. Хотя могут быть и дополнительные методы - реализация интерфейса от этого не сломается. Понимаю, всё несколько сложно и запутано, но дальше будут примеры и станет понятнее.
А что происходит, например, когда мы переменную определенного, конкретного типа, превращаем в другую переменную с интерфейсным типом? В Golang, как впрочем и многих других статически типизированных языках, у переменной можно легально получить доступ только к тем атрибутам и методам, которые существуют у типа этой переменной. В случае преобразования переменная получает определенный интерфейсный тип. Одна из очевидных проблем, которую мы встречаем, - потеря доступа ко всем атрибутам нашего конкретного типа и к его методам, которые не были указаны в интерфейсе.
Посмотрим на конкретном примере:
package main
import (
"fmt"
"reflect"
)
// создадим некоторый тип Человек,
// который умеет спать, есть и работать
type Man struct{}
func (m Man) Sleep() {}
func (m Man) Eat() {}
func (m Man) Work() {}
// и тип Пёс, который у нас умеет только лаять
type Dog struct{}
func (d Dog) Bark() {}
// теперь мы хотим понять, кто может стать программистом,
// для этого мы определяем интерфейс Programmer,
// и определяем в нём метод Work
// этим мы ограничиваем количество объектов,
// которые смогут быть программистами
// так случилось, что в нашем коде, чтобы стать программистом,
// достаточно уметь работать :)
type Programmer interface {
Work()
}
func main() {
// итак, теперь посмотрим, как работать с нашими типами и интерфейсом
// для начала создадим некоторого конкретного человека
Vasiliy := Man{}
// и посмотрим на его тип
fmt.Printf("%s\n", reflect.TypeOf(Vasiliy).String()) // main.Man
// также создадим Василию верного друга
Sharik := Dog{}
fmt.Printf("%s\n", reflect.TypeOf(Sharik).String()) // main.Dog
// что ж, теперь нужно проверить, кто же,
// Шарик или Василий нам лучше подойдёт на роль программиста
// для этого создадим абстрактного работника с типом Programmer
// и попробуем к нему присвоить наши объекты
var worker Programmer
worker = Sharik
// увы, этот код не будет скомпилирован,
// компилятор выведет следующую ошибку
// cannot use Sharik (type Dog) as type Programmer in assignment:
// Dog does not implement Programmer (missing Work method)
// теперь мы знаем, Шарик пока не может быть программистом,
// придётся им стать Василию
worker = Vasiliy // этот код компилируется и работает
// в целом, этого достаточно для базового понимания интерфейсов,
// но дальше будет ещё кое-что интересное
// проверим тип нашего работника теперь
fmt.Printf("%s\n", reflect.TypeOf(worker).String()) // main.Man
// в целом выглядит логично: мы сказали,
// что работником будет Василий, у которого тип Man,
// но раз уж так, давайте попробуем вызвать
// метод Sleep у нашего работника
worker.Sleep()
// worker.Sleep undefined (type Programmer has no field or method Sleep)
// увы, этот код не будет работать,
// так как на самом деле worker != Vasiliy
// типом переменной worker является интерфейс Programmer,
// который не содержит метод Sleep()
}
Что же происходит, и почему пакет reflect определяет тип нашего воркера как Man
, которым на самом деле он не является? Чтобы это понять, нужно погрузиться в недра исходного кода нашего "суслика" и посмотреть, чем представлен там тип interface
:
// https://golang.org/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
Если не вдаваться в подробности, то в поле tab
у нас хранится информация о конкретном типе объекта, который был преобразован в интерфейс. А в поле data
- ссылка на реальную область памяти, в которой лежат данные изначального объекта, в нашем случае Василия. Поэтому библиотека reflect, когда хочет получить настоящий исходный тип объекта интерфейса, идёт в это поле tab
и получает информацию о типе там.
У нас остаётся возможность преобразовать наш объект интерфейса обратно к исходному типу, для этого у нас есть синтаксис утверждений типа, выглядит он следующим образом:
m, ok := worker.(Man)
fmt.Printf("%#v\n", m) // main.Man{}
fmt.Printf("%t\n", ok) // true
Можно использовать и без переменной ok
, но в таком случае Вы получите панику, если в интерфейсе окажется несоответствующий тип.
Окей, с этим вроде разобрались, идём дальше.
Как нам говорит документация: The value of an uninitialized variable of interface type is nil
. То есть наша переменная интерфейсного типа может принимать nil
. Ну это же супер! Если отмотать немного назад, то можно увидеть, как мы изначально инициализировали нашего работника - var worker Programmer
. Если верить документации, то в этот момент наш worker
был равен nil
. Доверяй, но проверяй:
func main() {
var worker Programmer
fmt.Printf("%#v\n", reflect.TypeOf(worker)) // <nil>
// и так, тут уже тип нашего воркера не main.Man,
// как это было при присвоении туда Василия, а nil
// ну хорошо, давайте ещё сделаем проверку,
// действительно ли наш пустой работник равен nil
fmt.Printf("%t\n", worker == nil) // true
// отлично, и правда nil
}
Но мы пойдём дальше: а что если попробовать создать пустую переменную типа *Man
и преобразовать её к интерфейсу? Пробуем:
func main() {
var man *Man
fmt.Printf("%#v\n", man) // (*main.Man)(nil)
var worker Programmer
fmt.Printf("%#v\n", worker) // <nil>
worker = man
fmt.Printf("%#v\n", worker) // (*main.Man)(nil)
// наш воркер изменил тип, он больше не совсем nil,
// а nil от типа *main.Man
// что же это для нас значит, давайте смотреть
fmt.Printf("%t\n", man == nil) // true
// отлично, наш man равен nil,
// мы же туда ничего не положили и ссылка равна nil
fmt.Printf("%t\n", worker == man) // true
// ну тоже хорошо, мы положили в переменную
// worker переменную man,
// и как мы уже убеждались ранее, они равны
// ну и понятно, по законам математики,
// если x = 0, а y = x, значит y = 0
fmt.Printf("%t\n", worker == nil) // false
// а вот и нет, это вам Go, а не математика
}
Давайте разбираться, почему так происходит. Как я уже говорил, объект интерфейса в Go содержит два поля: tab
с информацией о конкретном типе и data
, где лежит ссылка на сами данные. И вот, по правилам Go, интерфейс может быть равен nil
только если оба этих поля не определены.
Давайте смотреть, что у нас получается, когда мы преобразуем переменную man
в интерфейс:
func main() {
var worker Programmer
fmt.Printf("%s\n", reflect.ValueOf(worker)) // <invalid reflect.Value>
// рефлект не может получить значение из переменной worker,
// потому что его просто нет,
// то есть поле `data` у нашего интерфейса не задано
fmt.Printf("%v\n", reflect.TypeOf(worker)) // <nil>
// как не может получить и тип,
// так как он тоже не задан в поле `tab`
fmt.Printf("%t\n", worker == nil) // true
// worker равен nil
var man *Man
fmt.Printf("%t\n", man == nil) // true
// переменная man равняется nil
worker = man
fmt.Printf("%s\n", reflect.ValueOf(worker).Elem())
// <invalid reflect.Value>
// после присвоения переменной worker переменной man
// мы точно также не можем получить значение через reflect
// так как man у нас не содержит каких-либо данных, он равен `nil`
fmt.Printf("%v\n", reflect.TypeOf(worker)) // *main.Man
// а вот с типом интереснее мы из переменной worker
// получили тип переменной man, то есть *main.Man
// соответственно поле `tab` у нашего интерфейса уже явно не пустое
// и поэтому переменная worker больше не будет равна nil
fmt.Printf("%t\n", worker == nil) // false
}
Как итог, мы имеем интерфейс с заполненным полем tab
, и проверка на равенство с nil
всегда будет возвращать false
. Несмотря на то, что изначально переменная возвращала true
при сравнении с nil
.
Описано ли это поведение в документации к нашему суслику? - Нет. Ну точнее не совсем. В спецификации языка это не описано, но это есть в FAQ по языку: https://golang.org/doc/faq#nil_error. И там как раз рассмотрена одна из распространённых ошибок, связанная с этим поведением интерфейсов: когда мы из функции возвращаем ссылку на объект нашей кастомной ошибки. Данная ссылка, в свою очередь, может быть nil
, если ошибка в функции не произошла. И, когда происходит преобразование нашего объекта кастомной ошибки к интерфейсу error
, этот новый преобразованный объект уже никогда больше не будет равен nil
, и все проверки if err != nil {}
перестанут работать корректно.
Приведу пример чуть более подробный чем в faq:
package main
import "fmt"
// создаём свой собственный тип ошибки,
// обычно это бывает нужно чтобы передавать дополнительные данные
// вместе с ошибкой, так например сделано в библиотеке twirp
type CustomError struct{}
// этот метод нам нужен, чтобы реализовать интерфейс error
func (CustomError) Error() string { return "CustomError" }
// а в этой функции мы будем возвращать нашу ошибку, либо nil
func raiseError(raise bool) *CustomError {
if raise == true {
return &CustomError{}
}
return nil
}
func main() {
// для начала нам нужно инициализировать переменную,
// которая будет принимать результат из функции raiseError
var err error
// пробуем вернуть из нашей функции ошибку
err = raiseError(true)
fmt.Println(err != nil) // true
// отлично, err у нас действительно не равен nil
// а теперь вызовем функцию таким образом, чтобы она вернула nil
err = raiseError(false)
fmt.Println(err != nil) // true
// после преобразования в интерфейс, наш nil уже не очень то и nil
}
На этом мой очерк заканчивается, читайте о других интересных моментах из мира Go в следующих сериях и будьте аккуратны с интерфейсами.
vesper-bot
А случайно переменные в Go, т.е. тот же man, не состоят ли из ссылки на класс плюс ссылки на данные инстанса? Этого могло бы хватить для описания всех проблем с интерфейсами. Потому что тогда если переменная типа класс, на нил проверяется только data, а если interface, то и класс и данные. (Что по мне нелогично, что толку, что переменной интерфейса присвоен типизированный nil?)
eandr_67
В Go не существует классов. Мы можем создать методы не только для структур, но и для любого типа, производного от примитивного: хоть от bool, хоть от int. Соответственно, любой созданный нами тип может реализовывать любой интерфейс. А абсолютно все типы (включая изначально встроенные в язык) реализуют интерфейс interface{}.
Особенностью принятого в Go подхода является то, что мы можем совершенно корректно вызвать метод для типизированного nullable-указателя. Так что поведение интерфейса, равного nil, и интерфейса, содержащего указатель на nil, отличается не только при сравнении с nil, но и при вызове методов этого интерфейса.
Ссылку на тип содержат только переменные-интерфейсы. Все прочие переменные имеют тип, точно известный в момент компиляции, и компилятор просто подставляет информацию о типе в нужные места кода.
WinPooh32
Го типизированный язык, данными структур и других атомарных типов компилятор может дирижировать как ему угодно, что позволяет не хранить в рантайме для них информацию о типе. Эта информация вкладывается в контейнер интерфейса при присваивании значения только во время компиляции.
Для слайсов, мап, каналов, интерфейсов оператор '==' работает над контейнером, а не над данными внутри.
Нетипизированный nil присваивается в контейнер, а типизированный в данные контейнера.
Чтобы узнать что в данных значение nil нужно преобразовать интерфейс сначала в этот тип, затем проверить на nil, либо через рефлексию получить сырой указатель и уже работать с ним.
Пример: https://go.dev/play/p/LHO6WsI9hY_R