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

Что понадобится до начала
Инструкция написана для Go 1.21+. Всё, что нужно:
- установленный Go (проверьте командой
go version— должно быть 1.21 или выше); - любой текстовый редактор или IDE с поддержкой Go (VS Code с расширением Go, GoLand, даже Vim с gopls);
- инициализированный модуль — создайте папку проекта и выполните
go mod init embedding-demo.
Если go version возвращает ошибку, Go не установлен или не добавлен в PATH. На странице go.dev/dl — свежие бинарники для всех платформ.
Шаг 1. Обычная структура без встраивания
Сначала создадим две структуры, которые пока никак не связаны. Файл main.go:
package main
import "fmt"
// Address хранит почтовый адрес.
type Address struct {
City string
Street string
}
// String возвращает адрес одной строкой.
func (a Address) String() string {
return a.City + ", " + a.Street
}
// Person хранит имя и адрес через обычное именованное поле.
type Person struct {
Name string
Address Address
}
func main() {
p := Person{
Name: "Алиса",
Address: Address{City: "Москва", Street: "Тверская 1"},
}
// Обращаемся к полю через имя поля Address
fmt.Println(p.Address.City) // Москва
fmt.Println(p.Address.String()) // Москва, Тверская 1
}
Запустите: go run main.go. Должно появиться:
Москва
Москва, Тверская 1
Всё работает, но обращение через p.Address.City требует лишнего уровня. Для одного поля терпимо, для пяти вложенных — читаемость падает.
Шаг 2. Встраиваем структуру
Уберите имя поля — оставьте только тип. Go превратит это во встраивание:
// Person со встроенной структурой Address.
type Person struct {
Name string
Address // без имени поля — это embedding
}
Теперь измените main:
func main() {
p := Person{
Name: "Алиса",
Address: Address{City: "Москва", Street: "Тверская 1"},
}
// Поля и методы Address доступны напрямую
fmt.Println(p.City) // Москва
fmt.Println(p.String()) // Москва, Тверская 1
// Полный путь тоже работает
fmt.Println(p.Address.Street) // Тверская 1
}
Запустите go run main.go. Вывод:
Москва
Москва, Тверская 1
Тверская 1
Произошло ровно одно изменение — убрали имя поля — и получили «продвижение» (promotion) полей и методов на уровень внешней структуры. Это не наследование: тип Person не становится подтипом Address. Компилятор просто генерирует сокращённый доступ.
Шаг 3. Конфликт имён и как его разрешить
Допустим, у Person есть собственный метод String:
func (p Person) String() string {
return p.Name + " — " + p.Address.String()
}
func main() {
p := Person{
Name: "Алиса",
Address: Address{City: "Москва", Street: "Тверская 1"},
}
fmt.Println(p.String()) // Алиса — Москва, Тверская 1
}
Метод внешней структуры побеждает. Встроенный Address.String никуда не исчез — он по-прежнему доступен через p.Address.String(). Правило простое: чем ближе к «поверхности», тем выше приоритет. Если два встроенных типа на одном уровне имеют одноимённое поле или метод, компилятор выдаст ошибку — неоднозначность придётся разрешить явным путём.
Проверка: добавьте второй встроенный тип с методом String и попробуйте скомпилировать. Вы увидите ошибку вида ambiguous selector p.String.
Шаг 4. Встраивание указателя на структуру
Встраивать можно не только значение, но и указатель:
type Employee struct {
Name string
*Address // встроенный указатель
}
func main() {
addr := &Address{City: "Казань", Street: "Баумана 5"}
e := Employee{Name: "Борис", Address: addr}
fmt.Println(e.City) // Казань
// Осторожно: если Address == nil, обращение к полю — паника
e2 := Employee{Name: "Вера"}
// fmt.Println(e2.City) // runtime panic: nil pointer dereference
_ = e2
}
Запустите и убедитесь, что Казань напечаталась. Раскомментируйте строку с e2.City — вы увидите панику. Это главный подводный камень встраивания указателей: нулевой указатель ловится только в рантайме.
Встраивание указателя оправдано, когда несколько структур должны разделять один и тот же экземпляр данных, а не копировать его. Если такой необходимости нет — встраивайте значение: безопаснее и предсказуемее.
Шаг 5. Встраивание и интерфейсы
Продвигаются не только поля, но и методы — а значит, встроенный тип может «подарить» внешнему реализацию интерфейса. Добавим интерфейс fmt.Stringer (у него ровно один метод — String() string):
package main
import "fmt"
type Address struct {
City string
Street string
}
func (a Address) String() string {
return a.City + ", " + a.Street
}
type Company struct {
Title string
Address // Address реализует fmt.Stringer — Company тоже
}
func printStringer(s fmt.Stringer) {
fmt.Println(s)
}
func main() {
c := Company{
Title: "ООО Ромашка",
Address: Address{City: "Новосибирск", Street: "Ленина 10"},
}
printStringer(c) // Новосибирск, Ленина 10
}
Запустите — должно напечататься Новосибирск, Ленина 10. Компилятор не жалуется, потому что Company теперь удовлетворяет fmt.Stringer благодаря продвинутому методу String от Address. Явного implements в Go нет — интерфейс реализуется автоматически.
Шаг 6. Встраивание интерфейса в структуру
Менее очевидный приём — встроить в структуру сам интерфейс*:
type Logger interface {
Log(msg string)
}
type Service struct {
Logger // встроенный интерфейс
}
type ConsoleLogger struct{}
func (ConsoleLogger) Log(msg string) {
fmt.Println("LOG:", msg)
}
func main() {
svc := Service{Logger: ConsoleLogger{}}
svc.Log("запуск") // LOG: запуск
}
Это удобно для тестов: можно подсунуть мок без генерации кода. Но если поле Logger не инициализировано, вызов svc.Log — паника, точно как с указателем.
* Встраивание интерфейса — это не наследование интерфейсов (когда один интерфейс включает другой). Здесь интерфейс становится полем структуры, значение которого можно задать любым типом, реализующим этот интерфейс.
Сводная таблица: что встраиваем и что получаем
| Что встраивается | Доступ к полям | Доступ к методам | Риск nil-паники | Когда использовать |
|---|---|---|---|---|
| Структура (значение) | да, продвигаются | да, продвигаются | нет | по умолчанию — самый безопасный вариант |
| Указатель на структуру | да, продвигаются | да, продвигаются | да, если указатель nil | несколько владельцев одних данных |
| Интерфейс | нет полей | да, продвигаются | да, если не инициализирован | подмена реализации, моки в тестах |
Если не получилось
cannot use p (variable of type Person) as type fmt.Stringer— уPersonесть собственныйStringс неправильной сигнатурой, либо встроенный тип не реализует интерфейс. Проверьте сигнатуру:String() stringбез аргументов.ambiguous selector p.City— два встроенных типа на одном уровне содержат полеCity. Обращайтесь через полное имя:p.Address.Cityилиp.Office.City.runtime error: invalid memory address or nil pointer dereference— встроен указатель или интерфейс, но значение не задано. Инициализируйте поле при создании структуры.unknown field Address in struct literal— при инициализации именованных полей Go ожидает имя типа как имя поля. ПишитеAddress: Address{...}, а неAddr: Address{...}.- Метод встроенного типа не виден — метод определён на указательном ресивере (
*Address), а встроено значение. Либо встраивайте*Address, либо определяйте метод на значении.
Что делать дальше
Встраивание — один из кирпичиков композиции в Go. Следующий шаг — попрактиковаться с интерфейсами: определите два-три маленьких интерфейса, встройте их в структуру и напишите тесты с моками. Официальная спецификация описывает правила продвижения полей и методов в разделе Struct types — там же указаны правила разрешения конфликтов для вложенности более одного уровня. Из практических задач: попробуйте встроить sync.Mutex в структуру, чтобы вызывать s.Lock() напрямую — это идиоматический паттерн, который встречается в стандартной библиотеке Go.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: The Go Programming Language Specification — Struct types, Effective Go — Embedding
Частые вопросы
Встраивание — это наследование?
Можно ли встроить в одну структуру несколько типов?
Если у меня другая версия Go (например, 1.18), примеры сработают?
Продвигаются ли неэкспортированные поля и методы?
Когда лучше использовать именованное поле, а не встраивание?
Что будет, если забыть инициализировать встроенный указатель?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.