Deep Engineering

ЗАМЕР

bench/goembed/practice.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".

Скрипт

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