Skip to content

unsafe

Официальная документация: unsafe package - unsafe - Go Packages

Стандартная библиотека unsafe — это официальная библиотека для низкоуровневого программирования. Предоставляемые ею операции позволяют обходить систему типов Go для прямого чтения и записи памяти. Этот пакет может быть непереносимым, и официальная документация указывает, что он не защищён правилами совместимости Go 1. Несмотря на это, unsafe широко используется во многих проектах, включая официальную стандартную библиотеку.

TIP

Причина непереносимости заключается в том, что результаты некоторых операций зависят от реализации операционной системы, и разные системы могут давать разные результаты.

ArbitraryType

go
type ArbitraryType int

Arbitrary переводится как «произвольный» и представляет любой тип, но не эквивалентен any. На самом деле этот тип не принадлежит пакету unsafe и представлен здесь исключительно в документационных целях.

IntegerType

go
type IntegerType int

IntegerType представляет любой целочисленный тип. На самом деле этот тип не принадлежит пакету unsafe и представлен здесь исключительно в документационных целях.

Эти два типа не должны вызывать беспокойства — они служат лишь представителями. При использовании функций пакета unsafe редактор может даже提示 о несоответствии типов; фактический тип будет тем, который вы передали.

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

Возвращает размер переменной x в байтах, не включая размер содержимого, на которое она ссылается. Пример:

go
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
16

Offsetof

go
func Offsetof(x ArbitraryType) uintptr

Эта функция возвращает смещение поля внутри структуры, поэтому x должно быть полем структуры. Возвращаемое значение — количество байт от начала адреса структуры до начала адреса поля. Пример:

go
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
8

Alignof

Если вы не знаете, что такое выравнивание памяти, см.: Выравнивание памяти в Go

go
func Alignof(x ArbitraryType) uintptr

Размер выравнивания обычно является минимальным значением между длиной слова компьютера (в байтах) и Sizeof. Например, на машине amd64 длина слова составляет 64 бита, то есть 8 байт. Пример:

go
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))
}
go
8 24
8 16
4 4

Pointer

go
type Pointer *ArbitraryType

Pointer — это «указатель», который может указывать на любой тип. Его тип — *ArbitraryType. Этот тип используется вместе с uintptr для реализации полной мощи пакета unsafe. Согласно официальной документации, с типом unsafe.Pointer можно выполнять четыре специальные операции:

  • Любой тип указателя может быть преобразован в unsafe.Pointer
  • unsafe.Pointer может быть преобразован в любой тип указателя
  • uintptr может быть преобразован в unsafe.Pointer
  • unsafe.Pointer может быть преобразован в uintptr

Эти четыре операции составляют основу всего пакета unsafe и позволяют писать код, игнорирующий систему типов для прямого чтения и записи памяти. При использовании следует быть особенно внимательным.

TIP

unsafe.Pointer нельзя разыменовать, также нельзя взять его адрес.

(1) Преобразование *T1 в unsafe.Pointer, затем в *T2

Если имеется тип *T1 и *T2, причём T2 не больше T1 и оба имеют эквивалентную структуру памяти, допускается преобразование данных типа T2 в T1. Пример:

go
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 хранит адрес, но разница в том, что первый синтаксически является указателем (ссылкой), а второй — просто целочисленным значением. Пример:

go
func main() {
   num := 1
   fmt.Println(unsafe.Pointer(&num))
   fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}
0xc00001c088
0xc00001c088

Большая разница заключается в обработке сборщиком мусора. Поскольку unsafe.Pointer является ссылкой, он не будет удалён при необходимости, тогда как uintptr как значение не имеет такой привилегии. Другой важный момент: когда адрес элемента, на который указывает указатель, перемещается, GC обновляет старый адрес ссылки указателя, но не обновляет значение, сохранённое в uintptr. Например, следующий код может вызвать проблемы:

go
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, можно выполнить арифметику указателей:

go
p = unsafe.Pointer(uintptr(p) + offset)

Таким образом, имея один указатель, можно получить доступ к внутренним элементам некоторых типов, таких как массивы и структуры, независимо от того,暴露лены ли их внутренние элементы. Пример:

go
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
2

Add

go
func Add(ptr Pointer, len IntegerType) Pointer

Add возвращает Pointer, обновлённый со смещением len, что эквивалентно Pointer(uintptr(ptr) + uintptr(len)):

go
Pointer(uintptr(ptr) + uintptr(len))

Пример:

go
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
3

SliceData

go
func SliceData(slice []ArbitraryType) *ArbitraryType

Эта функция принимает срез и возвращает начальный адрес его базового массива. Без использования SliceData можно получить адрес базового массива только через указатель на первый элемент:

go
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:

go
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

go
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

Функция Slice принимает указатель и длину смещения, возвращая срезовое представление этого участка памяти. В процессе не происходит копирования памяти, поэтому модификация среза напрямую влияет на данные по этому адресу, и наоборот. Обычно используется вместе с SliceData:

go
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

go
func StringData(str string) *byte

Аналогично функции SliceData, но выделено отдельно из-за частой необходимости преобразования строк в байтовые срезы. Пример использования:

go
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

go
func String(ptr *byte, len IntegerType) string

Аналогично функции Slice, принимает указатель на байт и длину смещения, возвращая строковое представление. В процессе не происходит копирования памяти. Пример преобразования байтового среза в строку:

go
func main() {
  bytes := []byte("hello world")
  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
  fmt.Println(str)
}

StringData и String не выполняют копирование памяти при преобразовании между строками и байтовыми срезами, что обеспечивает лучшую производительность по сравнению с прямым преобразованием типов. Однако это适用于 только для случаев чтения. Если планируется модификация данных, лучше не использовать эти функции.

Golang by www.golangdev.cn edit