Skip to content

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

go
type ArbitraryType int

Arbitrary 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

go
type IntegerType int

IntegerType 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

go
func Sizeof(x ArbitraryType) uintptr

Retourne la taille de la variable x en octets, sans inclure la taille de son contenu référencé, par exemple :

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

Cette 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

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

Si vous ne comprenez pas l'alignement mémoire, vous pouvez consulter : Go语言内存对齐详解 - 掘金 (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

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

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 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.Pointer peut être converti en tout type de pointeur
  • uintptr peut être converti en unsafe.Pointer
  • unsafe.Pointer peut être converti en uintptr

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 :

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

Ces 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

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

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

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

Il 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.

go
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

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 retourne un Pointer mis à jour avec le décalage len, équivalent à Pointer(uintptr(ptr) + uintptr(len))

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

Par exemple :

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) *ArbitraryTyp

Cette 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

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

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 :

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

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

La 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.

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]

Modifier les données de la tranche numsRef1 entraîne également une modification des données de nums

StringData

func StringData(str string) *byte

Similaire à 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 :

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

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

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

Similaire à 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

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

Golang by www.golangdev.cn edit