Skip to content

unsafe

Offizielle Dokumentation: unsafe package - unsafe - Go Packages

Die unsafe Standardbibliothek ist eine von der offiziellen Seite bereitgestellte Bibliothek für Low-Level-Programmierung. Die in diesem Paket bereitgestellten Operationen können das Go-Typsystem umgehen und direkt Speicher lesen und schreiben. Dieses Paket ist möglicherweise nicht portierbar, und die offizielle Seite erklärt, dass dieses Paket nicht den Go 1 Kompatibilitätsrichtlinien unterliegt. Dennoch wird unsafe von vielen Projekten verwendet, einschließlich der offiziellen Standardbibliothek.

TIP

Der Grund, warum es nicht portierbar ist, liegt daran, dass einige Operationsergebnisse von der Betriebssystemimplementierung abhängen. Unterschiedliche Systeme können zu unterschiedlichen Ergebnissen führen.

ArbitraryType

go
type ArbitraryType int

Arbitrary kann mit "beliebig" übersetzt werden und repräsentiert hier einen beliebigen Typ. Es ist nicht gleichbedeutend mit any. Tatsächlich gehört dieser Typ nicht zum unsafe Paket, er erscheint hier nur zu Dokumentationszwecken.

IntegerType

go
type IntegerType int

IntegerType repräsentiert einen beliebigen Integer-Typ. Tatsächlich gehört dieser Typ nicht zum unsafe Paket, er erscheint hier nur zu Dokumentationszwecken.

Diese beiden Typen müssen nicht zu sehr beachtet werden. Sie sind nur Repräsentationen. Bei der Verwendung von Funktionen aus dem unsafe Paket wird der Editor Sie sogar darauf hinweisen, dass die Typen nicht übereinstimmen. Ihre tatsächlichen Typen sind die spezifischen Typen, die Sie übergeben.

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

Gibt die Größe der Variable x in Bytes zurück, ohne die Größe ihres referenzierten Inhalts. Zum Beispiel:

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

Diese Funktion gibt den Offset eines Feldes innerhalb einer Struktur an. x muss also ein Strukturfeld sein. Der Rückgabewert ist die Anzahl der Bytes zwischen dem Anfang der Strukturadresse und dem Anfang der Feldadresse. Zum Beispiel:

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

Wenn Sie nicht wissen, was Speicherausrichtung bedeutet, können Sie hier nachlesen: Go Speicherausrichtung erklärt

go
func Alignof(x ArbitraryType) uintptr

Die Ausrichtungsgröße ist normalerweise das Minimum aus der Maschinenwortlänge in Bytes und Sizeof. Auf einer amd64-Maschine beträgt die Wortlänge beispielsweise 64 Bit, also 8 Bytes. Zum Beispiel:

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 ist ein "Zeiger", der auf einen beliebigen Typ zeigen kann. Sein Typ ist *ArbitraryType. In Kombination mit uintptr entfaltet sich die wahre Leistungsfähigkeit des unsafe Pakets. In der offiziellen Dokumentation wird beschrieben, dass der unsafe.Pointer-Typ vier spezielle Operationen durchführen kann:

  • Jeder Zeigertyp kann in unsafe.Pointer konvertiert werden
  • unsafe.Pointer kann in jeden Zeigertyp konvertiert werden
  • uintptr kann in unsafe.Pointer konvertiert werden
  • unsafe.Pointer kann in uintptr konvertiert werden

Diese vier speziellen Operationen bilden das Fundament des gesamten unsafe Pakets. Erst durch diese Operationen ist es möglich, Code zu schreiben, der das Typsystem ignoriert und direkt Speicher liest und schreibt. Es wird empfohlen, bei der Verwendung besonders vorsichtig zu sein.

TIP

unsafe.Pointer kann nicht dereferenziert werden, ebenso kann nicht die Adresse davon genommen werden.

(1) *T1 in unsafe.Pointer und dann in *T2 konvertieren

Gegeben seien die Typen *T1 und *T2. Angenommen, T2 ist nicht größer als T1 und beide haben ein äquivalentes Speicherlayout, dann ist es erlaubt, Daten vom Typ T2 in T1 zu konvertieren. Zum Beispiel:

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

Diese beiden Funktionen sind tatsächlich Teil des math-Pakets. Die Typänderungen im Prozess sind wie folgt:

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

(2) unsafe.Pointer in uintptr konvertieren

Beim Konvertieren von unsafe.Pointer in uintptr wird die Adresse, auf die Ersteres zeigt, als Wert Letzterer. uintptr speichert die Adresse. Der Unterschied ist, dass Ersteres syntaktisch ein Zeiger ist, eine Referenz, während Letzteres nur ein Integer-Wert ist. Zum Beispiel:

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

Ein größerer Unterschied liegt in der Behandlung durch den Garbage Collector. Da unsafe.Pointer eine Referenz ist, wird sie bei Bedarf nicht vom Garbage Collector eingesammelt. Letzteres ist nur ein Wert und hat diese Sonderbehandlung natürlich nicht. Ein weiterer zu beachtender Punkt ist, dass wenn sich die Adresse des Elements ändert, auf das der Zeiger zeigt, der GC die alte Adresse der Zeigerreferenz aktualisiert, aber nicht den in uintptr gespeicherten Wert. Der folgende Code könnte beispielsweise Probleme verursachen:

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

In einigen Situationen, nachdem der GC die Variable verschoben hat, ist die Adresse, auf die address zeigt, bereits ungültig. Wenn dann versucht wird, einen Zeiger mit diesem Wert zu erstellen, wird eine panic ausgelöst:

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

Es wird daher nicht empfohlen, den Wert von Pointer nach der Konvertierung in uintptr zu speichern.

(3) Durch uintptr in unsafe.Pointer konvertieren

Auf folgende Weise kann ein Zeiger durch uintptr erhalten werden. Solange der Zeiger gültig ist, tritt das Problem der ungültigen Adresse aus Beispiel 2 nicht auf. Pointer und Typzeiger selbst unterstützen keine Zeigerarithmetik, aber uintptr ist nur ein Integer-Wert, mit dem mathematische Operationen durchgeführt werden können. Nach der mathematischen Operation auf uintptr und der Konvertierung in Pointer kann Zeigerarithmetik durchgeführt werden.

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

Auf diese Weise kann nur mit einem Zeiger auf interne Elemente einiger Typen zugegriffen werden, wie Arrays und Strukturen, unabhängig davon, ob ihre internen Elemente öffentlich sind. Zum Beispiel:

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 gibt ein mit dem Offset len aktualisiertes Pointer zurück, äquivalent zu Pointer(uintptr(ptr) + uintptr(len)):

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

Zum Beispiel:

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

Diese Funktion empfängt ein Slice und gibt die Startadresse des zugrundeliegenden Arrays zurück. Ohne SliceData kann die Adresse des zugrundeliegenden Arrays nur durch die Adresse des ersten Elements ermittelt werden:

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

Natürlich kann auch durch den Typ reflect.SliceHeader darauf zugegriffen werden, aber in der Version 1.20 wurde er als veraltet markiert. SliceData wurde als Ersatz eingeführt. Ein Beispiel mit 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

Die Slice-Funktion empfängt einen Zeiger und einen Längen-Offset. Sie gibt eine Slice-Repräsentation dieses Speicherbereichs zurück. Dabei findet keine Speicherkopie statt. Änderungen am Slice wirken sich direkt auf die Daten an dieser Adresse aus und umgekehrt. Sie wird normalerweise zusammen mit SliceData verwendet.

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]

Das Ändern der Daten im numsRef1-Slice führt dazu, dass sich auch die Daten in nums ändern.

StringData

func StringData(str string) *byte

Ähnlich wie die SliceData-Funktion, aber da die Konvertierung von Strings in Byte-Slices häufiger vorkommt, wurde sie separat aufgeführt. Ein Anwendungsbeispiel:

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

Da String-Literale im Nur-Lese-Segment des Prozesses gespeichert werden, wird das Programm sofort abstürzen und fatal melden, wenn Sie versuchen, die zugrundeliegenden Daten des Strings hier zu ändern. Für String-Variablen, die auf dem Heap oder Stack gespeichert sind, ist es jedoch zur Laufzeit vollständig möglich, ihre zugrundeliegenden Daten zu ändern.

String

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

Ähnlich wie die Slice-Funktion. Sie empfängt einen Zeiger auf Byte-Typ und einen Längen-Offset und gibt dessen String-Repräsentation zurück. Dabei findet keine Speicherkopie statt. Hier ist ein Beispiel für die Konvertierung eines Byte-Slices in einen String:

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

StringData und String führen bei der Konvertierung zwischen Strings und Byte-Slices keine Speicherkopie durch, was performanter ist als eine direkte Typkonvertierung. Sie sollten jedoch nur für Nur-Lese-Situationen verwendet werden. Wenn Sie planen, die Daten zu ändern, sollten Sie diese besser nicht verwenden.

Golang by www.golangdev.cn edit