ЗАМЕР
bench/goreflect/practice.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 кругов), а не переносимая константа.
Скрипт
96 строк//go:build ignore
// Две задачи урока про рефлексию: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Четыре места, где рефлексия ведёт себя не так,
// как ждут: Value, полученный из значения, изменить нельзя; через Elem()
// указателя меняется сама переменная, а старый Value держит копию; Kind
// именованного типа над int — int; DeepEqual различает nil-срез и пустой.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз запись поля через
// FieldByName дороже записи через заранее найденный индекс поля. Ответ
// показывает, за что именно платят в рефлексии: за поиск по имени.
//
// ЗАПУСК:
//
// go run bench/goreflect/practice.go
package main
import (
"flag"
"fmt"
"reflect"
"runtime"
"testing"
)
// ---------------------------------------------------------------- задача 1
type Celsius int
// ---------------------------------------------------------------- задача 2
type Config struct {
Host string
User string
Password string
Database string
Timeout int
Retries int
Port int
Debug bool
}
var cfg Config
const rounds = 15
func main() {
fmt.Printf("%s %s/%s | рефлексия: практика\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
fmt.Println()
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
x := 3
v := reflect.ValueOf(x)
fmt.Println(v.CanSet())
p := reflect.ValueOf(&x).Elem()
p.SetInt(10)
fmt.Println(x, v.Int())
fmt.Println(reflect.TypeOf(Celsius(21)).Kind())
fmt.Println(reflect.DeepEqual([]int(nil), []int{}))
fmt.Println()
fmt.Println("ЗАДАЧА 2 — во сколько раз FieldByName дороже Field(idx)")
fmt.Println()
testing.Init()
flag.Set("test.benchtime", "300ms")
sf, _ := reflect.TypeOf(Config{}).FieldByName("Port")
idx := sf.Index[0]
byName, byIndex := 1e18, 1e18
for round := 0; round < rounds; round++ {
r := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
reflect.ValueOf(&cfg).Elem().FieldByName("Port").SetInt(int64(i))
}
})
byName = min(byName, float64(r.T.Nanoseconds())/float64(r.N))
r = testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
reflect.ValueOf(&cfg).Elem().Field(idx).SetInt(int64(i))
}
})
byIndex = min(byIndex, float64(r.T.Nanoseconds())/float64(r.N))
}
fmt.Printf(" FieldByName(\"Port\").SetInt %7.2f нс\n", byName)
fmt.Printf(" Field(idx).SetInt %7.2f нс\n", byIndex)
fmt.Printf(" кратность %7.1f\n", byName/byIndex)
fmt.Println()
fmt.Printf(" Лучший из %d кругов. %s %s/%s, GOMAXPROCS=%d\n",
rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
fmt.Println()
fmt.Println(" Разница — поиск поля по имени: на каждый вызов FieldByName")
fmt.Println(" перебирает поля типа и собирает описание найденного поля.")
fmt.Println(" encoding/json делает это один раз на тип и запоминает результат —")
fmt.Println(" так же стоит делать и в своём коде.")
}