Обход системы типов Go — конвертация между указателями разных типов и арифметика указателей.
*T | unsafe.Pointer | uintptr | |
|---|---|---|---|
| Что это | Типизированный указатель | Нетипизированный указатель (void* в C) | Целое число (адрес как integer) |
| Типизация | Строгая: *int нельзя кастить в *float64 | Нет типа: можно кастить в любой *T | Не указатель вообще |
| Разыменование | *p — да | Нельзя напрямую, сначала каст в *T | Нельзя |
| Арифметика | Нельзя | Нельзя | Можно (+, -) |
| GC отслеживает | Да — объект не соберётся | Да — объект не соберётся | Нет — для GC это просто число |
| GC двигает объект | Указатель обновится | Указатель обновится | Значение протухнет (dangling) |
unsafe.Pointer — нетипизированный указатель, аналог void* в C. Позволяет кастить между любыми *T.
uintptr — обычное целое число, хранящее адрес. GC не считает его указателем и не обновляет при перемещении объекта.
Цепочка конверсий
*T ↔ unsafe.Pointer ↔ *U // каст между типами
*T ↔ unsafe.Pointer ↔ uintptr // арифметика указателей
Напрямую *int → *float64 — compile error. Через unsafe.Pointer — можно:
var i int64 = 42
f := *(*float64)(unsafe.Pointer(&i)) // реинтерпретация тех же битов как float64
Почему uintptr опасен
GC в Go может перемещать объекты. Если адрес сохранён в uintptr (не в unsafe.Pointer), GC его не обновит, и переменная станет dangling pointer.
// ❌ 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.
p := Point2D{X: 100, Y: 200}
v := *(*Vec2)(unsafe.Pointer(&p)) // ок если layout одинаковый
2. Pointer → uintptr (только для печати)
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 — делает то же самое безопаснее:
p := unsafe.Add(unsafe.Pointer(&s), unsafe.Offsetof(s.age))
4. Syscall аргументы
Конверсия в uintptr допустима только прямо в аргументе вызова. Компилятор специально удерживает объект от GC до конца вызова.
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
5-6. reflect.Value и SliceHeader/StringHeader
reflect.Value.Pointer() возвращает uintptr — конвертировать обратно можно только сразу:
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 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 к структуре без копирования:
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.