unsafe
Documentation officielle : unsafe package - unsafe - Go Packages
La bibliothèque standard unsafe est une bibliothèque fournie officiellement qui permet la programmation de bas niveau. Les opérations fournies par ce package peuvent contourner directement le système de types de Go pour lire et écrire la mémoire. Ce package peut ne pas être portable, et officiellement il est déclaré que ce package n'est pas protégé par les directives de compatibilité Go 1. Malgré cela, unsafe est largement utilisé dans de nombreux projets, y compris dans les bibliothèques standard officielles.
TIP
La raison pour laquelle il n'est pas portable est que certains résultats d'opérations dépendent de l'implémentation du système d'exploitation, différents systèmes peuvent donner des résultats différents.
ArbitraryType
type ArbitraryType intArbitrary peut être traduit par arbitraire, ici il représente n'importe quel type, et n'est pas équivalent à any. En fait, ce type n'appartient pas au package unsafe, il apparaît ici uniquement à des fins de documentation.
IntegerType
type IntegerType intIntegerType représente n'importe quel type d'entier. En fait, ce type n'appartient pas au package unsafe, il apparaît ici uniquement à des fins de documentation.
Ces deux types ne nécessitent pas trop d'attention, ils ne sont que des représentations. Lors de l'utilisation des fonctions du package unsafe, l'éditeur vous indiquera même que les types ne correspondent pas. Leur type réel est le type spécifique que vous passez.
Sizeof
func Sizeof(x ArbitraryType) uintptrRetourne la taille de la variable x en octets, sans inclure la taille de son contenu référencé, par exemple :
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) uintptrCette fonction représente le décalage d'un champ dans une structure, donc x doit être un champ de structure. Ou on peut dire que la valeur de retour est le nombre d'octets entre l'adresse de début de la structure et l'adresse de début du champ, par exemple
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
Si vous ne comprenez pas l'alignement mémoire, vous pouvez consulter : Go语言内存对齐详解 - 掘金 (juejin.cn)
func Alignof(x ArbitraryType) uintptrLa taille d'alignement est généralement la plus petite valeur entre la longueur de mot de la machine en octets et Sizeof. Par exemple, sur une machine amd64, la longueur de mot est de 64 bits, soit 8 octets, par exemple :
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 est une sorte de "pointeur" qui peut pointer vers n'importe quel type. Son type est *ArbitraryType. Ce type doit être utilisé en combinaison avec uintptr pour vraiment exploiter la puissance du package unsafe. Dans la description de la documentation officielle, le type unsafe.Pointer peut effectuer quatre opérations spéciales :
- Tout type de pointeur peut être converti en
unsafe.Pointer unsafe.Pointerpeut être converti en tout type de pointeuruintptrpeut être converti enunsafe.Pointerunsafe.Pointerpeut être converti enuintptr
Ces quatre opérations spéciales constituent la base de tout le package unsafe. C'est grâce à ces quatre opérations qu'on peut écrire du code qui ignore le système de types pour lire et écrire directement la mémoire. Il est recommandé d'être très prudent lors de l'utilisation.
TIP
unsafe.Pointer ne peut pas être déréférencé, de même on ne peut pas prendre son adresse.
(1) Convertir *T1 en unsafe.Pointer puis en *T2
Soit les types *T1, *T2, supposons que T2 n'est pas plus grand que T1 et que les deux ont une disposition mémoire équivalente, alors il est permis de convertir une donnée de type T2 en T1. Par exemple :
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.3Ces deux fonctions sont en fait deux fonctions du package math. Les changements de type pendant le processus sont
float64 -> *float64 -> unsafe.Pointer -> *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64(2) Convertir unsafe.Pointer en uintptr
Lors de la conversion de unsafe.Pointer en uintptr, l'adresse pointée par le premier devient la valeur du second. uintptr stocke une adresse, la différence est que le premier est syntaxiquement un pointeur, une référence, le second est simplement une valeur entière. Par exemple
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088La différence la plus importante réside dans le traitement du ramasse-miettes. Puisque unsafe.Pointer est une référence, il ne sera pas recyclé si nécessaire. Le second, étant simplement une valeur, n'a naturellement pas ce traitement spécial. Un autre point à noter est que lorsque l'adresse de l'élément pointé par le pointeur change, le GC met à jour l'ancienne adresse référencée par le pointeur, mais ne met pas à jour la valeur stockée dans uintptr. Par exemple, le code suivant peut poser problème :
func main() {
num := 16
address := uintptr(unsafe.Pointer(&num))
np := (*int64)(unsafe.Pointer(address))
fmt.Println(*np)
}Dans certains cas, après que le GC a déplacé la variable, l'adresse pointée par address est déjà invalide, et créer un pointeur avec cette valeur provoquera une panic
panic: runtime error: invalid memory address or nil pointer dereferenceIl n'est donc pas recommandé de sauvegarder la valeur convertie de Pointer en uintptr.
(3) Convertir via uintptr en unsafe.Pointer
De la façon suivante, on peut obtenir un pointeur via uintptr, tant que le pointeur est valide, il n'y aura pas de problème d'adresse invalide comme dans l'exemple 2. Pointer et les pointeurs de type ne supportent pas eux-mêmes l'arithmétique de pointeurs, mais uintptr est simplement une valeur entière, on peut faire des opérations mathématiques. Après avoir fait des opérations mathématiques sur uintptr puis converti en Pointer, on peut réaliser l'arithmétique de pointeurs.
p = unsafe.Pointer(uintptr(p) + offset)Ainsi, avec un seul pointeur, on peut accéder aux éléments internes de certains types, comme les tableaux et les structures, que leurs éléments internes soient exposés ou non, par exemple
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 retourne un Pointer mis à jour avec le décalage len, équivalent à Pointer(uintptr(ptr) + uintptr(len))
Pointer(uintptr(ptr) + uintptr(len))Par exemple :
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) *ArbitraryTypCette fonction reçoit une tranche et retourne l'adresse de début de son tableau sous-jacent. Si on n'utilise pas SliceData, on ne peut obtenir l'adresse du tableau sous-jacent qu'en prenant l'adresse du premier élément, comme suit
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)
}
}On peut aussi l'obtenir via le type reflect.SliceHeader, mais il est obsolète depuis la version 1.20. SliceData est là pour le remplacer. Un exemple avec 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 fonction Slice reçoit un pointeur et un décalage de longueur, elle retourne l'expression sous forme de tranche de ce segment mémoire. Le processus n'implique pas de copie de mémoire. Modifier la tranche affectera directement les données à cette adresse, et inversement. Elle est généralement utilisée en combinaison avec 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]Modifier les données de la tranche numsRef1 entraîne également une modification des données de nums
StringData
func StringData(str string) *byteSimilaire à la fonction SliceData, mais comme la conversion de chaîne en tranche d'octets est fréquente, elle est séparée. Exemple d'utilisation :
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))
}
}Puisque les littéraux de chaîne sont stockés dans le segment de lecture seule du processus, si vous essayez de modifier les données sous-jacentes de la chaîne ici, le programme plantera directement avec une erreur fatal. Cependant, pour les variables de chaîne stockées sur le tas ou la pile, modifier leurs données sous-jacentes à l'exécution est tout à fait possible.
String
func String(ptr *byte, len IntegerType) stringSimilaire à la fonction Slice, elle reçoit un pointeur de type octet et un décalage de longueur, retourne son expression sous forme de chaîne, sans copie de mémoire. Voici un exemple de conversion de tranche d'octets en chaîne
func main() {
bytes := []byte("hello world")
str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println(str)
}StringData et String n'impliquent pas de copie de mémoire lors de la conversion entre chaîne et tranche d'octets, leurs performances sont meilleures que la conversion directe de type. Cependant, elles ne conviennent qu'aux cas de lecture seule. Si vous prévoyez de modifier les données, il vaut mieux ne pas les utiliser.
