Embedding структур и интерфейсов

О лекции

Безымянное поле продвигает методы внутреннего типа наружу. Это сборка из частей, не наследование классов: подтипа «является» и виртуального override в Go нет.

Типы и память Go golang собеседование

Go обошёлся без иерархии классов. Повтор поведения собирают композицией: поле с типом. Часто поле безымянное — это embedding. Методы встроенного типа становятся видны снаружи. s.Log() работает, хотя Log объявлен на Logger. Имя поля совпадает с именем типа.

На бэкенде так подмешивают логгер, метрики, mutex. Или встраивают маленький reader в декоратор. Это удобно, пока не ждут, что *Dog «является» *Animal.

Аналогия: в машину встроили магнитолу. У машины появилась кнопка Play. Машина при этом не стала магнитолой. Туда, где ждут магнитолу, машину не подставишь.

Что даёт embedding

Продвинутый метод вызывается с внешнего значения. Тело работает со встроенным полем. Для указателей важно, как встроили: Logger или *Logger. Нулевой внешний объект с nil-указателем внутри снова упирается в nil receiver.

type Logger struct{ prefix string }

func (l Logger) Log(s string) {
	fmt.Println(l.prefix, s)
}

type Server struct {
	Logger // embedding, не «extends»
	Addr   string
}

func main() {
	s := Server{Logger: Logger{prefix: "http"}, Addr: ":8080"}
	s.Log("up") // то же, что s.Logger.Log("up")
	// var l Logger = s // не компилируется: нет подтипа
}

Продвижение Log через безымянное поле. *Server не является подтипом Logger.

Пример. Есть type Handler struct { Logger }. Можно вызвать h.Log("ok"). Нельзя передать h в функцию, которая ждёт Logger.

Чего это не даёт

Нет подтипов. Нельзя передать *Server туда, где ждут *Logger, только потому что Logger встроен. Нет виртуальной таблицы внешнего типа. Метод, объявленный на Server с тем же именем, затеняет продвинутый. Вызов через интерфейс, который хранит внутренний тип, пойдёт во внутренний метод. Это не override в иерархии.

Конструкторов как в Java нет. Инициализация — литерал, NewServer() или functional options. Встраивание mutex в экспортируемую структуру опасно: снаружи возьмут копию Server и скопируют mutex. Mutex встраивают осторожно и не экспортируют без нужды.

Интерфейсы и конфликт

  • Встроить интерфейс в интерфейс — объединить контракты. Так устроен io.ReadCloser: Reader плюс Closer.
  • Встроить интерфейс в структуру — поле, которое надо заполнить реализацией. Методы продвинутся, пока поле не nil.
  • Два встроенных типа с одним именем метода — конфликт, пока внешний тип не определит метод сам.

Пример. Встроили Logger и Metrics, у обоих метод Name(). Компилятор не выберет сам. Нужен свой func (s Server) Name() или вызов через s.Logger.Name().

Типичная ошибка

Назвать embedding наследованием и ждать, что *Service «является» *Logger. Полиморфизм по поведению в Go делают интерфейсами, а не цепочкой встраивания. Вторая ошибка — продвинуть слишком много: наружу торчит API внутреннего типа, который вы не хотели обещать.

На собеседовании

Продвигаются методы. Нет наследования и нет подтипа. Полиморфизм — интерфейс. Имеет смысл сразу сказать про конфликт имён и что структуру со встроенным mutex нельзя копировать.