ПрограммированиеBackend разработчик

Как работает вложенность структур (embedding structs) в Go? Чем отличается embedding от обычного включения в структуру? Какие тонкости нужно учитывать?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В Go embedding (встраивание) — это механизм, позволяющий "наследовать" поведение структуры путем встраивания одной структуры в другую без использования явного наследования, как, например, в OOP. Встраиваемая структура становится полями-встраиваемой (анонимными полями) новой структуры.

При этом все методы встраиваемой структуры становятся методами новой структуры, будто бы "наследуются".

Пример:

package main import "fmt" type Animal struct { Name string } func (a Animal) Speak() { fmt.Println("My name is", a.Name) } type Dog struct { Animal // embedding, анонимное поле Breed string } func main() { d := Dog{ Animal: Animal{Name: "Rex"}, Breed: "Shepherd", } d.Speak() // Наследованный метод! fmt.Println(d.Name) // Поле доступно напрямую }

Главная разница между embedding и обычным включением структуры как именованного поля:

  • При embedding методы и поля становятся доступны напрямую у новой структуры,
  • При обычном включении требуется обращаться к ним через имя поля.

Вопрос с подвохом

"Если встраиваемая структура и внешняя содержат поля с одинаковыми именами, какое будет использоваться и как к ним обратиться?"

Многие считают, что будет использовано только "верхнее" поле, но это работает иначе:

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

Пример:

type Base struct { Name string } type Child struct { Base // embedding Name string // совпадающее поле } c := Child{Base: Base{Name: "Base"}, Name: "Child"} fmt.Println(c.Name) // Child fmt.Println(c.Base.Name) // Base

Примеры реальных ошибок из-за незнания тонкостей темы


История

В проекте встраивали стандартную структуру для логгирования, а затем определили поле Logger в дочернем типе. В результате, вызовы d.Logger.Info() работали не как ожидалось, потому что доступ к встроенному полю "терялся" среди одноименных.


История

Разработчик пытался переопределить метод в дочерней структуре, но методов не "override-ятся" — просто появляется новый, и родительский по-прежнему доступен как Child.Base.Method(). Это приводило к тому, что часть кода использовала "старую" версию метода, ожидая другую логику.


История

При сериализации JSON с вложенными структурами возникали неожиданности: поля "встраиваемой" структуры появлялись в корневом объекте. Из-за незнания, как embedding влияет на marshaling, структура сериализовалась некорректно, ломая обратную совместимость API.