Встраивание структур в Go: короткий гайд с примерами
Встраивание (embedding) — механизм Go, который заменяет классическое наследование. Разбираем синтаксис, продвижение методов, конфликты имён и типичные ловушки на рабочих примерах для Go 1.22+.

Что нужно до начала
Установленный Go версии 1.22 или новее (проверьте командой go version). Любой редактор с поддержкой Go — VS Code с расширением Go, GoLand или даже терминал. Все примеры ниже запускаются через go run main.go в отдельной директории с инициализированным модулем.
Суть встраивания за 30 секунд
В Go нет наследования. Вместо него — встраивание: вы помещаете одну структуру внутрь другой без имени поля, и все экспортируемые поля и методы внутренней структуры становятся доступны через внешнюю. Компилятор «продвигает» (promotes) их наверх, как будто они объявлены прямо во внешней структуре.
Это не наследование: внутренний тип ничего не знает о внешнем, полиморфизма через встраивание вы не получите. Зато получите компактный повторно используемый код без лишних обёрток.
Шаг 1 — минимальный пример
Создайте директорию, инициализируйте модуль и откройте main.go:
mkdir embedding-demo && cd embedding-demo
go mod init embedding-demo
Вставьте в main.go следующий код:
package main
import "fmt"
// Address хранит почтовый адрес.
type Address struct {
City string
Street string
}
// FullAddress возвращает строку вида "город, улица".
func (a Address) FullAddress() string {
return a.City + ", " + a.Street
}
// Employee содержит встроенную структуру Address.
type Employee struct {
Name string
Address // встраивание — без имени поля
}
func main() {
e := Employee{
Name: "Иван",
Address: Address{
City: "Москва",
Street: "Тверская 1",
},
}
// Поля Address доступны напрямую через Employee
fmt.Println(e.City) // Москва
fmt.Println(e.FullAddress()) // Москва, Тверская 1
}
Запустите:
go run main.go
Проверка: в консоли появятся две строки — Москва и Москва, Тверская 1. Если видите ошибку undefined: Address — убедитесь, что тип Address объявлен в том же пакете и файле.
Шаг 2 — продвижение методов и полей
Когда вы обращаетесь к e.City, компилятор разворачивает это в e.Address.City. То же с методами: e.FullAddress() — это e.Address.FullAddress(). Продвигаются только экспортируемые (с заглавной буквы) поля и методы.
Явный доступ через e.Address.City тоже работает — он не исчезает. Это важно, когда вам нужно передать именно встроенную структуру целиком, например, в функцию, принимающую Address.
Шаг 3 — встраивание указателя
Встроить можно не только значение, но и указатель:
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
type Service struct {
*Logger // встраиваем указатель
}
func main() {
sharedLogger := &Logger{Prefix: "SVC"}
s := Service{Logger: sharedLogger}
s.Log("запуск") // [SVC] запуск
}
Проверка: в консоли — [SVC] запуск.
Указательное встраивание позволяет нескольким структурам разделять один экземпляр. Но есть ловушка: если забыть инициализировать указатель, при вызове продвинутого метода получите nil pointer dereference — паника в рантайме без внятного сообщения. Компилятор тут не поможет.
Шаг 4 — конфликт имён
Когда две встроенные структуры экспортируют поле или метод с одинаковым именем, продвижение перестаёт работать. Компилятор не выберет за вас.
package main
import "fmt"
type Camera struct {
Resolution string
}
type Screen struct {
Resolution string
}
type Phone struct {
Camera
Screen
}
func main() {
p := Phone{
Camera: Camera{Resolution: "48 MP"},
Screen: Screen{Resolution: "1080x2400"},
}
// fmt.Println(p.Resolution) // ← ошибка компиляции: ambiguous selector
fmt.Println(p.Camera.Resolution) // 48 MP
fmt.Println(p.Screen.Resolution) // 1080x2400
}
Проверка: раскомментируйте строку с p.Resolution — компилятор выдаст ambiguous selector p.Resolution. Уберите комментарий обратно — программа выведет два значения разрешения.
Конфликт разрешается либо явным обращением, либо объявлением поля Resolution прямо в Phone — тогда оно "затенит" оба продвинутых.
Шаг 5 — встраивание и интерфейсы
Продвинутые методы учитываются при проверке интерфейсов. Если встроенная структура реализует интерфейс, внешняя структура тоже его реализует — без единой строки дополнительного кода.
package main
import "fmt"
type Stringer interface {
String() string
}
type Name struct {
First string
Last string
}
func (n Name) String() string {
return n.First + " " + n.Last
}
type User struct {
Name
Email string
}
func greet(s Stringer) {
fmt.Println("Привет,", s.String())
}
func main() {
u := User{
Name: Name{First: "Ольга", Last: "Иванова"},
Email: "olga@example.com",
}
greet(u) // Привет, Ольга Иванова
}
Проверка: greet(u) компилируется, потому что User получил метод String() от Name.
Встраивание — не наследование. Внешний тип не становится подтипом внутреннего. Вы не можете передатьUserтуда, где ждутName. Зато можете — туда, где ждутStringer, если метод продвинут.
Типичные ошибки и как их распознать
Ниже — пять ситуаций, с которыми сталкиваются чаще всего.
| Ошибка / симптом | Причина | Решение |
|---|---|---|
ambiguous selector при компиляции | Два встроенных типа продвигают поле/метод с одним именем | Обращайтесь явно или объявите поле во внешнем типе |
nil pointer dereference в рантайме | Встроен указатель, но не инициализирован | Инициализируйте при создании или добавьте конструктор |
| Неожиданная реализация интерфейса | Встроенный тип реализует интерфейс, и внешний тип "случайно" тоже | Проверяйте набор методов: var _ MyInterface = (*MyStruct)(nil) |
| JSON-поля "сплющиваются" | encoding/json продвигает поля встроенных структур как свои | Добавьте тег json:"address" к именованному полю вместо встраивания |
| Метод внешней структуры не вызывается | Вы определили метод на внешнем типе с тем же именем, но с другой сигнатурой — это затенение, а не перегрузка | В Go нет перегрузки; переименуйте метод или вызывайте внутренний явно |
Когда встраивать, а когда — именованное поле
Нет универсального правила, но есть рабочий критерий:
- Встраивайте, если внешний тип является расширением внутреннего и вам нужен его интерфейс. Пример:
http.Serverвстраивает мьютекс, потому что серверу нуженLock()/Unlock()как часть собственного поведения. - Именованное поле — если внутренний тип лишь используется.
Userимеет адрес, но не является адресом. Встраивание здесь засоряет API типа полями вродеuser.PostalCode, которые сбивают с толку.
Ещё один сигнал: если вы встраиваете структуру и сразу затеняете половину её методов — скорее всего, вам нужно именованное поле и делегирование вручную.
Сноска: Продвижение (promotion) — механизм, при котором компилятор Go автоматически делает поля и методы встроенного типа доступными через внешний тип, как если бы они были объявлены в нём напрямую. Термин используется в спецификации языка.
Что дальше
Встраивание — только один из способов композиции в Go. Следующий шаг — разобраться с встраиванием интерфейсов (когда в структуру встраивается не конкретный тип, а интерфейс), это отдельная тема со своими ловушками. Полезно также изучить, как encoding/json и encoding/xml обрабатывают продвинутые поля — поведение маршалинга при встраивании бывает неочевидным. Официальная спецификация описывает правила продвижения исчерпывающе, ссылка — в источниках ниже.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: The Go Programming Language Specification — Struct types (Promoted fields and methods), Effective Go — Embedding
Частые вопросы
Чем встраивание структур отличается от наследования в ООП-языках?
Можно ли встроить два типа с одинаковым методом?
Что будет, если встроить указатель и забыть его инициализировать?
Встраивание влияет на JSON-сериализацию?
Работают ли примеры из статьи на Go 1.21 или старше?
Что делать, если после встраивания тип случайно реализовал чужой интерфейс?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.