ЗАМЕР
bench/goembed/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/embedding
- Как запустить
go run bench/goembed/internals.go bash bench/goembed/ambiguous.sh bash bench/goembed/compile.sh go run bench/goembed/practice.go
Запись прогона
Замеры для урока «Встраивание структур»
| Файл | Что делает |
|---|---|
internals.go |
десять наблюдений без единого замера времени: подъём полей и методов, отсутствие виртуального вызова у встраивания, набор методов при встроенном T и *T, глубина имён, встроенный интерфейс, оставшийся nil, методы sync.Mutex в API внешнего типа, конфликт имён в JSON; динамический вызов через интерфейс, встроенный указатель, оставшийся nil, неоднозначное имя в наборе методов |
ambiguous.sh |
собирает ambiguous/main.go с обращением к спорному имени и без него и печатает ошибку компилятора |
ambiguous/main.go |
подопытная программа к ambiguous.sh; намеренно не собирается |
compile.sh |
собирает compile/* и печатает ошибки компилятора: метод на *T при встроенном T, неоднозначное имя при присваивании интерфейсу, одинаковые поля на глубине 0, тег json, который не разрешает неоднозначность селектора |
compile/*/main.go |
подопытные программы к compile.sh; намеренно не собираются |
practice.go |
источник ответов двух практических задач урока |
go run bench/goembed/internals.go
bash bench/goembed/ambiguous.sh
bash bench/goembed/compile.sh
go run bench/goembed/practice.go
Записи прогонов — в runs/, go1.24.7 linux/amd64.
Что получилось
Встраивание — не наследование, и это видно на одной строке. Derived
встраивает Base и объявляет свой Name. d.Name() даёт "Derived", а
d.Describe() — поднятый из Base — даёт "describe: Base": внутри поднятого
метода получатель — Base, и он вызывает свой Name.
Набор методов зависит от того, встроен T или *T. Save объявлен на
*Store. ByValue{} со встроенным Store интерфейс Saver не реализует,
&ByValue{} — реализует; со встроенным *Store реализуют оба. Ровно так это
сформулировано в спецификации.
Два одинаковых имени на одной глубине — не ошибка объявления. Ошибка
появляется только при обращении: ambiguous selector f.Close. Без обращения
файл собирается.
Встроенный sync.Mutex выходит в API. Методы *Cache: Lock,
TryLock, Unlock — ни одного из них Cache не объявлял.
JSON молча выбрасывает конфликт. Два встроенных типа с полем ID дают
{"Title":"report"} — без ID вовсе и без ошибки. Правило записано в
документации encoding/json: несколько полей на одном уровне без тега
игнорируются все.
Поднятый вызов не дороже прямого. Вызов метода, поднятого из встроенного *Engine, — 2,14 нс
против 2,18 нс прямого: кратность 0,98. Путь к методу выбирается при
компиляции.
Неоднозначное имя выпадает из набора методов. any(File{}).(Closer) даёт
false, а присваивание File{} интерфейсу Closer не собирается с
ambiguous selector File.Close — хотя обращения f.Close в файле нет
(compile.sh). Одинаковые имена на разной глубине — не ошибка; одинаковые поля
на глубине 0 — ошибка объявления ID redeclared.
Встроенный указатель, оставшийся nil. Метод с получателем *T получает
nil и может его проверить; метод с получателем T и поднятое поле — паника.
Динамический вызов в Go есть — через интерфейс. Та же формула, что у
Describe, но с параметром-интерфейсом даёт "describe: Derived".
Скрипт
286 строк//go:build ignore
// Встраивание структур в Go: что поднимается наверх и чего не происходит.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про встраивание обычно говорят «в Go вместо
// наследования композиция» — и на этом останавливаются. Но вопросы на
// собеседовании начинаются ровно там, где встраивание ведёт себя НЕ как
// наследование, и каждый из них проверяется запуском, а не пересказом.
//
// СЕМЬ СЮЖЕТОВ (И ТРИ УТОЧНЕНИЯ):
//
// 1. Подъём. Поля и методы встроенного типа доступны как свои.
// 2. Виртуального вызова нет. Метод встроенного типа, вызывающий другой свой
// метод, вызывает СВОЙ, даже если внешний тип «переопределил» его.
// 3. Набор методов. Встроенный T и встроенный *T поднимают разные методы, и
// от этого зависит, реализует ли внешний тип интерфейс.
// 4. Глубина. Внешнее имя прячет встроенное; два одинаковых имени на одной
// глубине — не ошибка, пока к ним не обратились (см. ambiguous.sh).
// 5. Встроенный интерфейс. Поле интерфейсного типа даёт все методы
// интерфейса — и панику, если поле осталось nil.
// 6. Чужие методы наружу. Встроенный sync.Mutex делает Lock и Unlock частью
// публичного API внешнего типа.
// 7. JSON. Поля встроенной структуры выходят на верхний уровень, а два поля с
// одним именем на одной глубине молча пропадают оба.
//
// И три уточнения, добавленные после вычитки урока:
//
// 8. Динамический вызов в Go есть — через интерфейс, а не через встраивание.
// 9. Встроенный указатель, оставшийся nil: метод с получателем *T работает,
// метод с получателем T и поле — паника.
// 10. Неоднозначное имя выпадает из набора методов; одинаковые имена на
// разной глубине — не ошибка.
//
// ЗАПУСК:
//
// go run bench/goembed/internals.go
package main
import (
"encoding/json"
"fmt"
"reflect"
"runtime"
"strings"
"sync"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// ------------------------------------------------------------- 1. подъём
type Logger struct{ prefix string }
func (l Logger) Log(msg string) string { return l.prefix + msg }
type Server struct {
Logger
name string
}
// ----------------------------------------------- 2. виртуального вызова нет
type Base struct{}
func (Base) Name() string { return "Base" }
// Describe вызывает Name — СВОЙ, Base.Name. Вопрос в том, чей Name окажется
// здесь, если Describe вызвать у типа, который встроил Base и объявил свой Name.
func (b Base) Describe() string { return "describe: " + b.Name() }
type Derived struct{ Base }
func (Derived) Name() string { return "Derived" }
// ------------------------------------------------------- 3. набор методов
type Saver interface{ Save() string }
type Store struct{}
func (s *Store) Save() string { return "saved" } // получатель — указатель
type ByValue struct{ Store } // встроен T
type ByPointer struct{ *Store } // встроен *T
// ------------------------------------------------------------- 4. глубина
type Inner struct{ ID string }
type Outer struct {
Inner
ID string // глубина 0 прячет Inner.ID с глубины 1
}
// ------------------------------------------------- 5. встроенный интерфейс
type ReadCloser interface {
Read() string
Close() string
}
// fakeRC — типичная тестовая заглушка: встраивает интерфейс, чтобы
// «реализовать» его целиком, а переопределяет только то, что нужно тесту.
type fakeRC struct {
ReadCloser
}
func (fakeRC) Read() string { return "данные" }
// ------------------------------------------------------- 6. чужие методы
type Cache struct {
sync.Mutex
data map[string]string
}
// ---------------------------------------------------------------- 7. JSON
type Audit struct{ ID int }
type Owner struct{ ID int }
type Doc struct {
Audit
Owner
Title string
}
type TaggedOwner struct {
ID int `json:"ID"`
}
type DocTagged struct {
Audit
TaggedOwner
Title string
}
// ------------------------------------------- 8. диспетчеризация — в интерфейсах
type Namer interface{ Name() string }
func DescribeAny(n Namer) string { return "describe: " + n.Name() }
// ------------------------------------------ 9. встроенный указатель, nil
type Engine struct{ X int }
func (e *Engine) Ptr() string {
if e == nil {
return "получатель nil, метод проверил и отработал"
}
return "получатель не nil"
}
func (e Engine) Val() string { return "значение" }
type Holder struct{ *Engine }
// ------------------------------------- 10. неоднозначность и набор методов
type Closer interface{ Close() string }
type Reader struct{}
func (Reader) Close() string { return "Reader.Close" }
type Writer struct{}
func (Writer) Close() string { return "Writer.Close" }
type File struct {
Reader
Writer
}
type Leaf struct{ ID string }
type Mid struct{ Leaf }
type Side struct{ ID string }
type Deep struct {
Mid // Mid.Leaf.ID — глубина 2
Side // Side.ID — глубина 1
}
func main() {
fmt.Printf("%s %s/%s | встраивание структур\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) ПОДЪЁМ ПОЛЕЙ И МЕТОДОВ")
s := Server{Logger: Logger{prefix: "[api] "}, name: "api"}
fmt.Printf(" s.Log(\"старт\") -> %q\n", s.Log("старт"))
fmt.Printf(" s.Logger.Log(\"старт\") -> %q\n", s.Logger.Log("старт"))
fmt.Printf(" s.prefix -> %q\n", s.prefix)
show("2) У ВСТРАИВАНИЯ ВИРТУАЛЬНОГО ВЫЗОВА НЕТ")
d := Derived{}
fmt.Printf(" d.Name() -> %q\n", d.Name())
fmt.Printf(" d.Describe() -> %q\n", d.Describe())
fmt.Printf(" d.Base.Name() -> %q\n", d.Base.Name())
fmt.Println(" Describe поднят из Base и получает Base, а не Derived:")
fmt.Println(" Name внутри него — Base.Name, что бы ни объявил внешний тип.")
show("3) НАБОР МЕТОДОВ: ВСТРОЕН T ИЛИ *T")
check := func(label string, v any) {
_, ok := v.(Saver)
fmt.Printf(" %-28s реализует Saver: %v\n", label, ok)
}
check("ByValue{} (встроен Store)", ByValue{})
check("&ByValue{}", &ByValue{})
check("ByPointer{} (встроен *Store)", ByPointer{&Store{}})
check("&ByPointer{}", &ByPointer{&Store{}})
fmt.Println(" Save объявлен на *Store. Встроенный Store поднимает его")
fmt.Println(" только в набор методов *ByValue; встроенный *Store —")
fmt.Println(" в оба набора.")
show("4) ГЛУБИНА: ВНЕШНЕЕ ИМЯ ПРЯЧЕТ ВСТРОЕННОЕ")
o := Outer{Inner: Inner{ID: "внутренний"}, ID: "внешний"}
fmt.Printf(" o.ID -> %q\n", o.ID)
fmt.Printf(" o.Inner.ID -> %q\n", o.Inner.ID)
show("5) ВСТРОЕННЫЙ ИНТЕРФЕЙС, ОСТАВШИЙСЯ nil")
var rc ReadCloser = fakeRC{}
fmt.Printf(" rc.Read() -> %q\n", rc.Read())
func() {
defer func() {
r := recover()
fmt.Printf(" rc.Close() -> паника: %v\n", r)
}()
rc.Close()
}()
fmt.Println(" Компилятор доволен: fakeRC реализует ReadCloser. Падение —")
fmt.Println(" только при вызове метода, который не переопределён.")
show("6) ЧУЖИЕ МЕТОДЫ В ПУБЛИЧНОМ API")
t := reflect.TypeOf(&Cache{})
var names []string
for i := 0; i < t.NumMethod(); i++ {
names = append(names, t.Method(i).Name)
}
fmt.Printf(" методы *Cache: %v\n", names)
fmt.Println(" Ни одного из них Cache не объявлял: все три пришли из")
fmt.Println(" встроенного sync.Mutex и доступны любому, кто получил *Cache.")
show("7) JSON: ПОДЪЁМ И МОЛЧАЛИВЫЙ КОНФЛИКТ")
b1, _ := json.Marshal(Doc{Audit: Audit{ID: 1}, Owner: Owner{ID: 2}, Title: "report"})
fmt.Printf(" два встроенных ID %s\n", b1)
b2, _ := json.Marshal(DocTagged{Audit: Audit{ID: 1}, TaggedOwner: TaggedOwner{ID: 2}, Title: "report"})
fmt.Printf(" один из них с тегом %s\n", b2)
fmt.Println(" Два поля ID на одной глубине: json не выбирает и не")
fmt.Println(" сообщает об ошибке — оба поля пропадают. С тегом у одного")
fmt.Println(" из них выбирается помеченное.")
show("8) ДИНАМИЧЕСКИЙ ВЫЗОВ — ЧЕРЕЗ ИНТЕРФЕЙС")
fmt.Printf(" DescribeAny(d) -> %q\n", DescribeAny(d))
fmt.Println(" Та же формула, но Name вызывается через интерфейс Namer:")
fmt.Println(" какой метод сработает, решает значение во время выполнения.")
show("9) ВСТРОЕННЫЙ УКАЗАТЕЛЬ, ОСТАВШИЙСЯ nil")
h := Holder{}
try := func(label string, f func() string) {
defer func() {
if r := recover(); r != nil {
fmt.Printf(" %-24s -> паника: %v\n", label, r)
}
}()
fmt.Printf(" %-24s -> %q\n", label, f())
}
try("h.Ptr() (получатель *T)", h.Ptr)
try("h.Val() (получатель T)", func() string { return h.Val() })
try("h.X (поле)", func() string { return fmt.Sprint(h.X) })
fmt.Println(" Метод с получателем *T получает nil и может его проверить.")
fmt.Println(" Для метода с получателем T и для поля указатель нужно")
fmt.Println(" разыменовать — и это паника.")
show("10) НЕОДНОЗНАЧНОЕ ИМЯ И НАБОР МЕТОДОВ")
_, ok := any(File{}).(Closer)
fmt.Printf(" any(File{}).(Closer) -> реализует: %v\n", ok)
fmt.Println(" Close есть у Reader и у Writer на глубине 1, поэтому у File")
fmt.Println(" его нет в наборе методов вовсе. Проверка во время выполнения")
fmt.Println(" молча даёт false; присваивание интерфейсу не собирается")
fmt.Println(" (см. compile.sh).")
dp := Deep{Mid: Mid{Leaf{ID: "глубина 2"}}, Side: Side{ID: "глубина 1"}}
fmt.Printf(" dp.ID -> %q\n", dp.ID)
fmt.Println(" ID есть на глубине 1 и на глубине 2: ошибки нет, выбрано")
fmt.Println(" имя на наименьшей глубине.")
}