Наследование является одним из основных принципов ООП и описывает отношение между классами по принципу того, кто/что представляет собой. Например, утка является птицей. Птица — это животное, то есть строится иерархия наследования к максимально абстрактному объекту.

Можно сказать, наследование должно подразумевать, что все утки — это птицы. При этом обратное неверно, ведь не все птицы утки.

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

Композиция — это отношение, которое что-либо включает в себя. Например, утка состоит из клюва, туловища и лап. Клюв даёт ей возможность крякать, значит, утка может крякать. Не всё, что имеет клюв — это утка. Но все утки имеют клюв.

Эмбеддинг, то есть встраивание — это реализация композиции в Go.

В общем случае встраивание выглядит так:

type OuterStruct struct {
    EmbeddedType
    A int
    B int
} 

Tip

Все поля и методы EmbeddedType будут переданы в структуру OuterStruct, как если бы она сама их содержала. Это позволяет переиспользовать код сложных структур, встраивая одни в другие.

Рассмотрим на примере. Создадим две структуры Person и Student. При этом Student будет включать в себя Person. Это похоже на наследование, но есть существенные отличия. Структура Student не является Person, можно сказать, что она включает её в себя.

Объект Student не может быть приведён к типу Person с помощью приведения типов. Здесь имеется в виду классическое приведение типов в ООП языках, где экземпляр производного класса может выступать в качестве экземпляра базового класса.

Условно говоря такой конструкции, как person := Student(person) для встроенных типов в Go нет.

package main
 
import (
    "fmt"
)
 
// Person — структура, описывающая человека.
type Person struct {
    Name string
    Year int
}
 
// NewPerson возвращает новую структуру Person.
func NewPerson(name string, year int) Person {
    return Person{
        Name: name,
        Year: year,
    }
}
 
// String возвращает информацию о человеке.
func (p Person) String() string {
    return fmt.Sprintf("Имя: %s, Год рождения: %d", p.Name, p.Year)
}
 
// Print выводит информацию о человеке.
func (p Person) Print() {
    // вызовется метод String() для Person 
    fmt.Println(p)
}
 
// Student описывает студента с использованием вложенной структуры Person. То есть структура Student описывает.  
type Student struct {
    Person // вложенный объект Person
    Group  string
}
 
func NewStudent(name string, year int, group string) Student {
    return Student{
        Person: NewPerson(name, year), // Явно создаём структуру Person 
        Group:  group,
    }
}
 
// String возвращает информацию о студенте. 
func (s Student) String() string {
    return fmt.Sprintf("%s, Группа: %s", s.Person, s.Group)
}
 
func main() {
    s := NewStudent("John Doe", 1980, "701")
    s.Print()
    // вызовется метод String() для Student
    fmt.Println(s)
    fmt.Println(s.Name, s.Year, s.Group)
}
Имя: John Doe, Год рождения: 1980
Имя: John Doe, Год рождения: 1980, Группа: 701
John Doe 1980 701

В примере тип Student наследует все поля и методы объекта Person. Так как метод Print определён только для типа Person, то он выводит только имя и год рождения. При вызове функции fmt.Println используется метод String(), который определён для типа Student, поэтому выводится вся информация о студенте.

Доступ к полям вложенных структур

Если вложенный тип описан в другом пакете, то использующий его тип имеет доступ только к экспортируемым (публичным) методам и полям. Есть несколько способов обеспечить доступ к полям вложенных структур. Покажем их, добавив Debug()-метод для типа Student:

func (s *Student) Debug() {
    // доступ к методам объекта Person
    s.Print()
    // или
    s.Person.Print()
 
    // доступ к полю 'Name' объекта Person
    s.Name = "Mark Smith"
    // или
    s.Person.Name = "Mark Smith"
 
    // вызовется метод String объекта Student
    fmt.Println(s)
    // вызовется метод String объекта Person
    fmt.Println(s.Person)
} 

Метод структуры, в которую встроен другой тип, не переопределяется, а затеняется. То есть при вызове метода с таким же именем Go сначала попытается найти метод среди методов структуры Student, затем ищет его среди вложенных типов. Если несколько вложенных типов имеют одинаковые методы, происходит конфликт.

Обращаться к полям (или методам) вложенной структуры можно как с указанием типа объекта (s.Person.Print()), так и без (s.Print()). Для String-метода объекта Person требуется явно указывать тип, так как метод переопределён типом Student.

Фактически структура имеет поле с именем, совпадающим с именем типа. Однако при вызове метода вложенной структуры в неё передаётся не переменная, которая её вызвала, а это поле. При необходимости можно явно обратиться к методу вложенной структуры.

Есть два правила разрешения конфликтов имён полей и методов:

  1. Именованное поле (метод) структуры скрывает поле (метод) с тем же именем для вложенных структур. Имя верхнего уровня доминирует над именами более низких уровней. Например, вызов метода student.String()вызывает метод структуры Student, а не структуры Person.
  2. Если имя поля (метода) встречается на том же уровне вложенности (дупликация имён), и оно использовано в коде, это ошибка. Если дупликация имён существует, но это имя не используется в коде, компилятор не выдаст ошибку. Например, если тип Student имел бы ещё одну вложенную структуру Faculty с методом Print(), то тогда при вызове метода необходимо указывать имя вложенного типа s.Person.Print() или s.Faculty.Print(). Например:
    type Faculty string
 
    func (f Faculty) String() string {
        return string(f)    
    }
 
    func (f Faculty) Print() { 
        fmt.Println(f)
    }
 
 
    type Student struct {
        Person 
        Faculty
        Group  string
    }
    
    func main {
    //    s.Print()  
        fmt.Println(s)
    }

Если в примере выше расскомментировать строку s.Print(), то получим ошибку компиляции. ambiguous selector s.Print компилятор не может определить точно, пытается сообразить, какой же метод выбрать от Faculty или от Person. И выдает ошибку.

При этом метод String(), который вызывается в Println(), скрывает все методы String() у вложенных типов. И ошибки не возникает.

Следует отменить, что эти же правила действуют не только для методов, но и для полей структур.

Можно переписать пример без использования вложенных структур и работать с нужными объектами, как с обычными полями:

type Student struct {
    Person Person
    Group  string
} 

В этом случае тип Person уже не будет вложенным и необходимо явно указывать s.Person.Print(), s.Person.Name и т.д.

Механизм embedding находится в отрыве от Go-интерфейсов. Он воспринимается как синтаксический сахар, но становится полезным, когда нужно, чтобы объект соответствовал определённым интерфейсам. Если тип имеет вложенный тип, то он реализует все интерфейсы этого типа.

Встраивание указателей на тип

В структуру может быть встроен указатель на тип. Рассмотрим на примере:

    type Student struct {
        *Person
        Group  string  
    } 

Внешние отличия небольшие, но встраивание указателя может быть удобно, если вы встраиваете в вашу структуру какую-то большую структуру и затем передаёте её по значению. Это может повысить производительность за счёт того, что копируется не вся структура, а только указатель на неё. Важно отметить, что в таком случае встроенная структура может быть изменена:

    // принимает структуру по значению
    func ChangeName( s Student, name string) {
        s.Name = name
    }
 
    s := Student{&Person{Name: "alex"}, "021"}
    ChangeName(s, "teodor")
    fmt.Println(s.Name) // "teodor" 

Эта функция изменит имя студента, если поле Person встроено как указатель. Если встроить просто Person, то имя не изменится:

    type Student struct {
        Person
        Group  string
    }
 
    func ChangeName( s Student, name string) {
        s.Name = name
    }
    
    s := Student{Person{Name: "alex"}, "021"}
    ChangeName(s, "teodor") 
    fmt.Println(s.Name) // "alex"

Ограничения на встроенные типы

Согласно спецификации Go, не все типы данных могут быть встроены в структуры. Могут быть встроены типы или указатели на типы. При этом, если встраивается указатель на тип, то сам тип не может быть указателем. То есть можно встроить, например, тип Person и тип *Person, но нельзя встроить тип **Person.

Применение

Ещё одно применение встраивания — это расширение возможностей внешних типов. Хочется иметь все возможности, которые представляет тип, находящийся во внешнем пакете, но не можем изменить сам пакет. В таком случае можно создать свой тип, встроить в него внешний и добавить необходимые методы и поля. Полученный тип будет содержать методы встроенного типа.

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

Возьмём из предыдущего урока тип CircularBuffer и добавим метод, который может добавлять в наш буфер сразу несколько значений. Представим, что нельзя изменить его код (такое очень часто бывает в промышленной разработке):

// ExtendedCircularBuffer — «наследник» типа CircularBuffer, реализующий добавление нескольких элементов
type ExtendedCircularBuffer struct {
    CircularBuffer
}
 
func (cb * ExtendedCircularBuffer) AddValues(vals... float64)  {
    for _,val := range vals {
        cb.Addvalues(val)    
    }
}
 
func NewExtendedCircularBuffer (size int) ExtendedCircularBuffer {
    return ExtendedCircularBuffer{
        CircularBuffer : NewCircularBuffer(size),    
    }
}
 
 
func main() {
    buffer := NewExtendedCircularBuffer(5)
    buffer.Addvalues(1,2,3,4,5)
    fmt.Printf("[%d]: %v\n", buffer.GetCurrentSize(), buffer.GetValues())        
} 
[5]: [1 2 3 4 5]

📂 Go | Последнее изменение: 26.08.2024 19:32