MEASUREMENT
bench/goembed/practice.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/embedding
- How to run it
go run bench/goembed/internals.go bash bench/goembed/ambiguous.sh bash bench/goembed/compile.sh go run bench/goembed/practice.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Встраивание структур»
| Файл | Что делает |
|---|---|
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".
Script
116 lines//go:build ignore
// Две задачи урока про встраивание: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Три места, где встраивание ведёт себя не как
// наследование: поднятый метод вызывает метод своего типа, а не внешнего;
// встроенный T не поднимает методы с получателем-указателем в набор методов
// значения; два одноимённых поля на одной глубине пропадают из JSON молча.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз вызов метода, поднятого из
// встроенного *T, дороже прямого вызова того же метода. Ответ нужен не ради
// наносекунд: он показывает, что встраивание — решение компилятора о путях
// к полю, а не механизм, работающий во время выполнения.
//
// ЗАПУСК:
//
// go run bench/goembed/practice.go
package main
import (
"encoding/json"
"fmt"
"runtime"
"testing"
)
// ---------------------------------------------------------------- задача 1
type Base struct{}
func (Base) Name() string { return "Base" }
func (b Base) Describe() string { return "describe: " + b.Name() }
type Derived struct{ Base }
func (Derived) Name() string { return "Derived" }
type Saver interface{ Save() string }
type Store struct{}
func (s *Store) Save() string { return "saved" }
type ByValue struct{ Store }
type Audit struct{ ID int }
type Owner struct{ ID int }
type Doc struct {
Audit
Owner
Title string
}
// ---------------------------------------------------------------- задача 2
type Engine struct{ rpm int }
//go:noinline
func (e *Engine) Rev() int { e.rpm++; return e.rpm }
type Car struct {
*Engine
brand string
}
var sink int
func best(f func(b *testing.B)) float64 {
lo := 1e18
for round := 0; round < 7; round++ {
r := testing.Benchmark(f)
if ns := float64(r.T.Nanoseconds()) / float64(r.N); ns < lo {
lo = ns
}
}
return lo
}
func main() {
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
d := Derived{}
fmt.Println(d.Name())
fmt.Println(d.Describe())
_, ok := any(ByValue{}).(Saver)
fmt.Println(ok)
b, _ := json.Marshal(Doc{Audit{1}, Owner{2}, "report"})
fmt.Println(string(b))
fmt.Println()
fmt.Println("ЗАДАЧА 2 — во сколько раз поднятый метод дороже прямого вызова")
fmt.Println()
e := &Engine{}
c := Car{Engine: &Engine{}, brand: "x"}
direct := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = e.Rev()
}
})
promoted := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = c.Rev()
}
})
fmt.Printf(" e.Rev() прямой вызов %5.2f нс\n", direct)
fmt.Printf(" c.Rev() поднят из встроенного *Engine %5.2f нс\n", promoted)
fmt.Printf(" кратность %5.2f\n", promoted/direct)
fmt.Printf(" она же округлённо %5.1f\n", promoted/direct)
fmt.Println()
fmt.Printf(" Лучший из 7 кругов. %s %s/%s, GOMAXPROCS=%d\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
fmt.Println()
fmt.Println(" c.Rev() — это c.Engine.Rev(): путь к методу выбирается при")
fmt.Println(" компиляции, по глубине. Во время выполнения нет ни поиска по")
fmt.Println(" цепочке «родителей», ни таблицы методов — разница в пределах шума.")
}