Deep Engineering

MEASUREMENT

bench/goreflect/cost.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/reflection
How to run it
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

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

Замеры для урока «Рефлексия в 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 кругов), а не переносимая константа.

Script

195 lines
//go:build ignore

// Цена рефлексии: присвоить одно поле структуры четырьмя способами.
//
// ЗАЧЕМ ЭТОТ ЗАМЕР. «Рефлексия медленная» — правда, но без числа это не
// ответ. Здесь видно, ИЗ ЧЕГО складывается цена: поиск поля по имени,
// получение reflect.Value и сама запись через него. И видно, что делают
// библиотеки вроде encoding/json, чтобы платить меньше: ищут поле по имени
// один раз на тип и дальше ходят по запомненному индексу.
//
// ЧЕТЫРЕ СПОСОБА:
//
//  1. cfg.Port = n — обычное присваивание, компилятор знает смещение поля.
//  2. ValueOf(&cfg).Elem().FieldByName("Port").SetInt — поиск по имени на
//     каждый вызов.
//  3. ValueOf(&cfg).Elem().Field(idx).SetInt — индекс поля найден заранее.
//  4. field.SetInt — заранее найден и сам reflect.Value поля.
//
// И отдельно — только поиск, без записи: FieldByName("Host") (первое поле,
// одно сравнение имён) против FieldByName("Port") (седьмое, семь сравнений).
// Разница между ними — цена шести лишних сравнений имён; то, что стоит
// первое поле, — постоянная часть: сборка описания поля StructField
// (reflect/type.go:1101) и переход к полю по индексу.
//
// Время — лучший из 15 кругов testing.Benchmark по 300 мс, наносекунды на
// операцию, округлены до сотых. Круги способов чередуются. Аллокации — из
// того же круга.
//
// ЗАПУСК:
//
//	go run bench/goreflect/cost.go
package main

import (
	"flag"
	"fmt"
	"reflect"
	"runtime"
	"strings"
	"testing"
)

type Config struct {
	Host     string
	User     string
	Password string
	Database string
	Timeout  int
	Retries  int
	Port     int
	Debug    bool
}

var cfg Config

// sinkV не даёт компилятору выбросить результат поиска.
var sinkV reflect.Value

const rounds = 15

type result struct {
	ns     float64
	allocs int64
	bytes  int64
}

// measure прогоняет все способы по кругу и для каждого берёт лучший круг.
// Круги чередуются, а не идут подряд для одного способа: на общей машине
// фоновая нагрузка тогда достаётся всем способам поровну, а не одному.
func measure(fs []func(b *testing.B)) []result {
	out := make([]result, len(fs))
	for i := range out {
		out[i].ns = 1e18
	}
	for round := 0; round < rounds; round++ {
		for i, f := range fs {
			br := testing.Benchmark(func(b *testing.B) {
				b.ReportAllocs()
				f(b)
			})
			if ns := float64(br.T.Nanoseconds()) / float64(br.N); ns < out[i].ns {
				out[i] = result{ns: ns, allocs: br.AllocsPerOp(), bytes: br.AllocedBytesPerOp()}
			}
		}
	}
	return out
}

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

func main() {
	fmt.Printf("%s %s/%s | цена рефлексии: запись одного поля\n",
		runtime.Version(), runtime.GOOS, runtime.GOARCH)

	// Короче стандартной секунды на круг: кругов больше, и лучший из них
	// чаще попадает на спокойную машину.
	testing.Init()
	flag.Set("test.benchtime", "300ms")

	sf, _ := reflect.TypeOf(Config{}).FieldByName("Port")
	idx := sf.Index[0]
	field := reflect.ValueOf(&cfg).Elem().Field(idx)

	rs := measure([]func(b *testing.B){
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				cfg.Port = i
			}
		},
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				reflect.ValueOf(&cfg).Elem().FieldByName("Port").SetInt(int64(i))
			}
		},
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				reflect.ValueOf(&cfg).Elem().Field(idx).SetInt(int64(i))
			}
		},
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				field.SetInt(int64(i))
			}
		},
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				sinkV = reflect.ValueOf(&cfg).Elem().FieldByName("Host")
			}
		},
		func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				sinkV = reflect.ValueOf(&cfg).Elem().FieldByName("Port")
			}
		},
	})
	direct, byName, byIndex, cached := rs[0], rs[1], rs[2], rs[3]
	findFirst, findSeventh := rs[4], rs[5]

	show("1) ЗАПИСЬ ПОЛЯ PORT ЧЕТЫРЬМЯ СПОСОБАМИ")
	fmt.Printf("  поле Port — седьмое из восьми, индекс %d\n", idx)
	fmt.Println()
	fmt.Printf("  %-36s %7s %6s %5s\n", "способ", "нс/оп", "аллок", "байт")
	rows := []struct {
		name string
		r    result
	}{
		{"cfg.Port = n", direct},
		{"ValueOf(&cfg).Elem().FieldByName", byName},
		{"ValueOf(&cfg).Elem().Field(idx)", byIndex},
		{"field.SetInt (Value поля запомнен)", cached},
	}
	for _, row := range rows {
		fmt.Printf("  %-36s %7.2f %6d %5d\n",
			row.name, row.r.ns, row.r.allocs, row.r.bytes)
	}
	fmt.Println()
	fmt.Printf("  FieldByName дороже Field(idx)          в %4.1f раза\n", byName.ns/byIndex.ns)
	fmt.Printf("  Field(idx) дороже запомненного Value   в %4.1f раза\n", byIndex.ns/cached.ns)
	fmt.Println("  Прямое присваивание — одна запись в память; кратность к нему")
	fmt.Println("  не печатается: делитель меньше наносекунды и сам по себе шум.")
	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("  Индекс, найденный один раз, убирает поиск; запомненный Value")
	fmt.Println("  поля убирает и ValueOf с Elem. Остаётся проверка флагов и Kind")
	fmt.Println("  в SetInt.")

	show("2) ПОИСК ПО ИМЕНИ: ПЕРВОЕ ПОЛЕ ПРОТИВ СЕДЬМОГО")
	fmt.Printf("  %-36s %7s %6s %5s\n", "только поиск, без записи", "нс/оп", "аллок", "байт")
	for _, row := range []struct {
		name string
		r    result
	}{
		{"FieldByName(\"Host\") — 1-е поле", findFirst},
		{"FieldByName(\"Port\") — 7-е поле", findSeventh},
	} {
		fmt.Printf("  %-36s %7.2f %6d %5d\n",
			row.name, row.r.ns, row.r.allocs, row.r.bytes)
	}
	fmt.Println()
	fmt.Printf("  поиск 1-го поля / поиск 7-го поля     %5.2f\n", findFirst.ns/findSeventh.ns)
	fmt.Println()
	fmt.Println("  У первого поля одно сравнение имён, у седьмого — семь, а цена")
	fmt.Println("  различается далеко не в семь раз. Большая её часть постоянна:")
	fmt.Println("  это то, что делается после того, как поле найдено, — сборка")
	fmt.Println("  описания StructField и переход к полю по индексу.")
}