unsafe
Официальная документация: unsafe package - unsafe - Go Packages
Стандартная библиотека unsafe — это официальная библиотека для низкоуровневого программирования. Предоставляемые ею операции позволяют обходить систему типов Go для прямого чтения и записи памяти. Этот пакет может быть непереносимым, и официальная документация указывает, что он не защищён правилами совместимости Go 1. Несмотря на это, unsafe широко используется во многих проектах, включая официальную стандартную библиотеку.
TIP
Причина непереносимости заключается в том, что результаты некоторых операций зависят от реализации операционной системы, и разные системы могут давать разные результаты.
ArbitraryType
type ArbitraryType intArbitrary переводится как «произвольный» и представляет любой тип, но не эквивалентен any. На самом деле этот тип не принадлежит пакету unsafe и представлен здесь исключительно в документационных целях.
IntegerType
type IntegerType intIntegerType представляет любой целочисленный тип. На самом деле этот тип не принадлежит пакету unsafe и представлен здесь исключительно в документационных целях.
Эти два типа не должны вызывать беспокойства — они служат лишь представителями. При использовании функций пакета unsafe редактор может даже提示 о несоответствии типов; фактический тип будет тем, который вы передали.
Sizeof
func Sizeof(x ArbitraryType) uintptrВозвращает размер переменной x в байтах, не включая размер содержимого, на которое она ссылается. Пример:
func main() {
var ints byte = 1
fmt.Println(unsafe.Sizeof(ints))
var floats float32 = 1.0
fmt.Println(unsafe.Sizeof(floats))
var complexs complex128 = 1 + 2i
fmt.Println(unsafe.Sizeof(complexs))
var slice []int = make([]int, 100)
fmt.Println(unsafe.Sizeof(slice))
var mp map[string]int = make(map[string]int, 0)
fmt.Println(unsafe.Sizeof(mp))
type person struct {
name string
age int
}
fmt.Println(unsafe.Sizeof(person{}))
type man struct {
name string
}
fmt.Println(unsafe.Sizeof(man{}))
}1
4
16
24
8
24
16Offsetof
func Offsetof(x ArbitraryType) uintptrЭта функция возвращает смещение поля внутри структуры, поэтому x должно быть полем структуры. Возвращаемое значение — количество байт от начала адреса структуры до начала адреса поля. Пример:
func main() {
type person struct {
name string
age int
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Sizeof(p))
fmt.Println(unsafe.Offsetof(p.name))
fmt.Println(unsafe.Sizeof(p.name))
fmt.Println(unsafe.Offsetof(p.age))
fmt.Println(unsafe.Sizeof(p.age))
}24
0
16
16
8Alignof
Если вы не знаете, что такое выравнивание памяти, см.: Выравнивание памяти в Go
func Alignof(x ArbitraryType) uintptrРазмер выравнивания обычно является минимальным значением между длиной слова компьютера (в байтах) и Sizeof. Например, на машине amd64 длина слова составляет 64 бита, то есть 8 байт. Пример:
func main() {
type person struct {
name string
age int32
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Alignof(p), unsafe.Sizeof(p))
fmt.Println(unsafe.Alignof(p.name), unsafe.Sizeof(p.name))
fmt.Println(unsafe.Alignof(p.age), unsafe.Sizeof(p.age))
}8 24
8 16
4 4Pointer
type Pointer *ArbitraryTypePointer — это «указатель», который может указывать на любой тип. Его тип — *ArbitraryType. Этот тип используется вместе с uintptr для реализации полной мощи пакета unsafe. Согласно официальной документации, с типом unsafe.Pointer можно выполнять четыре специальные операции:
- Любой тип указателя может быть преобразован в
unsafe.Pointer unsafe.Pointerможет быть преобразован в любой тип указателяuintptrможет быть преобразован вunsafe.Pointerunsafe.Pointerможет быть преобразован вuintptr
Эти четыре операции составляют основу всего пакета unsafe и позволяют писать код, игнорирующий систему типов для прямого чтения и записи памяти. При использовании следует быть особенно внимательным.
TIP
unsafe.Pointer нельзя разыменовать, также нельзя взять его адрес.
(1) Преобразование *T1 в unsafe.Pointer, затем в *T2
Если имеется тип *T1 и *T2, причём T2 не больше T1 и оба имеют эквивалентную структуру памяти, допускается преобразование данных типа T2 в T1. Пример:
func main() {
fmt.Println(Float64bits(12.3))
fmt.Println(Float64frombits(Float64bits(12.3)))
}
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
func Float64frombits(b uint64) float64 {
return *(*float64)(unsafe.Pointer(&b))
}4623113902481840538
12.3Эти функции фактически являются двумя функциями из пакета math. Изменение типов в процессе:
float64 -> *float64 -> unsafe.Pointer -> *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64(2) Преобразование unsafe.Pointer в uintptr
При преобразовании unsafe.Pointer в uintptr адрес, на который указывает первый, становится значением второго. uintptr хранит адрес, но разница в том, что первый синтаксически является указателем (ссылкой), а второй — просто целочисленным значением. Пример:
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088Большая разница заключается в обработке сборщиком мусора. Поскольку unsafe.Pointer является ссылкой, он не будет удалён при необходимости, тогда как uintptr как значение не имеет такой привилегии. Другой важный момент: когда адрес элемента, на который указывает указатель, перемещается, GC обновляет старый адрес ссылки указателя, но не обновляет значение, сохранённое в uintptr. Например, следующий код может вызвать проблемы:
func main() {
num := 16
address := uintptr(unsafe.Pointer(&num))
np := (*int64)(unsafe.Pointer(address))
fmt.Println(*np)
}В некоторых случаях после перемещения переменной GC адрес, на который указывает address, становится недействительным, и попытка создать указатель с использованием этого значения вызовет panic:
panic: runtime error: invalid memory address or nil pointer dereferenceПоэтому не рекомендуется сохранять значение после преобразования Pointer в uintptr.
(3) Преобразование uintptr в unsafe.Pointer
Следующий способ позволяет получить указатель из uintptr. Пока указатель действителен, ситуация с недействительным адресом, как во втором примере, не возникнет. Pointer и указатель типа не поддерживают арифметику указателей, но uintptr — это просто целочисленное значение, с которым можно выполнять математические операции. Выполнив математические операции над uintptr и преобразовав результат в Pointer, можно выполнить арифметику указателей:
p = unsafe.Pointer(uintptr(p) + offset)Таким образом, имея один указатель, можно получить доступ к внутренним элементам некоторых типов, таких как массивы и структуры, независимо от того,暴露лены ли их внутренние элементы. Пример:
func main() {
type person struct {
name string
age int32
}
p := &person{"jack", 18}
pp := unsafe.Pointer(p)
fmt.Println(*(*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.name))))
fmt.Println(*(*int32)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.age))))
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 8)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 16)))
}jack
18
2Add
func Add(ptr Pointer, len IntegerType) PointerAdd возвращает Pointer, обновлённый со смещением len, что эквивалентно Pointer(uintptr(ptr) + uintptr(len)):
Pointer(uintptr(ptr) + uintptr(len))Пример:
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Add(ps, 8)))
fmt.Println(*(*int)(unsafe.Add(ps, 16)))
}2
3SliceData
func SliceData(slice []ArbitraryType) *ArbitraryTypeЭта функция принимает срез и возвращает начальный адрес его базового массива. Без использования SliceData можно получить адрес базового массива только через указатель на первый элемент:
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}Также можно использовать тип reflect.SliceHeader, но начиная с версии 1.20 он устарел. SliceData создан для его замены. Пример использования SliceData:
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(unsafe.SliceData(nums)), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(int(0))), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}Slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypeФункция Slice принимает указатель и длину смещения, возвращая срезовое представление этого участка памяти. В процессе не происходит копирования памяти, поэтому модификация среза напрямую влияет на данные по этому адресу, и наоборот. Обычно используется вместе с SliceData:
func main() {
nums := []int{1, 2, 3, 4}
numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))
numsRef1[0] = 2
fmt.Println(nums)
}[2 2 3 4]Изменение данных среза numsRef1 приводит к изменению данных nums.
StringData
func StringData(str string) *byteАналогично функции SliceData, но выделено отдельно из-за частой необходимости преобразования строк в байтовые срезы. Пример использования:
func main() {
str := "hello,world!"
for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {
char := *(*byte)(ptr)
fmt.Println(string(char))
}
}Поскольку строковые литералы хранятся в сегменте только для чтения процесса, попытка изменить данные底层 строки приведёт к аварийному завершению программы с ошибкой fatal. Однако для строковых переменных, хранящихся в куче или стеке, модификация их底层 данных во время выполнения вполне возможна.
String
func String(ptr *byte, len IntegerType) stringАналогично функции Slice, принимает указатель на байт и длину смещения, возвращая строковое представление. В процессе не происходит копирования памяти. Пример преобразования байтового среза в строку:
func main() {
bytes := []byte("hello world")
str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println(str)
}StringData и String не выполняют копирование памяти при преобразовании между строками и байтовыми срезами, что обеспечивает лучшую производительность по сравнению с прямым преобразованием типов. Однако это适用于 только для случаев чтения. Если планируется модификация данных, лучше не использовать эти функции.
