Deep Engineering

ЗАМЕР

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