Deep Engineering

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("  цепочке «родителей», ни таблицы методов — разница в пределах шума.")
}