ЗАМЕР
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 — копия и не меняется.")
}
}