Skip to content

unsafe

Indirizzo documentazione ufficiale: unsafe package - unsafe - Go Packages

La libreria standard unsafe è una libreria fornita ufficialmente per la programmazione di basso livello. Le operazioni fornite da questo pacchetto possono saltare il sistema dei tipi di Go per leggere e scrivere direttamente la memoria. Questo pacchetto potrebbe non essere portatile e l'ufficiale dichiara che non è protetto dalle linee guida di compatibilità Go 1. Nonostante ciò, unsafe è ancora ampiamente utilizzato in molti progetti, inclusi quelli forniti ufficialmente dalla libreria standard.

TIP

Il motivo per cui non è portatile è che alcuni risultati dipendono dall'implementazione del sistema operativo, e sistemi diversi potrebbero avere risultati diversi.

ArbitraryType

go
type ArbitraryType int

Arbitrary può essere tradotto come arbitrario, qui rappresenta qualsiasi tipo, e non è equivalente a any. In realtà questo tipo non appartiene al pacchetto unsafe, appare qui solo per scopi documentali.

IntegerType

go
type IntegerType int

IntegerType rappresenta qualsiasi tipo intero, in realtà questo tipo non appartiene al pacchetto unsafe, appare qui solo per scopi documentali.

Non c'è bisogno di preoccuparsi troppo di questi due tipi, sono solo una rappresentazione. Quando si utilizzano le funzioni del pacchetto unsafe, l'editor potrebbe persino segnalare che il tipo non corrisponde. Il loro tipo effettivo è il tipo specifico che si passa.

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

Restituisce la dimensione della variabile x in byte, esclusa la dimensione del contenuto a cui fa riferimento, ad esempio:

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

Questa funzione è utilizzata per rappresentare l'offset del campo all'interno di una struct, quindi x deve essere un campo di struct, o in altre parole il valore restituito è il numero di byte tra l'inizio dell'indirizzo della struct e l'inizio dell'indirizzo del campo, ad esempio

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

Se non si conosce cos'è l'allineamento della memoria, si può andare qui: Go 语言内存对齐详解 - 掘金 (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

La dimensione di allineamento è generalmente il minimo tra la lunghezza della parola del computer in byte e Sizeof, ad esempio su una macchina amd64, la lunghezza della parola è 64 bit, cioè 8 byte, ad esempio:

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 è un "puntatore" che può puntare a qualsiasi tipo, il suo tipo è *ArbitraryType. Questo tipo deve essere utilizzato in combinazione con uintptr per sfruttare appieno la potenza del pacchetto unsafe. Nella descrizione della documentazione ufficiale, il tipo unsafe.Pointer può eseguire quattro operazioni speciali,分别是:

  • Qualsiasi tipo di puntatore può essere convertito in unsafe.Pointer
  • unsafe.Pointer può essere convertito in qualsiasi tipo di puntatore
  • uintptr può essere convertito in unsafe.Pointer
  • unsafe.Pointer può essere convertito in uintptr

Queste quattro operazioni speciali costituiscono la base dell'intero pacchetto unsafe, ed è proprio grazie a queste quattro operazioni che è possibile scrivere codice in grado di ignorare il sistema dei tipi per leggere e scrivere direttamente la memoria. Si consiglia di prestare particolare attenzione durante l'utilizzo.

TIP

unsafe.Pointer non può essere dereferenziato, e allo stesso modo non è possibile ottenere il suo indirizzo.

(1) Convertire *T1 in unsafe.Pointer e poi in *T2

Esistono i tipi *T1 e *T2. Supponendo che T2 non sia maggiore di T1 e che entrambi abbiano un layout di memoria equivalente, è consentito convertire un dato di tipo T2 in T1. Ad esempio:

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

Queste due funzioni sono effettivamente le due funzioni del pacchetto math. Le variazioni di tipo durante il processo sono le seguenti

float64 -> *float64 -> unsafe.Pointer ->  *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64

(2) Convertire unsafe.Pointer in uintptr

Quando si converte unsafe.Pointer in uintptr, l'indirizzo puntato dal primo diventa il valore del secondo. uintptr memorizza un indirizzo, la differenza è che il primo è sintatticamente un puntatore, un riferimento, mentre il secondo è solo un valore intero. Ad esempio

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

Una differenza maggiore sta nella gestione del garbage collection. Poiché unsafe.Pointer è un riferimento, non verrà recuperato quando necessario, mentre il secondo è solo un valore e naturalmente non avrà questo trattamento speciale. Un altro punto da notare è che quando l'indirizzo dell'elemento puntato dal puntatore si sposta, il GC aggiornerà il vecchio indirizzo del riferimento del puntatore, ma non aggiornerà il valore salvato da uintptr. Ad esempio, il seguente codice potrebbe causare problemi:

go
func main() {
   num := 16
   address := uintptr(unsafe.Pointer(&num))
   np := (*int64)(unsafe.Pointer(address))
   fmt.Println(*np)
}

In alcuni casi, dopo che il GC ha spostato la variabile, l'indirizzo puntato da address non è più valido, e utilizzare quel valore per creare un puntatore causerà un panic

panic: runtime error: invalid memory address or nil pointer dereference

Quindi non si consiglia di salvare il valore convertito da Pointer in uintptr.

(3) Convertire uintptr in unsafe.Pointer

Il modo seguente può ottenere un puntatore da uintptr, purché il puntatore sia valido, non si verificherà la situazione di indirizzo invalido dell'esempio due. Pointer e il puntatore di tipo non supportano l'aritmetica dei puntatori, ma uintptr è solo un valore intero e può eseguire operazioni matematiche. Eseguendo operazioni matematiche su uintptr e poi convertendolo in Pointer si può completare l'aritmetica dei puntatori.

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

In questo modo, è possibile accedere ad alcuni elementi interni di alcuni tipi solo tramite un puntatore, come array e struct, indipendentemente dal fatto che i loro elementi interni siano esposti esternamente, ad esempio

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 restituisce il Pointer aggiornato con l'offset len, equivalente a Pointer(uintptr(ptr) + uintptr(len))

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

Ad esempio:

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

Questa funzione riceve una slice e restituisce l'indirizzo iniziale del suo array sottostante. Se non si utilizza SliceData, si può solo ottenere l'indirizzo dell'array sottostante prendendo il puntatore del suo primo elemento, come segue

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)
  }
}

Naturalmente si può anche ottenere tramite il tipo reflect.SliceHeader, ma dalla versione 1.20 è stato deprecato. SliceData è stato creato per sostituirlo. Di seguito un esempio di utilizzo di 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

La funzione Slice riceve un puntatore e un offset di lunghezza, e restituisce la forma di slice di quel segmento di memoria. Durante il processo non viene coinvolta copia di memoria, la modifica della slice influenzerà direttamente i dati su quell'indirizzo, e viceversa. Di solito viene utilizzato in combinazione con 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]

La modifica dei dati della slice numsRef1 causerà cambiamenti nei dati di nums

StringData

go
func StringData(str string) *byte

Come la funzione SliceData, ma poiché la conversione da stringa a slice di byte è frequente, viene presa separatamente. Di seguito un esempio di utilizzo

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))
  }
}

Poiché i letterali di stringa sono memorizzati nel segmento di sola lettura del processo, se si tenta di modificare i dati sottostanti della stringa qui, il programma si interromperà con un fatal. Tuttavia, per le variabili stringa memorizzate sullo stack heap, è completamente fattibile modificare i dati sottostanti durante l'esecuzione.

String

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

Come la funzione Slice, riceve un puntatore di tipo byte e il suo offset di lunghezza, e restituisce la sua forma di espressione stringa. Durante il processo non viene coinvolta copia di memoria. Di seguito un esempio di conversione da slice di byte a stringa

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

StringData e String non coinvolgono copia di memoria nel processo di conversione tra stringa e slice di byte, le prestazioni sono migliori rispetto alla conversione diretta di tipo, ma sono adatte solo per casi di sola lettura. Se si intende modificare i dati, è meglio non utilizzarli.

Golang by www.golangdev.cn edit