Deep Engineering

ЗАМЕР

bench/goreflect/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/reflection
Как запустить
go run bench/goreflect/internals.go
go run bench/goreflect/cost.go
go run bench/goreflect/practice.go
bash bench/goreflect/example.sh
GO=/usr/local/go1.25.1/bin/go bash bench/goreflect/typeassert.sh

Запись прогона

Замеры для урока «Рефлексия в Go»

Файл Что делает
internals.go семь наблюдений без единого замера времени: путь значения в reflect.Value и обратно, Type против Kind на именованном типе над int, правило изменяемости (CanAddr, CanSet, паника SetInt), неэкспортируемое поле, теги через StructTag.Get и Lookup рядом с выводом encoding/json, reflect.DeepEqual, nil внутри интерфейса и Value.IsNil
cost.go цена записи одного поля четырьмя способами: прямое присваивание, FieldByName, заранее найденный индекс поля, заранее найденный reflect.Value поля; и отдельно только поиск FieldByName первого поля против седьмого. Наносекунды и аллокации, лучший из 15 кругов
practice.go источник ответов двух практических задач урока
example.sh, cases/example/ полный короткий пример урока — отдельный модуль, его можно скопировать в пустой каталог и запустить go run .
typeassert.sh, cases/typeassert/ reflect.TypeAssert (Go 1.25): два результата, как утверждение типа с ok; модуль с go 1.25, запускать go1.25.1
go run bench/goreflect/internals.go
go run bench/goreflect/cost.go
go run bench/goreflect/practice.go
bash bench/goreflect/example.sh
GO=/usr/local/go1.25.1/bin/go bash bench/goreflect/typeassert.sh

Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра. internals.go прогнан ещё и на go1.25.1 (runs/internals-go1.25.txt): вывод тот же, кроме строки версии, — правило изменяемости, тексты паник и DeepEqual между версиями не изменились.

Времена сняты на общей машине, где параллельно шли другие задачи: абсолютные наносекунды от запуска к запуску гуляют заметно, кратность «FieldByName к индексу» устойчивее. Чтобы фоновая нагрузка доставалась всем способам поровну, круги способов в cost.go чередуются.

Что получилось

reflect.ValueOf видит копию. x := 3.4; v := reflect.ValueOf(x), потом x = 7.1 — а v.Float() по-прежнему 3.4. Поэтому такой Value и нельзя изменить: изменение копии ни на что бы не повлияло.

Type и Kind — разные вещи. У Celsius(21) Type — main.Celsius, Kind — int. Переключатель по Kind узнаёт в нём целое, переключатель по типу (case int:) — нет: «не знаю: main.Celsius».

Правило изменяемости. ValueOf(x) и ValueOf(&x) — CanSet=false, и SetInt паникует дословно так: reflect: reflect.Value.SetInt using unaddressable value. ValueOf(&x).Elem() — CanAddr=true CanSet=true, и после SetInt(42) в x лежит 42.

Неэкспортируемое поле читается, но не меняется. У поля age CanAddr=true CanSet=false; Int() даёт 30, Interface() паникует (cannot return value obtained from unexported field or method), SetInt — тоже (using value obtained using unexported field).

Теги. Get("json") даёт "" и для json:"", и для поля без тега; Lookup различает: "", true против "", false. encoding/json читает тег через Get("json") (encode.go:1134 в go1.24.7): "-" — поле пропущено, пустое имя — взято имя поля: {"id":1,"Note":"n","Plain":"p"}.

DeepEqual. []int(nil) и []int{} — false; карты с теми же парами в другом порядке — true; одна и та же функция, не равная nil, сама с собой — false, две nil-функции — true; Celsius(1) и 1 — false; NaN и NaN — false.

nil внутри интерфейса. any((*User)(nil)) == nil — false, а reflect.ValueOf(i).IsNil() — true. У пустого интерфейса ValueOf даёт нулевой Value: IsValid() — false, IsNil() паникует (call of reflect.Value.IsNil on zero Value); у int — тоже паника.

Цена. Ни один из способов не аллоцирует. Дороже всего поиск по имени: FieldByName — 76,21 нс, запись по заранее найденному индексу — 5,82 нс, кратность 13,1; запомненный Value поля — 2,02 нс. Прямое присваивание — доли наносекунды. В practice.go та же пара дала 78,77 и 7,10 нс, кратность 11,1.

Цена поиска почти не зависит от номера поля. Только поиск, без записи: первое поле (одно сравнение имён) — 53,97 нс, седьмое (семь сравнений) — 76,18 нс, отношение 0,71. Большая часть цены постоянна: после того как поле найдено, тип собирает полное описание StructField (reflect/type.go:1101), и только потом Value переходит к полю по индексу.

Контейнеры (добавлено после внешнего аудита 14.10.2026, раздел 8 internals.go). Правило «менять только через указатель» — про саму переменную. ValueOf(s).Index(0).SetInt(7) меняет элемент среза без &s (s = [7 2 3]), а ValueOf(s).Set(...) паникует. У массива элемент копии неадресуем. SetMapIndex пишет в карту (m = map[a:1 b:2]), хотя CanSet у Value карты — false; значение из MapIndex неизменяемо.

TypeAssert (go1.25.1). TypeAssert[string](ValueOf(1)) даёт "", false — как v.Interface().(string) с ok; без ok утверждение паникует. На поле, полученном через неэкспортируемое имя, TypeAssert паникует: reflect.TypeAssert: cannot return value obtained from unexported field or method.

На чём проверено: go1.24.7 и go1.25.1, linux/amd64, сверено 14.10.2026. Времена — наблюдение одной общей машины (2 ядра, лучший из 15 кругов), а не переносимая константа.

Скрипт

320 строк
//go:build ignore

// Рефлексия в Go: reflect.Type, reflect.Value и правило изменяемости.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про рефлексию на собеседовании обычно спрашивают
// «что такое reflect.Type и reflect.Value» — и дальше идут туда, где пакет
// ведёт себя неожиданно: почему SetInt паникует, чем Type отличается от Kind,
// почему неэкспортируемое поле можно прочитать, но нельзя изменить, как
// encoding/json узнаёт имена из тегов, почему DeepEqual считает nil-срез не
// равным пустому. Каждый ответ здесь — строка вывода, а не пересказ.
//
// СЕМЬ СЮЖЕТОВ:
//
//  1. Туда и обратно. Значение уходит в интерфейс, из интерфейса — в
//     reflect.Value, из Value — обратно в интерфейс. ValueOf видит копию.
//  2. Type против Kind. Именованный тип над int: Type — main.Celsius,
//     Kind — int. Переключатель по Kind ловит Celsius, переключатель по
//     типу — нет.
//  3. Правило изменяемости. ValueOf(x).SetInt паникует; ValueOf(&x) тоже;
//     ValueOf(&x).Elem() — изменяет x.
//  4. Неэкспортируемое поле. Читается, но не изменяется и не отдаётся
//     через Interface().
//  5. Теги. StructTag.Get и Lookup; что из этого делает encoding/json.
//  6. DeepEqual. nil-срез против пустого, карты по содержимому, функции.
//  7. nil внутри интерфейса. Value.IsNil против == nil и нулевой Value.
//
// И одно уточнение, добавленное после внешнего аудита 14.10.2026:
//
//  8. Контейнеры. Правило «менять только через указатель» верно для самой
//     переменной, но не для того, что доступно через неё: элемент среза
//     меняется через ValueOf(s).Index(0) без &s, а карта — через
//     SetMapIndex, хотя у Value карты CanSet — false. У массива, наоборот,
//     ValueOf(a) — копия, и её элементы неадресуемы.
//
// ЗАПУСК:
//
//	go run bench/goreflect/internals.go
package main

import (
	"encoding/json"
	"fmt"
	"math"
	"reflect"
	"runtime"
	"strings"
)

func show(title string) {
	fmt.Println()
	fmt.Println(title)
	fmt.Println(strings.Repeat("─", len([]rune(title))))
}

// try выполняет f и возвращает текст паники, если она была.
func try(f func()) (msg string) {
	defer func() {
		if r := recover(); r != nil {
			msg = "паника: " + fmt.Sprint(r)
		}
	}()
	f()
	return ""
}

// --------------------------------------------------------- 2. Type и Kind

type Celsius int

type User struct {
	Name string
	age  int
}

// byKind смотрит на устройство значения: годится любой тип над int.
func byKind(i any) string {
	v := reflect.ValueOf(i)
	switch v.Kind() {
	case reflect.Int, reflect.Int64:
		return fmt.Sprintf("целое: %d", v.Int())
	case reflect.String:
		return "строка: " + v.String()
	}
	return "не знаю: " + v.Kind().String()
}

// byType смотрит на сам тип: Celsius — не int.
func byType(i any) string {
	switch x := i.(type) {
	case int:
		return fmt.Sprintf("целое: %d", x)
	case string:
		return "строка: " + x
	}
	return fmt.Sprintf("не знаю: %T", i)
}

// ------------------------------------------------------------------ 5. теги

type Account struct {
	ID     int    `json:"id"`
	Email  string `json:"email,omitempty" db:"email_addr"`
	Note   string `json:""`
	Secret string `json:"-"`
	Plain  string
}

// ------------------------------------------------------- 7. nil в интерфейсе

// isNil — проверка «внутри интерфейса nil», которой нет у оператора ==.
func isNil(i any) bool {
	if i == nil {
		return true
	}
	v := reflect.ValueOf(i)
	switch v.Kind() {
	case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
		return v.IsNil()
	}
	return false
}

func main() {
	fmt.Printf("%s %s/%s | рефлексия: reflect.Type, reflect.Value и правило изменяемости\n",
		runtime.Version(), runtime.GOOS, runtime.GOARCH)

	// ------------------------------------------------------------------ 1
	show("1) ТУДА И ОБРАТНО: ИНТЕРФЕЙС -> REFLECT.VALUE -> ИНТЕРФЕЙС")
	{
		x := 3.4
		v := reflect.ValueOf(x)
		fmt.Printf("  reflect.TypeOf(x)        -> %v\n", reflect.TypeOf(x))
		fmt.Printf("  v.Kind()                 -> %v\n", v.Kind())
		fmt.Printf("  v.Float()                -> %v\n", v.Float())
		y := v.Interface().(float64)
		fmt.Printf("  v.Interface().(float64)  -> %v\n", y)
		x = 7.1
		fmt.Printf("  x = 7.1; v.Float()       -> %v\n", v.Float())
		fmt.Println("  ValueOf получил аргумент типа any — копию x. Value описывает")
		fmt.Println("  эту копию, а не переменную x, и изменение x её не трогает.")
	}

	// ------------------------------------------------------------------ 2
	show("2) TYPE ПРОТИВ KIND")
	{
		rows := []struct {
			expr string
			val  any
		}{
			{"Celsius(21)", Celsius(21)},
			{"21", 21},
			{"User{}", User{}},
			{"&User{}", &User{}},
			{"[]Celsius{}", []Celsius{}},
			{"map[string]int{}", map[string]int{}},
		}
		fmt.Printf("  %-18s %-16s %s\n", "значение", "Type", "Kind")
		for _, r := range rows {
			t := reflect.TypeOf(r.val)
			fmt.Printf("  %-18s %-16s %s\n", r.expr, t.String(), t.Kind())
		}
		fmt.Printf("  TypeOf(Celsius(21)) == TypeOf(21)  -> %v\n",
			reflect.TypeOf(Celsius(21)) == reflect.TypeOf(21))
		fmt.Printf("  byKind(Celsius(21))      -> %q\n", byKind(Celsius(21)))
		fmt.Printf("  byType(Celsius(21))      -> %q\n", byType(Celsius(21)))
		fmt.Println("  Type — это сам тип, с именем и пакетом. Kind — его устройство:")
		fmt.Println("  у Celsius и int оно одно. Код, который должен работать с любым")
		fmt.Println("  типом над int, переключается по Kind.")
	}

	// ------------------------------------------------------------------ 3
	show("3) ПРАВИЛО ИЗМЕНЯЕМОСТИ: CanSet")
	{
		x := 3
		cases := []struct {
			expr string
			v    reflect.Value
		}{
			{"ValueOf(x)", reflect.ValueOf(x)},
			{"ValueOf(&x)", reflect.ValueOf(&x)},
			{"ValueOf(&x).Elem()", reflect.ValueOf(&x).Elem()},
		}
		for _, c := range cases {
			fmt.Printf("  %-19s Kind=%-4v CanAddr=%-5v CanSet=%v\n",
				c.expr, c.v.Kind(), c.v.CanAddr(), c.v.CanSet())
		}
		for _, c := range cases {
			v := c.v
			msg := try(func() { v.SetInt(42) })
			if msg == "" {
				msg = fmt.Sprintf("x = %d", x)
			}
			fmt.Printf("  %-19s .SetInt(42) -> %s\n", c.expr, msg)
		}
		fmt.Println("  ValueOf(x) держит копию: менять её бессмысленно, и пакет")
		fmt.Println("  запрещает. ValueOf(&x) держит указатель — сам он тоже копия.")
		fmt.Println("  Elem() разыменовывает его: это и есть переменная x.")
	}

	// ------------------------------------------------------------------ 4
	show("4) НЕЭКСПОРТИРУЕМОЕ ПОЛЕ: ЧИТАЕТСЯ, НО НЕ МЕНЯЕТСЯ")
	{
		u := User{Name: "Ann", age: 30}
		v := reflect.ValueOf(&u).Elem()
		name, age := v.FieldByName("Name"), v.FieldByName("age")
		fmt.Printf("  поле Name: CanAddr=%v CanSet=%v\n", name.CanAddr(), name.CanSet())
		fmt.Printf("  поле age:  CanAddr=%v CanSet=%v\n", age.CanAddr(), age.CanSet())
		fmt.Printf("  age.Kind()               -> %v\n", age.Kind())
		fmt.Printf("  age.Int()                -> %d\n", age.Int())
		fmt.Printf("  age.Interface()          -> %s\n", try(func() { _ = age.Interface() }))
		fmt.Printf("  age.SetInt(31)           -> %s\n", try(func() { age.SetInt(31) }))
		name.SetString("Bob")
		fmt.Printf("  name.SetString(\"Bob\")    -> u = %+v\n", u)
		b, _ := json.Marshal(u)
		fmt.Printf("  json.Marshal(u)          -> %s\n", b)
		fmt.Println("  Поле age адресуемо — до него дошли через указатель, — но")
		fmt.Println("  получено через неэкспортируемое имя: Value несёт пометку")
		fmt.Println("  «только чтение». Int() читает; Interface() и SetInt паникуют.")
	}

	// ------------------------------------------------------------------ 5
	show("5) ТЕГИ СТРУКТУРЫ: Get И Lookup")
	{
		t := reflect.TypeOf(Account{})
		fmt.Printf("  %-7s %-20s %s\n", "поле", `Get("json")`, `Lookup("json")`)
		for i := 0; i < t.NumField(); i++ {
			f := t.Field(i)
			val, ok := f.Tag.Lookup("json")
			fmt.Printf("  %-7s %-20q %q, %v\n", f.Name, f.Tag.Get("json"), val, ok)
		}
		email, _ := t.FieldByName("Email")
		fmt.Printf("  Email.Tag.Get(\"db\")      -> %q\n", email.Tag.Get("db"))
		b, _ := json.Marshal(Account{ID: 1, Note: "n", Secret: "s", Plain: "p"})
		fmt.Printf("  json.Marshal             -> %s\n", b)
		fmt.Println("  Тег — просто строка в типе. Get не отличает пустой ключ от")
		fmt.Println("  отсутствующего; Lookup отличает. encoding/json читает тег через")
		fmt.Println("  Get(\"json\"): \"-\" — пропустить поле, пустое имя — взять имя поля.")
	}

	// ------------------------------------------------------------------ 6
	show("6) DEEPEQUAL: СОДЕРЖИМОЕ, А НЕ ОПЕРАТОР ==")
	{
		f := func() {}
		rows := []struct {
			expr string
			got  bool
		}{
			{"[]int(nil) и []int{}", reflect.DeepEqual([]int(nil), []int{})},
			{"[]int{1,2} и []int{1,2}", reflect.DeepEqual([]int{1, 2}, []int{1, 2})},
			{"карты {a:1,b:2} и {b:2,a:1}", reflect.DeepEqual(map[string]int{"a": 1, "b": 2}, map[string]int{"b": 2, "a": 1})},
			{"&User{Ann,30} и &User{Ann,30}", reflect.DeepEqual(&User{"Ann", 30}, &User{"Ann", 30})},
			{"User{Ann,30} и User{Ann,31}", reflect.DeepEqual(User{"Ann", 30}, User{"Ann", 31})},
			{"Celsius(1) и 1", reflect.DeepEqual(Celsius(1), 1)},
			{"f и f (одна функция)", reflect.DeepEqual(f, f)},
			{"func()(nil) и func()(nil)", reflect.DeepEqual((func())(nil), (func())(nil))},
			{"NaN и NaN", reflect.DeepEqual(math.NaN(), math.NaN())},
		}
		for _, r := range rows {
			fmt.Printf("  %-31s -> %v\n", r.expr, r.got)
		}
		p1, p2 := &User{"Ann", 30}, &User{"Ann", 30}
		fmt.Printf("  те же указатели через ==        -> %v\n", p1 == p2)
		fmt.Println("  DeepEqual сравнивает содержимое, но различает nil-срез и")
		fmt.Println("  пустой, разные типы с одинаковым устройством, а функцию")
		fmt.Println("  считает равной только nil — даже саму себя нет.")
	}

	// ------------------------------------------------------------------ 7
	show("7) nil ВНУТРИ ИНТЕРФЕЙСА: IsNil И НУЛЕВОЙ VALUE")
	{
		var p *User
		var i any = p
		var e any
		fmt.Printf("  i := any((*User)(nil)); i == nil     -> %v\n", i == nil)
		fmt.Printf("  reflect.ValueOf(i).Kind()            -> %v\n", reflect.ValueOf(i).Kind())
		fmt.Printf("  reflect.ValueOf(i).IsNil()           -> %v\n", reflect.ValueOf(i).IsNil())
		fmt.Printf("  var e any; e == nil                  -> %v\n", e == nil)
		fmt.Printf("  reflect.ValueOf(e).IsValid()         -> %v\n", reflect.ValueOf(e).IsValid())
		fmt.Printf("  reflect.ValueOf(e).IsNil()           -> %s\n", try(func() { reflect.ValueOf(e).IsNil() }))
		fmt.Printf("  reflect.ValueOf(42).IsNil()          -> %s\n", try(func() { reflect.ValueOf(42).IsNil() }))
		fmt.Printf("  isNil(i), isNil(e), isNil(42)        -> %v, %v, %v\n", isNil(i), isNil(e), isNil(42))
		fmt.Println("  == nil у интерфейса проверяет, что пуст и тип, и значение.")
		fmt.Println("  IsNil смотрит на значение внутри — но только у указателя,")
		fmt.Println("  карты, среза, канала, функции и интерфейса; у пустого")
		fmt.Println("  интерфейса Value нулевой, и IsNil на нём паникует.")
	}

	// ------------------------------------------------------------------ 8
	show("8) КОНТЕЙНЕРЫ: СРЕЗ, МАССИВ И КАРТА БЕЗ УКАЗАТЕЛЯ")
	{
		sl := []int{1, 2, 3}
		vs := reflect.ValueOf(sl)
		fmt.Printf("  ValueOf(s)               CanAddr=%-5v CanSet=%v\n", vs.CanAddr(), vs.CanSet())
		fmt.Printf("  ValueOf(s).Index(0)      CanAddr=%-5v CanSet=%v\n", vs.Index(0).CanAddr(), vs.Index(0).CanSet())
		vs.Index(0).SetInt(7)
		fmt.Printf("  ValueOf(s).Index(0).SetInt(7)  -> s = %v\n", sl)
		fmt.Printf("  ValueOf(s).Set(ValueOf([]int{9}))  -> %s\n",
			try(func() { vs.Set(reflect.ValueOf([]int{9})) }))

		arr := [3]int{1, 2, 3}
		va := reflect.ValueOf(arr)
		fmt.Printf("  ValueOf(a).Index(0)      CanAddr=%-5v CanSet=%v   (a — массив [3]int)\n",
			va.Index(0).CanAddr(), va.Index(0).CanSet())

		m := map[string]int{"a": 1}
		vm := reflect.ValueOf(m)
		fmt.Printf("  ValueOf(m)               CanAddr=%-5v CanSet=%v\n", vm.CanAddr(), vm.CanSet())
		vm.SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf(2))
		fmt.Printf("  ValueOf(m).SetMapIndex(\"b\", 2)  -> m = %v\n", m)
		ma := vm.MapIndex(reflect.ValueOf("a"))
		fmt.Printf("  ValueOf(m).MapIndex(\"a\") CanAddr=%-5v CanSet=%v\n", ma.CanAddr(), ma.CanSet())
		fmt.Printf("  ValueOf(m).MapIndex(\"a\").SetInt(5) -> %s\n", try(func() { ma.SetInt(5) }))
		fmt.Println("  Срез — заголовок с указателем на массив: копия заголовка ведёт")
		fmt.Println("  к тому же массиву, и его элементы адресуемы. Заменить сам срез")
		fmt.Println("  (Set) без &s нельзя. Массив копируется целиком — его элементы")
		fmt.Println("  в копии неадресуемы. SetMapIndex не требует CanSet от карты,")
		fmt.Println("  а значение из MapIndex — копия и не меняется.")
	}
}