Язык Go — embedding: встраивание типов
Embedding в Go — как встраивать интерфейсы и структуры, продвижение методов, отличие от наследования и правила разрешения конфликтов имён.
Содержание
В Go нет наследования в привычном смысле. Вместо него — встраивание: тип можно включить в структуру или интерфейс без имени поля. Встроенный тип передаёт свои методы внешнему автоматически.
Встраивание интерфейсов
Интерфейсы можно составлять из других интерфейсов. Вместо того чтобы перечислять методы вручную — включаем готовые интерфейсы:
// стандартные интерфейсы из пакета io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter — объединение двух интерфейсов через embedding
// эквивалентно явному перечислению обоих методов, но читается яснее
type ReadWriter interface {
Reader
Writer
}
ReadWriter автоматически требует оба метода: Read и Write. Любой тип, реализующий оба, удовлетворяет всем трём интерфейсам сразу.
Более сложный пример — интерфейс с тремя составными частями:
// ReadWriteCloser объединяет три интерфейса
type ReadWriteCloser interface {
Reader
Writer
io.Closer // Close() error
}
// любой тип с Read, Write и Close удовлетворяет ReadWriteCloser
В интерфейс можно встроить только другой интерфейс — не структуру.
Встраивание структур
Со структурами embedding работает глубже. Встроенный тип указывается без имени поля — только тип:
// bufio.ReadWriter из стандартной библиотеки
// встраивает указатели на Reader и Writer без имён полей
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Сравним с обычным подходом через именованные поля:
// именованные поля — так тоже работает, но методы не продвигаются автоматически
type ReadWriterNamed struct {
reader *Reader
writer *Writer
}
// пришлось бы писать форвардинг вручную для каждого метода:
func (rw *ReadWriterNamed) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
func (rw *ReadWriterNamed) Write(p []byte) (n int, err error) {
return rw.writer.Write(p)
}
// ... и так для каждого метода обоих типов
Embedding избавляет от этого. bufio.ReadWriter автоматически получает все методы bufio.Reader и bufio.Writer — и удовлетворяет io.Reader, io.Writer и io.ReadWriter без единой строки форвардинга.
Встраивание — не наследование
Ключевое отличие: когда вызывается метод встроенного типа, ресивером остаётся внутренний тип, а не внешний.
rw := &bufio.ReadWriter{...}
rw.Read(p) // вызывает Read у *bufio.Reader, не у *bufio.ReadWriter
Это как если бы форвардинг был написан явно: return rw.Reader.Read(p). Внешний тип не «наследует» поведение — он делегирует его.
// наглядный пример: два типа с одним методом
type Base struct{}
func (b Base) Describe() string {
return "я Base"
}
type Outer struct {
Base
}
o := Outer{}
fmt.Println(o.Describe()) // "я Base" — ресивер Base, не Outer
Если нужно переопределить метод — объявляем его на внешнем типе явно:
func (o Outer) Describe() string {
return "я Outer, внутри: " + o.Base.Describe()
}
fmt.Println(o.Describe()) // "я Outer, внутри: я Base"
Практический пример: Job с логгером
Embedding удобен когда хочется добавить функциональность без лишних полей. Job встраивает *log.Logger — и сразу получает все его методы:
type Job struct {
Command string
*log.Logger // встраиваем указатель — нет имени поля
}
// инициализация через конструктор
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
// или через составной литерал
job := &Job{
Command: "backup",
Logger: log.New(os.Stderr, "Job: ", log.Ldate),
// тип без пакетного префикса служит именем поля: Logger, не log.Logger
}
// методы Logger доступны напрямую на job
job.Println("starting now...") // как будто у Job есть метод Println
job.Printf("running %s", job.Command)
Обратиться к встроенному полю напрямую — через имя типа без пакета:
job.Logger.SetPrefix("URGENT: ") // прямой доступ к встроенному Logger
Можно переопределить метод встроенного типа, добавив контекст:
// переопределяем Printf чтобы автоматически добавлять команду в каждое сообщение
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
job.Printf("done") // напечатает: "backup": done
Встраивание нескольких типов
Структура может встраивать несколько типов одновременно:
type Server struct {
net.Listener // принимаем соединения
*sync.Mutex // защищаем общее состояние
connections map[string]net.Conn
}
func NewServer(addr string) (*Server, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return &Server{
Listener: l,
Mutex: &sync.Mutex{},
connections: make(map[string]net.Conn),
}, nil
}
s, _ := NewServer(":8080")
s.Lock() // метод от *sync.Mutex
defer s.Unlock()
conn, _ := s.Accept() // метод от net.Listener
s.connections[conn.RemoteAddr().String()] = conn
Конфликты имён
При встраивании нескольких типов может возникнуть конфликт имён. Правила разрешения простые.
Правило 1: поле или метод внешнего типа перекрывает одноимённое поле встроенного:
type Logger struct{ Command string }
func (l Logger) Describe() string { return "logger" }
type Job struct {
Command string // перекрывает Logger.Command
Logger
}
job := Job{Command: "backup", Logger: Logger{Command: "ignored"}}
fmt.Println(job.Command) // "backup" — поле Job, не Logger
fmt.Println(job.Logger.Command) // "ignored" — явный доступ к Logger.Command
Правило 2: если одинаковое имя на одном уровне вложенности — ошибка при обращении (но не при объявлении):
type A struct{ Name string }
type B struct{ Name string }
type C struct {
A
B
}
c := C{}
// fmt.Println(c.Name) // ошибка компиляции: ambiguous selector c.Name
fmt.Println(c.A.Name) // ок — явное указание
fmt.Println(c.B.Name) // ок
Если Name нигде не используется — конфликта нет, код компилируется.
Итоги
- Embedding встраивает тип в структуру или интерфейс без имени поля
- Методы встроенного типа продвигаются на внешний — форвардинг писать не нужно
- Ресивер встроенного метода — внутренний тип, не внешний: это делегирование, не наследование
- Обратиться к встроенному полю — через имя типа без пакетного префикса:
job.Logger, неjob.log.Logger - Внешний тип может переопределить метод встроенного — просто объявите метод с тем же именем
- Поле внешнего типа перекрывает одноимённое поле встроенного (более глубокого уровня)
- Одинаковые имена на одном уровне вложенности — ошибка компиляции при обращении