Пакет unsafe -- это "аварийный выход" из системы типов Go. Он позволяет обходить type safety для низкоуровневых оптимизаций и взаимодействия с C-кодом. Название пакета -- предупреждение: код с unsafe может сломаться при обновлении компилятора.
unsafe.Pointer -- обход системы типов
unsafe.Pointer -- это Go-аналог void* в C. Он может быть конвертирован из/в любой тип указателя:
import "unsafe"
// Normal Go: cannot convert between unrelated pointer types
var x int64 = 42
// p := (*float64)(unsafe.Pointer(&x)) -- converts *int64 to *float64
// Reading int64 as float64 (reinterpret cast)
var n int64 = 0x4059000000000000 // IEEE 754 representation of 100.0
f := *(*float64)(unsafe.Pointer(&n))
fmt.Println(f) // 100
Ключевое свойство: unsafe.Pointer -- это единственный тип, из которого можно конвертировать в uintptr и обратно, и из/в любой *T.
unsafe.Sizeof, unsafe.Alignof, unsafe.Offsetof
type Example struct {
a bool // 1 byte
b int64 // 8 bytes
c bool // 1 byte
d int32 // 4 bytes
}
// Sizeof -- size of the type in bytes (including padding)
fmt.Println(unsafe.Sizeof(Example{})) // 24 (not 14! due to alignment padding)
// Alignof -- required alignment of the type
fmt.Println(unsafe.Alignof(Example{}.a)) // 1
fmt.Println(unsafe.Alignof(Example{}.b)) // 8
fmt.Println(unsafe.Alignof(Example{}.d)) // 4
// Offsetof -- offset of a field within a struct
fmt.Println(unsafe.Offsetof(Example{}.a)) // 0
fmt.Println(unsafe.Offsetof(Example{}.b)) // 8 (7 bytes padding after a!)
fmt.Println(unsafe.Offsetof(Example{}.c)) // 16
fmt.Println(unsafe.Offsetof(Example{}.d)) // 20
Оптимизация расположения полей
Порядок полей влияет на размер структуры из-за выравнивания:
// Bad layout: 24 bytes (with padding)
type Bad struct {
a bool // 1 + 7 padding
b int64 // 8
c bool // 1 + 3 padding
d int32 // 4
}
// Good layout: 16 bytes (fields sorted by alignment)
type Good struct {
b int64 // 8
d int32 // 4
a bool // 1
c bool // 1 + 2 padding
}
fmt.Println(unsafe.Sizeof(Bad{})) // 24
fmt.Println(unsafe.Sizeof(Good{})) // 16 -- saved 33%!
Правило: Сортируйте поля от наибольшего к наименьшему размеру для минимизации padding.
Арифметика указателей
В Go нельзя делать арифметику с указателями напрямую, но можно через uintptr:
// Access array elements via pointer arithmetic
arr := [5]int{10, 20, 30, 40, 50}
// Get pointer to first element
base := unsafe.Pointer(&arr[0])
// Access third element (index 2)
elementSize := unsafe.Sizeof(arr[0])
third := (*int)(unsafe.Pointer(uintptr(base) + 2*elementSize))
fmt.Println(*third) // 30
// This is equivalent to arr[2], but UNSAFE
Критически важно: Конвертация
unsafe.Pointer->uintptr->unsafe.Pointerдолжна происходить в одном выражении! Если разбить на отдельные строки, GC может переместить объект между конвертациями.
// WRONG: GC can move the object between these lines!
p := unsafe.Pointer(&x)
addr := uintptr(p)
// GC runs here, moves x to different address
// addr is now stale!
p2 := unsafe.Pointer(addr) // DANGLING POINTER!
// CORRECT: single expression
p2 := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset)
Конвертация между типами указателей
// Convert *T1 to *T2 (when you know the memory layout is compatible)
type Vec2 struct {
X, Y float64
}
type Complex struct {
Real, Imag float64
}
v := Vec2{X: 3, Y: 4}
c := (*Complex)(unsafe.Pointer(&v))
fmt.Printf("%.1f + %.1fi\n", c.Real, c.Imag) // 3.0 + 4.0i
String/[]byte zero-copy конвертация
Стандартная конвертация string(b) и []byte(s) копирует данные. С unsafe можно избежать копирования:
// Zero-copy string -> []byte (Go 1.17+ with unsafe.Slice)
func stringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// Zero-copy []byte -> string (Go 1.20+ with unsafe.String)
func bytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
Опасно: Модификация
[]byte, полученного изstringToBytes, -- это undefined behavior! Строки в Go иммутабельны, и компилятор может разделять память между строками.
// Safe usage: only read from the result
s := "hello"
b := stringToBytes(s)
fmt.Println(b[0]) // OK: reading
// b[0] = 'H' // UNDEFINED BEHAVIOR: modifying string data!
Когда zero-copy оправдан
// Hot path processing large strings without modification
func containsPattern(s string, pattern []byte) bool {
// Convert string to []byte without copying
data := stringToBytes(s)
return bytes.Contains(data, pattern) // Only reads data
}
cgo: взаимодействие с C
Основы cgo
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// Custom C function
int add(int a, int b) {
return a + b;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Call C function
result := C.add(C.int(40), C.int(2))
fmt.Println(int(result)) // 42
// Use C math library
sqrtResult := C.sqrt(C.double(144))
fmt.Println(float64(sqrtResult)) // 12
// C strings
cStr := C.CString("Hello from Go!")
defer C.free(unsafe.Pointer(cStr))
C.puts(cStr)
// C malloc
p := C.malloc(C.size_t(100))
defer C.free(p)
}
Передача данных между Go и C
/*
#include <string.h>
typedef struct {
int id;
char name[64];
} User;
void fill_user(User* u, int id, const char* name) {
u->id = id;
strncpy(u->name, name, 63);
u->name[63] = '\0';
}
*/
import "C"
func main() {
var cUser C.User
name := C.CString("Alice")
defer C.free(unsafe.Pointer(name))
C.fill_user(&cUser, C.int(1), name)
fmt.Printf("ID: %d, Name: %s\n",
int(cUser.id),
C.GoString(&cUser.name[0]),
)
}
Стоимость cgo
Вызов C-функции через cgo значительно дороже вызова Go-функции:
// Go function call: ~1-2 ns
// cgo call: ~50-100 ns (due to stack switching, scheduler)
Причины:
- Переключение стека (Go goroutine stack -> C stack)
- Блокировка потока OS
- Невозможность прервать C-код для GC
Компиляторные директивы
//go:linkname
Связывает локальное имя с символом из другого пакета (обход экспорта):
//go:linkname nanotime runtime.nanotime
func nanotime() int64
// Now you can call runtime's internal nanotime function
t := nanotime()
Предупреждение:
//go:linknameломает инкапсуляцию и может перестать работать при обновлении Go. Используйте только как крайнюю меру.
//go:noescape
Сообщает компилятору, что аргументы не "убегают" на heap:
//go:noescape
func fastHash(data []byte) uint64
Это помогает escape analysis оставить данные на стеке. Применяется только к функциям без тела (реализованным на ассемблере).
//go:nosplit
Запрещает проверку стека при вызове (no stack split check):
//go:nosplit
func criticalPath() {
// Very fast but limited stack space
}
//go:noinline
Запрещает инлайнинг:
//go:noinline
func doWork() int {
return 42
}
Полезно для точного бенчмаркинга и отладки.
Шесть легальных паттернов unsafe.Pointer
Спецификация Go определяет только 6 допустимых конвертаций с unsafe.Pointer:
1. *T1 -> unsafe.Pointer -> *T2
// Convert between pointer types with same memory layout
f := math.Float64bits(1.5)
p := *(*float64)(unsafe.Pointer(&f))
2. unsafe.Pointer -> uintptr (только для печати)
p := unsafe.Pointer(&x)
fmt.Printf("address: %x\n", uintptr(p))
// Do NOT convert uintptr back to unsafe.Pointer later!
3. unsafe.Pointer -> uintptr -> arithmetic -> unsafe.Pointer (одно выражение)
// Access struct field by offset
p := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.field))
4. syscall.Syscall arguments
// Pass to syscall (uintptr converted at call boundary)
syscall.Syscall(SYS_READ, uintptr(fd),
uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
5. reflect.Value.Pointer/UnsafeAddr -> unsafe.Pointer
p := unsafe.Pointer(reflect.ValueOf(&x).Pointer())
6. reflect.SliceHeader/StringHeader (deprecated, use unsafe.Slice/String instead)
// Old way (deprecated):
header := (*reflect.StringHeader)(unsafe.Pointer(&s))
// New way (Go 1.17+):
data := unsafe.StringData(s)
slice := unsafe.Slice(data, len(s))
Почему unsafe существует и когда оправдан
Оправданные случаи:
- Системное программирование -- syscalls, memory-mapped I/O
- Высокопроизводительная сериализация -- zero-copy парсинг
- Интероп с C через cgo
- Runtime и стандартная библиотека (сам Go использует unsafe повсюду)
- Атомарные операции на нестандартных типах
Когда НЕ использовать:
- Для "оптимизации", которая экономит наносекунды на некритичном пути
- Когда есть безопасная альтернатива
- В бизнес-логике
- Без бенчмарков, доказывающих необходимость
- Без тестов с
-race
Правило: Если вы не можете объяснить, почему
unsafeбезопасен в вашем конкретном случае -- не используйте его. "It's called unsafe for a reason."