Reflect и Unsafe — рефлексия и unsafe.Pointer в Go
Что это: пакеты
reflectиunsafe— мощные инструменты для работы с типами в runtime и обхода системы типов соответственно. Используются в библиотеках (encoding/json, fmt, gorm), нужны для глубокого понимания того, как Go работает с памятью. Зачем знать на Middle 1: на собесе спрашивают про “как работает json.Unmarshal”, про performance reflect, про правила unsafe.Pointer. Без понимания — нельзя писать high-performance библиотеки, нельзя читать чужой код, использующий unsafe.
Содержание (TOC)
Заголовок раздела «Содержание (TOC)»- Базовая концепция
- Под капотом
- Тонкие моменты / Gotchas (12+)
- Производительность и compiler optimizations
- Когда использовать / когда НЕ использовать
- Вопросы на собесе Middle 1 (25)
- Practice — задачи (7)
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»1.1 Reflect
Заголовок раздела «1.1 Reflect»reflect — пакет для runtime интроспекции типов и значений. Позволяет:
- Узнать тип любой переменной.
- Прочитать/изменить поля структуры.
- Вызвать метод по имени.
- Создать значение динамически.
Два основных типа:
reflect.Type— описание типа (имя, kind, поля, методы).reflect.Value— описание значения (можно читать, иногда менять).
import "reflect"
type User struct { Name string `json:"name"` Age int `json:"age"`}
u := User{Name: "Alice", Age: 30}t := reflect.TypeOf(u)v := reflect.ValueOf(u)
fmt.Println(t.Name()) // Userfmt.Println(t.Kind()) // structfmt.Println(t.NumField()) // 2fmt.Println(t.Field(0).Name) // Namefmt.Println(t.Field(0).Tag.Get("json")) // namefmt.Println(v.Field(0).String()) // Alice1.2 Unsafe
Заголовок раздела «1.2 Unsafe»unsafe — пакет для обхода системы типов. Позволяет:
- Конвертировать указатели любых типов через
unsafe.Pointer. - Узнать размер типа в runtime (
Sizeof). - Узнать смещения полей (
Offsetof).
import "unsafe"
var x int = 42p := unsafe.Pointer(&x)f := (*float64)(p) // ← reinterpret int as float64 (опасно!)fmt.Println(*f) // мусор, но не panic1.3 Когда нужны
Заголовок раздела «1.3 Когда нужны»reflect— для encoders/decoders (json, xml, gorm), debug-вывода, DI-фреймворков.unsafe— для performance critical кода (string ↔ []byte без копирования), для линковки с C, для тонкой работы с памятью.
2. Под капотом
Заголовок раздела «2. Под капотом»2.1 Reflect: связь с интерфейсами
Заголовок раздела «2.1 Reflect: связь с интерфейсами»Каждое значение в Go, передаваемое в reflect.ValueOf(x), упаковывается в interface{} (eface). Дальше reflect.Value хранит:
*_type(из eface) — тип.unsafe.Pointer— указатель на данные.flag— биты (направление, settable, и т.д.).
// Упрощённая структура reflect.Value (из src/reflect/value.go):type Value struct { typ_ *abi.Type ptr unsafe.Pointer flag uintptr // младшие биты: kind, флаги}Поэтому reflect.TypeOf(42) сначала упаковывает 42 в any (allocation!), потом достаёт *_type из eface.
x int = 42 │ ├─ упаковка в eface { _type: int_type, data: -> heap[42] } ← alloc │ reflect.ValueOf(x) │ Value { typ: int_type, ptr: heap[42], flag: kindInt }2.2 Kind vs Type
Заголовок раздела «2.2 Kind vs Type»- Type — конкретный тип (
int,string,User,[]int,map[string]int). - Kind — категория (Int, String, Struct, Slice, Map).
type Celsius float64var c Celsius = 36.6
t := reflect.TypeOf(c)fmt.Println(t.Name()) // "Celsius"fmt.Println(t.Kind()) // "float64" ← базовая kindKind — это int8 enum: Invalid, Bool, Int, Int8, …, Float32, Float64, Complex64, Complex128, Array, Chan, Func, Interface, Map, Pointer, Slice, String, Struct, UnsafePointer.
2.3 Settable values
Заголовок раздела «2.3 Settable values»reflect.Value может быть settable, только если он представляет адрес (а не копию).
var x int = 1v := reflect.ValueOf(x)v.SetInt(2) // ← panic: reflect: reflect.Value.SetInt using unaddressable value
v = reflect.ValueOf(&x).Elem() // ← Elem() разыменовывает указательv.SetInt(2) // ок, теперь settablefmt.Println(x) // 2Это потому что reflect.ValueOf(x) создаёт копию x в eface. Чтобы менять оригинал — нужен указатель.
2.4 reflect.DeepEqual
Заголовок раздела «2.4 reflect.DeepEqual»Сравнивает структурно:
- Скаляры — через ==.
- Указатели — рекурсивно, если адреса разные.
- Slices/Arrays — поэлементно.
- Maps — ключи и значения.
- Structs — поля.
- Каналы — по reference.
- Функции — только nil == nil.
⚠️ Gotchas:
- NaN ≠ NaN:
DeepEqual(math.NaN(), math.NaN())→ false (потому чтоNaN != NaN). - Циклы:
DeepEqualобрабатывает циклические ссылки через visited set. - Функции: всегда false (кроме обоих nil).
2.5 Структура reflect.StructField
Заголовок раздела «2.5 Структура reflect.StructField»type StructField struct { Name string PkgPath string // не пустой только для unexported Type Type Tag StructTag Offset uintptr // смещение в байтах Index []int // индекс для FieldByIndex Anonymous bool}Tag — это reflect.StructTag, парсится через Tag.Get("json").
2.6 Unsafe.Pointer rules (4 правила из Go spec)
Заголовок раздела «2.6 Unsafe.Pointer rules (4 правила из Go spec)»Из документации unsafe.Pointer:
Правило 1: Конверсия *T1 → unsafe.Pointer → *T2 valid, если размеры и выравнивания совместимы.
var x [8]bytep := unsafe.Pointer(&x)i := (*int64)(p) // ok, sizeof[8]byte == sizeof(int64) == 8, align ok*i = 42Правило 2: Конверсия unsafe.Pointer → uintptr → unsafe.Pointer valid только в одном выражении (без присваивания uintptr в переменную).
// OK:p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 8)
// НЕ OK:addr := uintptr(unsafe.Pointer(&x)) // ← GC может переместить x, addr станет невалиднымaddr += 8p := unsafe.Pointer(addr) // ← dangling pointerПричина: GC может перемещать объекты (на самом деле stack-allocated, но в общем случае — да). uintptr для GC — обычное число, не отслеживается. Между шагами GC может произойти, и адрес станет невалидным.
Правило 3: Конверсия unsafe.Pointer → uintptr valid для syscall (syscall.Syscall(..., uintptr(ptr), ...)).
Правило 4: Конверсия результата reflect.Value.Pointer() или UnsafePointer() в unsafe.Pointer — valid (но reflect.Pointer() возвращает uintptr — устарел, теперь UnsafePointer()).
2.7 Sizeof, Alignof, Offsetof
Заголовок раздела «2.7 Sizeof, Alignof, Offsetof»type T struct { a bool // 1 byte b int32 // 4 bytes (выравнивание!) c int64 // 8 bytes}
unsafe.Sizeof(T{}) // 16 (с padding)unsafe.Alignof(T{}) // 8 (по int64)unsafe.Offsetof(T{}.a) // 0unsafe.Offsetof(T{}.b) // 4 (после bool + 3 байта padding)unsafe.Offsetof(T{}.c) // 8Все три — constant expressions (compile-time). Не runtime.
2.8 string ↔ []byte без копирования
Заголовок раздела «2.8 string ↔ []byte без копирования»Структуры (внутреннее представление):
// string:type stringHeader struct { Data unsafe.Pointer Len int}
// slice:type sliceHeader struct { Data unsafe.Pointer Len int Cap int}В Go 1.20+ — официальный API:
import "unsafe"
func BytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b))}
func StringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s))}⚠️ Опасно: получившийся []byte нельзя мутировать (строки immutable в Go). Получившаяся string нельзя использовать после изменения underlying []byte.
2.9 noescape pattern
Заголовок раздела «2.9 noescape pattern»//go:nosplitfunc noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) // компилятор не видит зависимости}Этот pattern (используется в runtime) “обманывает” escape analysis — компилятор не видит, что p уезжает дальше, и считает, что объект может быть на стеке.
⚠️ Опасный приём, используется только в low-level коде (runtime, sync). Для обычного кода не нужен.
2.10 linkname
Заголовок раздела «2.10 linkname»//go:linkname runtimeNanotime runtime.nanotimefunc runtimeNanotime() int64Линкует функцию текущего пакета к функции из другого пакета (даже unexported). Используется в стандартной библиотеке для оптимизации (избежать публичного API).
Это breaking abstraction, может ломаться между версиями Go. Для middle 1 — достаточно знать, что существует.
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»Gotcha 1: reflect.TypeOf(nil) — panic? Нет.
Заголовок раздела «Gotcha 1: reflect.TypeOf(nil) — panic? Нет.»t := reflect.TypeOf(nil)fmt.Println(t) // <nil>fmt.Println(t == nil) // true (потому что reflect.Type — это интерфейс)reflect.TypeOf принимает interface{}, и если передать nil — получит eface с nil типом. Возвращает nil-reflect.Type.
Gotcha 2: reflect.Value.Set на unexported поле
Заголовок раздела «Gotcha 2: reflect.Value.Set на unexported поле»type T struct { name string // unexported}
t := T{}v := reflect.ValueOf(&t).Elem()v.Field(0).SetString("hello") // ← panic: reflect.Value.SetString using value obtained using unexported fieldReflect не может менять unexported поля (через публичный API). Можно через unsafe:
v := reflect.ValueOf(&t).Elem()f := v.Field(0)ptr := unsafe.Pointer(f.UnsafeAddr())strPtr := (*string)(ptr)*strPtr = "hello" // ← работаетНо это нарушает инкапсуляцию.
Gotcha 3: reflect и интерфейсы
Заголовок раздела «Gotcha 3: reflect и интерфейсы»var x any = 42v := reflect.ValueOf(x) // v.Kind() == Int
var p any = &MyStruct{}v := reflect.ValueOf(p) // v.Kind() == Ptrv.Elem() // → struct valuereflect.ValueOf развёртывает eface. Чтобы узнать “тип интерфейса”, надо явно использовать reflect.TypeOf((*Interface)(nil)).Elem().
Gotcha 4: reflect.DeepEqual и NaN
Заголовок раздела «Gotcha 4: reflect.DeepEqual и NaN»fmt.Println(reflect.DeepEqual(math.NaN(), math.NaN())) // falsefmt.Println(reflect.DeepEqual([]float64{math.NaN()}, []float64{math.NaN()})) // falseВ IEEE 754 NaN не равен ничему, включая себя. DeepEqual использует ==, поэтому возвращает false.
Gotcha 5: reflect — медленнее в 100x
Заголовок раздела «Gotcha 5: reflect — медленнее в 100x»// Without reflect:type User struct { Name string }u := User{}u.Name = "Alice"
// With reflect:t := reflect.TypeOf(User{})v := reflect.New(t).Elem()v.FieldByName("Name").SetString("Alice")Reflect версия — в 50-100 раз медленнее. На холодном пути ок, на горячем — никогда.
Gotcha 6: reflect.Value.Interface() аллоцирует
Заголовок раздела «Gotcha 6: reflect.Value.Interface() аллоцирует»v := reflect.ValueOf(42)i := v.Interface() // ← allocation (boxing в any)Это снова convT2E под капотом.
Gotcha 7: unsafe.Pointer и stack/heap
Заголовок раздела «Gotcha 7: unsafe.Pointer и stack/heap»func bad() *int { x := 42 p := unsafe.Pointer(&x) return (*int)(p) // ← x escape to heap (через unsafe тоже)}Escape analysis отслеживает unsafe.Pointer и обычно правильно определяет, что объект escape. Но в сложных случаях может ошибиться → use after free на stack.
Gotcha 8: stringHeader и sliceHeader устарели
Заголовок раздела «Gotcha 8: stringHeader и sliceHeader устарели»// Старый код (до Go 1.20):import "reflect"
s := "hello"sh := (*reflect.StringHeader)(unsafe.Pointer(&s))data := sh.Data // uintptr — небезопасноreflect.StringHeader и reflect.SliceHeader deprecated в Go 1.20+. Использовать unsafe.StringData, unsafe.SliceData, unsafe.String, unsafe.Slice.
Gotcha 9: unsafe.Pointer и struct field
Заголовок раздела «Gotcha 9: unsafe.Pointer и struct field»type T struct { a int b int}
t := &T{}b := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(t)) + unsafe.Offsetof(t.b)))*b = 42 // правильноНо в одном выражении! Если разбить — GC может переместить t:
addr := uintptr(unsafe.Pointer(t))// ... GC может произойти здесьb := (*int)(unsafe.Pointer(addr + 8)) // ← возможно danglingGotcha 10: reflect.Value.Call
Заголовок раздела «Gotcha 10: reflect.Value.Call»v := reflect.ValueOf(someFunc)args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf("hello")}results := v.Call(args) // []reflect.ValueКаждый аргумент — это reflect.Value (boxing!). Reflect.Call в десятки раз медленнее прямого вызова.
Gotcha 11: проверка type assertion через reflect
Заголовок раздела «Gotcha 11: проверка type assertion через reflect»v := reflect.ValueOf(x)if v.Type().Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { // x implements io.Reader}Сложный синтаксис: reflect.TypeOf((*io.Reader)(nil)).Elem() — извлекает тип интерфейса (потому что nil ptr нельзя передать в TypeOf напрямую).
Gotcha 12: unsafe.Sizeof возвращает size типа, не значения
Заголовок раздела «Gotcha 12: unsafe.Sizeof возвращает size типа, не значения»type T struct{ a int; b []int }s := unsafe.Sizeof(T{a: 1, b: []int{1, 2, 3}})fmt.Println(s) // не 1+24 = 32, а fixed-size header T: 8 (int) + 24 (slice header) = 32Sizeof — компайл-таймовый, возвращает размер типа, не учитывая динамические данные (slice/map/string).
4. Производительность и compiler optimizations
Заголовок раздела «4. Производительность и compiler optimizations»4.1 Стоимость операций reflect
Заголовок раздела «4.1 Стоимость операций reflect»Operation ns/op Notes---------------------------------------------------reflect.TypeOf(x) 5 boxing in efacereflect.ValueOf(x) 5 boxingv.Field(i) 10 offset computationv.SetInt(n) 15 type check + writev.Interface() 30 unboxing/reboxingv.Call(args) 500 args boxing, slot setupReflect — для cold path (init, config, encoding). На горячем пути — code generation (easyjson, sqlc, ffjson).
4.2 reflect и compiler
Заголовок раздела «4.2 reflect и compiler»- Reflect блокирует inlining (вызовы через interface).
- Escape analysis обычно “сдаётся” с reflect — все значения escape to heap.
- Поэтому код с reflect = много heap allocations + GC pressure.
4.3 unsafe — обычно zero cost
Заголовок раздела «4.3 unsafe — обычно zero cost»b := []byte{...}s := unsafe.String(unsafe.SliceData(b), len(b))// ↑ ~1-2 ns, без allocationunsafe.String / unsafe.Slice — это просто reinterpret cast, без копирования. Поэтому unsafe может быть быстрее обычного кода (например, []byte → string без копии).
⚠️ Но: безопасность! Если []byte меняется после конверсии — string тоже меняется (что нарушает immutability).
5. Когда использовать / когда НЕ использовать
Заголовок раздела «5. Когда использовать / когда НЕ использовать»Reflect
Заголовок раздела «Reflect»Использовать:
- Encoders/decoders общего назначения (json, xml).
- ORM (read tags, scan rows).
- DI-фреймворки.
- Debug-инструменты (pretty print).
НЕ использовать:
- На горячем пути (>10^5 операций/сек).
- Когда можно код-генерация (easyjson, sqlc).
- Когда можно дженерики (Go 1.18+).
Использовать:
- Performance critical конверсии (string ↔ []byte).
- Линковка с C (cgo, syscall).
- Низкоуровневые библиотеки (runtime, sync).
НЕ использовать:
- В обычном бизнес-коде.
- “Just because I can” — высокий риск bug.
- В библиотеках, которые шарятся с командой.
Правило: если ты не можешь объяснить ВСЕ 4 правила unsafe.Pointer — не пользуйся unsafe.
6. Вопросы на собесе Middle 1
Заголовок раздела «6. Вопросы на собесе Middle 1»Q1: Что такое reflect?
A: Пакет стандартной библиотеки для runtime интроспекции типов. Позволяет узнать тип любого значения, прочитать/изменить поля, вызвать методы по имени. Используется в encoding/json, fmt, gorm.
Q2: Чем отличается reflect.Type от reflect.Value?
A:
reflect.Type— описание типа (имя, kind, поля, методы). Без значения.reflect.Value— описание значения (тип + указатель на данные). Можно читать/менять.
Q3: Что такое Kind?
A: Категория типа (Int, Slice, Map, Struct, и т.д.). В отличие от Type (конкретный тип), Kind — базовая категория. Например, для type Celsius float64 Kind = Float64, Type = “Celsius”.
Q4: Когда reflect.Value.Set работает?
A: Только когда Value представляет settable значение — это либо разыменованный указатель (reflect.ValueOf(&x).Elem()), либо поле такой структуры. Прямой reflect.ValueOf(x) возвращает копию, не settable.
Q5: Что такое struct tag?
A: Строка, прикреплённая к полю структуры (json:"name"), доступная через reflect:
field.Tag.Get("json") // → "name"Используется библиотеками для маппинга (json, gorm, validate).
Q6: Как работает json.Unmarshal под капотом?
A: Использует reflect: получает тип целевой структуры, читает теги полей, парсит JSON по байтам, для каждого ключа находит соответствующее поле через FieldByName, вызывает Set с распарсенным значением. Из-за reflect — медленно (можно ускорить через code generation типа easyjson).
Q7: Почему reflect медленный?
A:
- Каждое значение упаковывается в
interface{}(boxing). - Type checks на каждой операции.
- Inlining невозможен.
- Часто allocations (Value.Interface(), Call args). В сумме — 10-100x медленнее статического кода.
Q8: Что такое reflect.DeepEqual и его подвохи?
A: Рекурсивное сравнение значений. Подвохи:
- NaN ≠ NaN (поэтому
[]float64{NaN}неравна себе). - Функции — всегда не равны (кроме обоих nil).
- Циклические ссылки обрабатываются.
- Map/slice — поэлементно.
Q9: Что такое unsafe.Pointer?
A: Тип, представляющий untyped pointer. Можно конвертировать в unsafe.Pointer любой *T, и обратно в любой *U. Обходит систему типов Go.
Q10: Какие 4 правила unsafe.Pointer?
A:
- Конверсия
*T1 → unsafe.Pointer → *T2valid, если размеры/выравнивания совместимы. - Конверсия
unsafe.Pointer → uintptr → unsafe.Pointervalid только в одном выражении. - Конверсия
unsafe.Pointer → uintptrдля syscall — valid. - Конверсия
reflect.Value.UnsafePointer()вunsafe.Pointer— valid.
Q11: Почему нельзя сохранять uintptr в переменную для адресной арифметики?
A: uintptr не отслеживается GC. Если объект на heap, GC может его переместить (с компактификацией), и сохранённый uintptr станет dangling. Все операции с адресом должны быть в одном выражении, где компилятор может удержать оригинальный pointer.
Q12: Что такое unsafe.Sizeof?
A: Compile-time выражение, возвращающее размер типа в байтах. Включает padding структур. Для slice/map/string возвращает размер header (не данных).
Q13: Что такое unsafe.Offsetof?
A: Compile-time выражение, возвращающее смещение поля внутри структуры (в байтах). Учитывает padding.
type T struct{ a bool; b int }unsafe.Offsetof(T{}.b) // 8 (после bool + padding)Q14: Как конвертировать []byte в string без копирования?
A:
import "unsafe"s := unsafe.String(unsafe.SliceData(b), len(b))⚠️ Опасно: если b модифицируется после — s тоже изменится (нарушит immutability строк).
Q15: Какая разница между reflect и unsafe?
A:
- Reflect — runtime интроспекция, типобезопасная (panic при ошибках), медленная.
- Unsafe — обход системы типов, быстрая, опасная (undefined behavior при ошибках).
Q16: Можно ли изменить unexported поле через reflect?
A: Через публичный API — нет (SetString panic). Через unsafe.Pointer(field.UnsafeAddr()) — да, но это нарушает инкапсуляцию.
Q17: Что такое reflect.New?
A: Создаёт новое значение заданного типа, возвращая reflect.Value, указывающее на него.
v := reflect.New(reflect.TypeOf(0)) // *int указывающий на 0v.Elem().SetInt(42)fmt.Println(v.Elem().Int()) // 42Q18: Что такое reflect.MakeSlice?
A: Создаёт новый slice заданного типа, длины, capacity:
sliceType := reflect.SliceOf(reflect.TypeOf(0))s := reflect.MakeSlice(sliceType, 3, 5) // []int{0,0,0} с cap=5Q19: Как вызвать метод через reflect?
A:
v := reflect.ValueOf(obj)m := v.MethodByName("Foo")args := []reflect.Value{reflect.ValueOf(42)}results := m.Call(args)Каждый аргумент — boxing в reflect.Value (медленно).
Q20: Что такое noescape pattern?
A: Хитрость, обманывающая escape analysis. Используется в runtime/sync:
//go:nosplitfunc noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0)}Компилятор не отслеживает зависимость через XOR — объект остаётся на стеке. Для обычного кода — не нужен.
Q21: Что такое go:linkname?
A: Директива компилятора, связывающая функцию текущего пакета с функцией другого пакета (даже unexported). Используется в стандартной библиотеке для оптимизации без публичного API. Хрупкое, ломается между версиями Go.
Q22: Чем заменить reflect для производительности?
A: Code generation:
easyjson— для JSON (вместоencoding/json).sqlc— для SQL → Go types.protobuf— для protobuf → Go types. Генерируют статический код, без reflect.
Q23: Что такое reflect.Value.CanAddr?
A: Возвращает true, если Value представляет адрес (а не копию). Settable values всегда CanAddr.
Q24: Какой тип возвращает unsafe.Sizeof?
A: uintptr (compile-time константа). Не runtime, поэтому можно использовать в array size.
Q25: Можно ли через unsafe написать “C-style” union?
A: Технически да:
type Union struct { data [8]byte}func (u *Union) AsInt() *int64 { return (*int64)(unsafe.Pointer(&u.data)) }func (u *Union) AsFloat() *float64 { return (*float64)(unsafe.Pointer(&u.data)) }Но идиоматично в Go — interface или sum type через discriminator.
7. Practice — задачи
Заголовок раздела «7. Practice — задачи»Задача 1
Заголовок раздела «Задача 1»Реализовать Copy(dst, src any) через reflect — копировать поля одного struct в другой по имени.
Решение:
import "reflect"
func Copy(dst, src any) error { dstV := reflect.ValueOf(dst) if dstV.Kind() != reflect.Ptr || dstV.IsNil() { return errors.New("dst must be non-nil pointer") } dstV = dstV.Elem() if dstV.Kind() != reflect.Struct { return errors.New("dst must point to struct") }
srcV := reflect.ValueOf(src) if srcV.Kind() == reflect.Ptr { srcV = srcV.Elem() } if srcV.Kind() != reflect.Struct { return errors.New("src must be struct or pointer to struct") }
for i := 0; i < srcV.NumField(); i++ { fieldName := srcV.Type().Field(i).Name srcField := srcV.Field(i) dstField := dstV.FieldByName(fieldName) if dstField.IsValid() && dstField.CanSet() && dstField.Type() == srcField.Type() { dstField.Set(srcField) } } return nil}Задача 2
Заголовок раздела «Задача 2»Что выведет?
package main
import ( "fmt" "reflect")
type Celsius float64
func main() { var c Celsius = 36.6 t := reflect.TypeOf(c) fmt.Println(t.Name(), t.Kind())}Решение:
Celsius float64. Name — конкретное имя типа, Kind — базовая категория.
Задача 3
Заголовок раздела «Задача 3»Сравните производительность reflect vs static.
Решение:
type User struct{ Name string }
func StaticAccess(u *User) string { return u.Name}
func ReflectAccess(u *User) string { v := reflect.ValueOf(u).Elem() return v.FieldByName("Name").String()}
func BenchmarkStatic(b *testing.B) { u := &User{Name: "Alice"} for i := 0; i < b.N; i++ { _ = StaticAccess(u) }}
func BenchmarkReflect(b *testing.B) { u := &User{Name: "Alice"} for i := 0; i < b.N; i++ { _ = ReflectAccess(u) }}Ожидаемый результат:
- Static: ~0.5 ns/op.
- Reflect: ~50-100 ns/op (включая boxing).
Задача 4
Заголовок раздела «Задача 4»Реализовать BytesToString и StringToBytes без копирования, используя unsafe.
Решение:
import "unsafe"
func BytesToString(b []byte) string { if len(b) == 0 { return "" } return unsafe.String(unsafe.SliceData(b), len(b))}
func StringToBytes(s string) []byte { if len(s) == 0 { return nil } return unsafe.Slice(unsafe.StringData(s), len(s))}⚠️ Использовать осторожно: после BytesToString нельзя менять b. После StringToBytes нельзя записывать в результат.
Задача 5
Заголовок раздела «Задача 5»Что выведет?
package main
import ( "fmt" "unsafe")
type T struct { a bool b int32 c bool d int64}
func main() { var t T fmt.Println(unsafe.Sizeof(t)) fmt.Println(unsafe.Offsetof(t.a)) fmt.Println(unsafe.Offsetof(t.b)) fmt.Println(unsafe.Offsetof(t.c)) fmt.Println(unsafe.Offsetof(t.d))}Решение:
2404 (3 байта padding после a)816 (7 байт padding после c)Из-за выравнивания:
a(1) + padding (3) → 4 байта.b(4) → 8 байт.c(1) + padding (7) → 16 байт.d(8) → 24 байта.
Можно оптимизировать порядок:
type T2 struct { d int64 // 8 b int32 // 4 a bool // 1 c bool // 1 (+ 2 байта padding = 16 total)}// unsafe.Sizeof(T2{}) == 16Задача 6
Заголовок раздела «Задача 6»Напишите функцию IsEmpty(v any) bool, которая через reflect проверяет, что значение — zero value.
Решение:
func IsEmpty(v any) bool { if v == nil { return true } rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Ptr, reflect.Interface: return rv.IsNil() case reflect.Slice, reflect.Map, reflect.Array, reflect.String: return rv.Len() == 0 case reflect.Chan, reflect.Func: return rv.IsNil() case reflect.Struct: // Сравним с zero value того же типа zero := reflect.Zero(rv.Type()) return reflect.DeepEqual(rv.Interface(), zero.Interface()) default: // Скалярные типы return rv.IsZero() // Go 1.13+ }}Задача 7
Заголовок раздела «Задача 7»Найдите баг:
type Counter struct { val int}
func (c *Counter) Add(n int) { addr := uintptr(unsafe.Pointer(&c.val)) runtime.GC() // искусственно valPtr := (*int)(unsafe.Pointer(addr)) *valPtr += n}Решение:
Нарушение правила 2 unsafe.Pointer. После addr := uintptr(...) — может произойти GC (вызван явно или нет), который переместит c.val (если c в heap). addr указывает на старое место, *valPtr += n пишет по неверному адресу — UB.
Исправление:
func (c *Counter) Add(n int) { c.val += n // зачем unsafe вообще? :)}Если уж очень нужно unsafe (в реальной задаче — не нужно):
func (c *Counter) Add(n int) { *(*int)(unsafe.Pointer(&c.val)) += n // всё в одном выражении}8. Источники
Заголовок раздела «8. Источники»- Go Spec — Package unsafe — https://go.dev/ref/spec#Package_unsafe
- Go Blog — The Laws of Reflection — https://go.dev/blog/laws-of-reflection (Rob Pike).
- Go source —
src/reflect/,src/unsafe/. - Dave Cheney — Six things I dislike about Go reflect — https://dave.cheney.net.
- Habr 2024 — Reflect и unsafe — практическое руководство.
- Russ Cox — Go Data Structures — пост про память и указатели.
- easyjson, sqlc, ffjson — code generation альтернативы reflect.
- Go 1.20 release notes — про
unsafe.String,unsafe.SliceData(новый API без устаревших StringHeader/SliceHeader).