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
type ArbitraryType intArbitrary 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
type IntegerType intIntegerType 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
func Sizeof(x ArbitraryType) uintptrRestituisce la dimensione della variabile x in byte, esclusa la dimensione del contenuto a cui fa riferimento, ad esempio:
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) uintptrQuesta 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
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
Se non si conosce cos'è l'allineamento della memoria, si può andare qui: Go 语言内存对齐详解 - 掘金 (juejin.cn)
func Alignof(x ArbitraryType) uintptrLa 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:
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 è 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.Pointerpuò essere convertito in qualsiasi tipo di puntatoreuintptrpuò essere convertito inunsafe.Pointerunsafe.Pointerpuò essere convertito inuintptr
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:
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.3Queste 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
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088Una 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:
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 dereferenceQuindi 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.
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
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 restituisce il Pointer aggiornato con l'offset len, equivalente a Pointer(uintptr(ptr) + uintptr(len))
Pointer(uintptr(ptr) + uintptr(len))Ad esempio:
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) *ArbitraryTypeQuesta 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
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
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) []ArbitraryTypeLa 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.
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
func StringData(str string) *byteCome la funzione SliceData, ma poiché la conversione da stringa a slice di byte è frequente, viene presa separatamente. Di seguito un esempio di utilizzo
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
func String(ptr *byte, len IntegerType) stringCome 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
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.
