Обложка: Приключение с interface в мире Golang

Приключение с interface в мире Golang

2
6
Евгений Митин
Евгений Митин

Руководитель отдела разработки платформенных сервисов

Вы когда-нибудь задумывались что такое 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.

The Go Programming Language Specification

На мой взгляд, хорошее абстрактное описание, которое не очень сильно помогает осознать интерфейсы на практике, но в целом даёт общее понимание. Очень советую пойти в документации в раздел 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 — ссылка на реальную область памяти, в которой лежат данные изначального объекта, в нашем случае Василия. Поэтому библиотека рефлект, когда хочет получить настоящий исходный тип объекта интерфейса, идёт в это поле 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
	// а вот и нет, это вам Golang, а не математика
}

Давайте разбираться, почему так происходит. Как я уже говорил, объект интерфейса в Golang содержит два поля: tab с информацией о конкретном типе и data, где лежит ссылка на сами данные. И вот, по правилам Golang, интерфейс может быть равен 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 по языку: Why is my nil error value not equal to nil?. И там как раз рассмотрена одна из распространённых ошибок, связанная с этим поведением интерфейсов: когда мы из функции возвращаем ссылку на объект нашей кастомной ошибки.

Данная ссылка, в свою очередь, может быть nil, если ошибка в функции не произошла. И, когда происходит преобразование нашего объекта кастомной ошибки к интерфейсу error, этот новый преобразованный объект уже никогда больше не будет равен nil, и все проверки if err != nil {} перестанут работать корректно.

Что думаете?