Обход системы типов Go — конвертация между указателями разных типов и арифметика указателей.

*Tunsafe.Pointeruintptr
Что этоТипизированный указательНетипизированный указатель (void* в C)Целое число (адрес как integer)
ТипизацияСтрогая: *int нельзя кастить в *float64Нет типа: можно кастить в любой *TНе указатель вообще
Разыменование*p — даНельзя напрямую, сначала каст в *TНельзя
АрифметикаНельзяНельзяМожно (+, -)
GC отслеживаетДа — объект не соберётсяДа — объект не соберётсяНет — для GC это просто число
GC двигает объектУказатель обновитсяУказатель обновитсяЗначение протухнет (dangling)

unsafe.Pointer — нетипизированный указатель, аналог void* в C. Позволяет кастить между любыми *T.

uintptr — обычное целое число, хранящее адрес. GC не считает его указателем и не обновляет при перемещении объекта.

Цепочка конверсий

text
*T ↔ unsafe.Pointer ↔ *U // каст между типами *T ↔ unsafe.Pointer ↔ uintptr // арифметика указателей

Напрямую *int*float64 — compile error. Через unsafe.Pointer — можно:

go
var i int64 = 42 f := *(*float64)(unsafe.Pointer(&i)) // реинтерпретация тех же битов как float64

Почему uintptr опасен

GC в Go может перемещать объекты. Если адрес сохранён в uintptr (не в unsafe.Pointer), GC его не обновит, и переменная станет dangling pointer.

go
// ❌ GC может сработать между строками addr := uintptr(unsafe.Pointer(&x)) // сохранили число // ... GC подвинул x ... p := unsafe.Pointer(addr) // dangling pointer // ✅ Одно выражение — компилятор гарантирует safety p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset)

6 легальных паттернов из документации. Всё остальное — undefined behavior.

1. Каст *T → unsafe.Pointer → *U

Реинтерпретация памяти. Оба типа должны иметь совместимый memory layout.

go
p := Point2D{X: 100, Y: 200} v := *(*Vec2)(unsafe.Pointer(&p)) // ок если layout одинаковый

2. Pointer → uintptr (только для печати)

go
fmt.Printf("addr: %x\n", uintptr(unsafe.Pointer(&x)))\n``` Pointer → uintptr для печати — допустимо. Обратно uintptr → Pointer конвертировать **нельзя** (кроме паттернов 3-5). ## 3. Арифметика — в одном выражении Конверсия uintptr → Pointer **обязана** быть в одном выражении — иначе GC может сдвинуть объект и uintptr протухнет. ```go // ✅ одно выражение p := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.age)) // ❌ сохранил в переменную — dangling u := uintptr(unsafe.Pointer(&s)) p := unsafe.Pointer(u + offset)

С Go 1.17 есть unsafe.Add — делает то же самое безопаснее:

go
p := unsafe.Add(unsafe.Pointer(&s), unsafe.Offsetof(s.age))

4. Syscall аргументы

Конверсия в uintptr допустима только прямо в аргументе вызова. Компилятор специально удерживает объект от GC до конца вызова.

go
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

5-6. reflect.Value и SliceHeader/StringHeader

reflect.Value.Pointer() возвращает uintptr — конвертировать обратно можно только сразу:

go
p := unsafe.Pointer(reflect.ValueOf(&x).Pointer()) // сразу в одном выражении

SliceHeader/StringHeader — deprecated с Go 1.20.

Вместо них: unsafe.String, unsafe.StringData, unsafe.Slice, unsafe.SliceData.

go vet

go vet проверяет паттерны unsafe.Pointer и ловит нарушения правил. Если go vet ругается — код невалиден.


Три compile-time функции для получения информации о layout типов в памяти.

Функции

unsafe.Sizeof(x) — размер типа в байтах. Не учитывает данные за указателем: для слайса вернёт 24 (размер заголовка), а не размер underlying array.

unsafe.Alignof(x) — выравнивание типа в байтах. CPU читает память блоками, адрес поля должен быть кратен этому числу.

unsafe.Offsetof(s.field) — смещение поля от начала структуры в байтах. Работает только для полей структур.

Все три функции вычисляются в compile-time — это не runtime-вызовы.


Когда unsafe реально используется и когда не стоит.

Zero-copy string ↔ byte

Обычная конверсия []byte(s) копирует данные. unsafe — нет:

go
// Go 1.20+ func StringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } func BytesToString(b []byte) string { return unsafe.String(&b[0], len(b)) }

Контракт: нельзя мутировать полученный []byte — строки иммутабельны, нарушишь — undefined behavior.

Где нужно: hot path с высоким RPS, парсинг логов/JSON, сетевые буферы.

Каст byte → struct (бинарные протоколы)

Zero-copy парсинг бинарного протокола — приводим []byte к структуре без копирования:

go
type Header struct { Size uint32 Flags uint16 TypeId uint16 } func Parse(buf []byte) *Header { return (*Header)(unsafe.Pointer(&buf[0])) }

Ограничения: struct должен быть без padding-сюрпризов, данные должны быть в native endian.

Доступ к unexported полям

Через Offsetof можно читать/писать приватные поля чужих структур. Используется в runtime, reflect, тестах. В прикладном коде — red flag.

Когда НЕ использовать

  • Если можно решить без unsafe — решай без unsafe.
  • Профилируй сначала: убедись что копирование реально bottleneck.
  • Layout структур не гарантирован между версиями Go.
  • go vet + fuzz-тесты обязательны для кода с unsafe.