Deep Engineering

MEASUREMENT

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

286 lines
//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("  имя на наименьшей глубине.")
}