---
id: ebf46089667a6469b258b8e52e57571f
author: Yunjin Lee
title: 'Интерфейсы Go и встраивание структур: различия с наследованием классов и проектирование на основе композиции'
description: Сравнивается различие между наследованием классов в Java/C++ и структурной типизацией интерфейсов в Go, а также с помощью примеров кода поясняется паттерн композиции (Composition) с использованием встраивания структур и делегирования методов.
language: ru
date: 2025-07-17T10:40:02.284435348Z
path: /blog/posts/go-interfaces-are-not-inheritance-zd04c1084
category: language
---

# Обзор

Интерфейсы в Go позволяют легко реализовать функции с одинаковыми аргументами и возвращаемыми значениями во многих структурах, однако это отличается от способа, которым ключевое слово extends в Java расширяет и переопределяет поведение внутренних функций. Чтобы не путать Go с наследованием, необходимо правильно понимать компositionную переиспользуемость кода в Go, но с самого начала полностью теоретически понять её непросто. Разберём это на примерах сценариев, где легко допустить ошибку.

## Частые ошибки

Новички могут допускать следующие ошибки.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type Apple struct {
	Label string
	Name  string
	Brix  float64
}

type Watermelon struct {
	Label string
	Name  string
	Brix  float64
}

func (a *Apple) PrintAll() {
	fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
}

const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
}

func main() {
	fmt.Println("Inheritance test #1")
	apple := new(Apple)
	watermelon := apple
	apple.SetLabel("Apple_1")
	fmt.Println("Apple, before copied to Watermelon")
	apple.PrintAll()
	watermelon.SetLabel("WaterMelon_2")
	fmt.Println("Apple, after copied to Watermelon")
	apple.PrintAll()
	fmt.Println("Watermelon, which inherited Apple's Method")
	watermelon.PrintAll()
}
```

Если предположить, что Go следует традиционному наследованию, то этот код кажется корректным. Однако его вывод выглядит так:

```
Inheritance test #1
Succeed: Label was  Apple_1
Apple, before copied to Watermelon
Fruit: apple, Label: Apple_1, Brix: 14.5
Failed: Label was  WaterMelon_2
Apple, after copied to Watermelon
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
Watermelon, which inherited Apple's Method
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
```

Здесь поведение Go становится понятнее:

```go
watermelon := apple
```

Этот код вовсе не превращает Apple в структуру Watermelon.
Переменная watermelon является лишь указателем на apple.

Повторим ещё раз: **Go не следует традиционной концепции наследования.**

Если писать код с таким непониманием, возникают серьёзные ошибки: бессмысленное создание указателей, непреднамеренное копирование функций для других структур и т. п.

Как же выглядит образцовый код?

## Подходящий пример на Go

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	Fruit BaseFruit
}

type Watermelon struct {
	Label string
	Fruit BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Fruit.Brix 	= 14.5;
	a.Fruit.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Fruit.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
	a.Fruit.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Fruit.Brix = 10;
	w.Fruit.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Fruit.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
	w.Fruit.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

Однако в Go можно добиться того, чтобы выглядело **как** наследование.
Это анонимное встраивание (embedding). Оно возможно, если внутреннюю структуру объявить без имени.
В этом случае **поля вложенной структуры можно использовать без явного указания**, и к ним есть прямой доступ.
Используя этот паттерн продвижения (**promotion**) полей вложенной структуры в родительскую, в некоторых случаях можно повысить читаемость. Однако если нужно явно показывать вложенную структуру, не рекомендуется её использовать.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	BaseFruit
}

type Watermelon struct {
	Label string
	BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
	a.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
	w.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

В этом примере есть такое различие:

```go
w.PrintAll() // Не w.Fruit.PrintAll(), а автоматический вызов через анонимную структуру (promotion)
```

Оба примера объединяет следующее важное положение:

- main должна быть краткой, функции разделяются по функциональности
- для разных структур используются разные объекты
- при необходимости совместного использования применяйте вложенную структуру

Какие преимущества даёт такой подход к программированию?

## Преимущества

- Чёткое разграничение методов, требующих совместного использования, и остальных
- Разделение ответственности между отдельными структурами и методами
- Код, структурно разделённый в соответствии с требуемой спецификацией функций

Поначалу Go может казаться непривычным, отличаясь от традиционного ООП, но после освоения становится возможным явное и понятное программирование.

## Итоги

- Изолируйте зоны ответственности
- Разделяйте код на детальные единицы по структурам
- Методы не следует понимать как абстрактные классы из Java
- Пишите явный и конкретный код
  Go гораздо проще и понятнее традиционной объектно-ориентированной модели и должен рассматриваться по отдельности. Вместо расширяемого программирования пишите код поэтапно и структурно, разделяя его.
