Перейти к содержимому

Reflect и Unsafe — рефлексия и unsafe.Pointer в Go

Что это: пакеты reflect и unsafe — мощные инструменты для работы с типами в runtime и обхода системы типов соответственно. Используются в библиотеках (encoding/json, fmt, gorm), нужны для глубокого понимания того, как Go работает с памятью. Зачем знать на Middle 1: на собесе спрашивают про “как работает json.Unmarshal”, про performance reflect, про правила unsafe.Pointer. Без понимания — нельзя писать high-performance библиотеки, нельзя читать чужой код, использующий unsafe.


  1. Базовая концепция
  2. Под капотом
  3. Тонкие моменты / Gotchas (12+)
  4. Производительность и compiler optimizations
  5. Когда использовать / когда НЕ использовать
  6. Вопросы на собесе Middle 1 (25)
  7. Practice — задачи (7)
  8. Источники

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()) // User
fmt.Println(t.Kind()) // struct
fmt.Println(t.NumField()) // 2
fmt.Println(t.Field(0).Name) // Name
fmt.Println(t.Field(0).Tag.Get("json")) // name
fmt.Println(v.Field(0).String()) // Alice

unsafe — пакет для обхода системы типов. Позволяет:

  • Конвертировать указатели любых типов через unsafe.Pointer.
  • Узнать размер типа в runtime (Sizeof).
  • Узнать смещения полей (Offsetof).
import "unsafe"
var x int = 42
p := unsafe.Pointer(&x)
f := (*float64)(p) // ← reinterpret int as float64 (опасно!)
fmt.Println(*f) // мусор, но не panic
  • reflect — для encoders/decoders (json, xml, gorm), debug-вывода, DI-фреймворков.
  • unsafe — для performance critical кода (string ↔ []byte без копирования), для линковки с C, для тонкой работы с памятью.

Каждое значение в 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 }
  • Type — конкретный тип (int, string, User, []int, map[string]int).
  • Kind — категория (Int, String, Struct, Slice, Map).
type Celsius float64
var c Celsius = 36.6
t := reflect.TypeOf(c)
fmt.Println(t.Name()) // "Celsius"
fmt.Println(t.Kind()) // "float64" ← базовая kind

Kind — это int8 enum: Invalid, Bool, Int, Int8, …, Float32, Float64, Complex64, Complex128, Array, Chan, Func, Interface, Map, Pointer, Slice, String, Struct, UnsafePointer.

reflect.Value может быть settable, только если он представляет адрес (а не копию).

var x int = 1
v := reflect.ValueOf(x)
v.SetInt(2) // ← panic: reflect: reflect.Value.SetInt using unaddressable value
v = reflect.ValueOf(&x).Elem() // ← Elem() разыменовывает указатель
v.SetInt(2) // ок, теперь settable
fmt.Println(x) // 2

Это потому что reflect.ValueOf(x) создаёт копию x в eface. Чтобы менять оригинал — нужен указатель.

Сравнивает структурно:

  • Скаляры — через ==.
  • Указатели — рекурсивно, если адреса разные.
  • Slices/Arrays — поэлементно.
  • Maps — ключи и значения.
  • Structs — поля.
  • Каналы — по reference.
  • Функции — только nil == nil.

⚠️ Gotchas:

  • NaN ≠ NaN: DeepEqual(math.NaN(), math.NaN()) → false (потому что NaN != NaN).
  • Циклы: DeepEqual обрабатывает циклические ссылки через visited set.
  • Функции: всегда false (кроме обоих nil).
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").

Из документации unsafe.Pointer:

Правило 1: Конверсия *T1 → unsafe.Pointer → *T2 valid, если размеры и выравнивания совместимы.

var x [8]byte
p := 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 += 8
p := 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()).

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) // 0
unsafe.Offsetof(T{}.b) // 4 (после bool + 3 байта padding)
unsafe.Offsetof(T{}.c) // 8

Все три — constant expressions (compile-time). Не runtime.

Структуры (внутреннее представление):

// 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.

//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0) // компилятор не видит зависимости
}

Этот pattern (используется в runtime) “обманывает” escape analysis — компилятор не видит, что p уезжает дальше, и считает, что объект может быть на стеке.

⚠️ Опасный приём, используется только в low-level коде (runtime, sync). Для обычного кода не нужен.

//go:linkname runtimeNanotime runtime.nanotime
func runtimeNanotime() int64

Линкует функцию текущего пакета к функции из другого пакета (даже unexported). Используется в стандартной библиотеке для оптимизации (избежать публичного API).

Это breaking abstraction, может ломаться между версиями Go. Для middle 1 — достаточно знать, что существует.


t := reflect.TypeOf(nil)
fmt.Println(t) // <nil>
fmt.Println(t == nil) // true (потому что reflect.Type — это интерфейс)

reflect.TypeOf принимает interface{}, и если передать nil — получит eface с nil типом. Возвращает nil-reflect.Type.

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 field

Reflect не может менять unexported поля (через публичный API). Можно через unsafe:

v := reflect.ValueOf(&t).Elem()
f := v.Field(0)
ptr := unsafe.Pointer(f.UnsafeAddr())
strPtr := (*string)(ptr)
*strPtr = "hello" // ← работает

Но это нарушает инкапсуляцию.

var x any = 42
v := reflect.ValueOf(x) // v.Kind() == Int
var p any = &MyStruct{}
v := reflect.ValueOf(p) // v.Kind() == Ptr
v.Elem() // → struct value

reflect.ValueOf развёртывает eface. Чтобы узнать “тип интерфейса”, надо явно использовать reflect.TypeOf((*Interface)(nil)).Elem().

fmt.Println(reflect.DeepEqual(math.NaN(), math.NaN())) // false
fmt.Println(reflect.DeepEqual([]float64{math.NaN()}, []float64{math.NaN()})) // false

В IEEE 754 NaN не равен ничему, включая себя. DeepEqual использует ==, поэтому возвращает false.

// 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 раз медленнее. На холодном пути ок, на горячем — никогда.

v := reflect.ValueOf(42)
i := v.Interface() // ← allocation (boxing в any)

Это снова convT2E под капотом.

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.

// Старый код (до 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.

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)) // ← возможно dangling
v := reflect.ValueOf(someFunc)
args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf("hello")}
results := v.Call(args) // []reflect.Value

Каждый аргумент — это reflect.Value (boxing!). Reflect.Call в десятки раз медленнее прямого вызова.

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) = 32

Sizeof — компайл-таймовый, возвращает размер типа, не учитывая динамические данные (slice/map/string).


Operation ns/op Notes
---------------------------------------------------
reflect.TypeOf(x) 5 boxing in eface
reflect.ValueOf(x) 5 boxing
v.Field(i) 10 offset computation
v.SetInt(n) 15 type check + write
v.Interface() 30 unboxing/reboxing
v.Call(args) 500 args boxing, slot setup

Reflect — для cold path (init, config, encoding). На горячем пути — code generation (easyjson, sqlc, ffjson).

  • Reflect блокирует inlining (вызовы через interface).
  • Escape analysis обычно “сдаётся” с reflect — все значения escape to heap.
  • Поэтому код с reflect = много heap allocations + GC pressure.
b := []byte{...}
s := unsafe.String(unsafe.SliceData(b), len(b))
// ↑ ~1-2 ns, без allocation

unsafe.String / unsafe.Slice — это просто reinterpret cast, без копирования. Поэтому unsafe может быть быстрее обычного кода (например, []byte → string без копии).

⚠️ Но: безопасность! Если []byte меняется после конверсии — string тоже меняется (что нарушает immutability).


5. Когда использовать / когда НЕ использовать

Заголовок раздела «5. Когда использовать / когда НЕ использовать»

Использовать:

  • 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.


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:

  1. Конверсия *T1 → unsafe.Pointer → *T2 valid, если размеры/выравнивания совместимы.
  2. Конверсия unsafe.Pointer → uintptr → unsafe.Pointer valid только в одном выражении.
  3. Конверсия unsafe.Pointer → uintptr для syscall — valid.
  4. Конверсия 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 указывающий на 0
v.Elem().SetInt(42)
fmt.Println(v.Elem().Int()) // 42

Q18: Что такое reflect.MakeSlice?

A: Создаёт новый slice заданного типа, длины, capacity:

sliceType := reflect.SliceOf(reflect.TypeOf(0))
s := reflect.MakeSlice(sliceType, 3, 5) // []int{0,0,0} с cap=5

Q19: Как вызвать метод через 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:nosplit
func 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.


Реализовать 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
}

Что выведет?

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 — базовая категория.


Сравните производительность 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).

Реализовать 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 нельзя записывать в результат.


Что выведет?

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))
}

Решение:

24
0
4 (3 байта padding после a)
8
16 (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

Напишите функцию 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+
}
}

Найдите баг:

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 // всё в одном выражении
}

  1. Go SpecPackage unsafehttps://go.dev/ref/spec#Package_unsafe
  2. Go BlogThe Laws of Reflectionhttps://go.dev/blog/laws-of-reflection (Rob Pike).
  3. Go sourcesrc/reflect/, src/unsafe/.
  4. Dave CheneySix things I dislike about Go reflecthttps://dave.cheney.net.
  5. Habr 2024Reflect и unsafe — практическое руководство.
  6. Russ CoxGo Data Structures — пост про память и указатели.
  7. easyjson, sqlc, ffjson — code generation альтернативы reflect.
  8. Go 1.20 release notes — про unsafe.String, unsafe.SliceData (новый API без устаревших StringHeader/SliceHeader).