Skip to content

unsafe

官方文檔地址:unsafe package - unsafe - Go Packages

unsafe標准庫是官方提供的一個可以進行低級編程的庫,該包提供的操作可以直接跳過 Go 的類型系統從而讀寫內存。該包可能不具有移植性,且官方聲稱該包不受Go 1 兼容性准則的保護。即便如此,unsafe也還是被大量的項目使用,其中也包括官方提供的標准庫。

TIP

之所以不可移植的原因是一些操作的結果取決於操作系統實現,不同的系統可能會有不同的結果。

ArbitraryType

go
type ArbitraryType int

Arbitrary 可以翻譯為任意的,在這裡代表的是任意類型,且不等同於any,實際上該類型並不屬於unsafe包,出現在這裡僅僅只是為了文檔目的。

IntegerType

go
type IntegerType int

IntegerType代表的是任意整數類型,實際上該類型並不屬於unsafe包,出現在這裡僅僅只是為了文檔目的。

上面這兩個類型不需要太在意,它們僅僅只是一個代表而已,在使用unsafe包函數時編輯器甚至會提示你類型不匹配,它們的實際類型就是你傳入的具體類型。

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

以字節為單位返回變量x的大小,不包括其引用內容的大小,例如:

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

該函數用於表示結構體內字段偏移量,所以x必須是一個結構體字段,或者說返回值就是結構體地址起始處到字段地址起始處兩者之間的字節數,例如

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

如果不懂什麼是內存對齊,可以前往:Go 語言內存對齊詳解 - 掘金 (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

對齊大小通常是以字節為單位的計算機字長與Sizeof的最小值,例如在amd64的機器上,字長為 64 位,也就是 8 個字節,例如:

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是一種可以指向任意類型的"指針",其類型為*ArbitraryType,該類型與uintptr結合使用,才能真正發揮unsafe包的真正威力。在官方文檔的描述中,unsafe.Pointer類型可以進行四個特殊操作,分別是:

  • 任何類型的指針都可以轉換為unsafe.Pointer
  • unsafe.Pointer可以轉換為任何類型的指針
  • uintptr可以轉換為unsafe.Pointer
  • unsafe.Pointer可以轉換為uintptr

這四個特殊操作構成了整個unsafe包的基石,也正是這四個操作才能寫出能夠忽略類型系統從而直接讀寫內存的代碼,建議在使用時應當格外注意。

TIP

unsafe.Pointer無法解引用,同樣的也無法取地址。

(1) *T1轉換為unsafe.Pointer再轉換為*T2

現有類型*T1*T2,假設T2不大於T1並且兩者內存布局等效,就允許將一種T2類型的數據轉換為T1。例如:

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

這兩個函數實際是math包下的兩個函數,過程中的類型變化如下

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

(2) unsafe.Pointer轉換為uintptr

unsafe.Pointer轉換為uintptr時,會將前者所指向的地址作為後者的值,uintptr保存的是地址,區別在於,前者在語法上是一個指針,是一個引用,後者僅僅是一個整數值。例如

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

更大的區別在於垃圾回收的處理,由於unsafe.Pointer是一個引用,在需要的時候並不會被回收掉,而後者僅僅作為一個值,自然不會有這種特殊待遇了,另一個需要注意的點是當指針指向的元素地址移動時,GC 會去更新指針引用的舊地址,但不會去更新uinptr所保存的值。例如下面的代碼就可能會出現問題:

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

當一些情況下,GC 移動變量後,address指向的地址已經無效了,此時再使用該值去創建指針就會引發panic

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

所以並不建議保存Pointer轉換為uintptr後的值。

(3) 通過uintptr轉換為unsafe.Pointer

如下方式可以通過uintptr獲得一個指針,只要指針是有效的,那麼便不會出現例二的無效地址情況。Pointer與類型指針本身是不支持指針運算,但是uintptr只是一個整數值,可以進行數學運算,對uintptr進行數學運算後再轉換為Pointer就可以完成指針運算。

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

這樣,可以僅通過一個指針,就能訪問到一些類型的內部元素,比如數組和結構體,無論其內部元素是否對外暴露,例如

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將返回使用偏移量len更新後的Pointer,等價於Pointer(uintptr(ptr) + uintptr(len))

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

例如:

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

該函數接收一個切片,返回其底層數組的其實地址。如果不使用SliceData,那麼只能通過取其第一個元素的指針來獲取底層數組的地址,如下

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

當然也可以通過reflect.SliceHeader類型來獲取,不過在 1.20 版本以後它就已經被廢棄了,SliceData就是為了替代它的,使用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

Slice函數接收一個指針,以及長度偏移量,它會返回該段內存的切片表達形式,過程中不會涉及到內存拷貝,對切片進行修改將會直接影響該地址上的數據,反過來也是如此,它通常和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]

修改numsRef1切片的數據,會導致nums的數據也會發生變化

StringData

func StringData(str string) *byte

SliceData函數,只不過因為字符串轉字節切片需求比較頻繁,所以單獨拿出來,使用例子如下

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

由於字符串字面量是存放在進程中的只讀段,所以如果你在這裡嘗試修改字符串底層的數據,程序會直接崩掉報fatal。不過對於存放在堆棧上的字符串變量而言,在運行時修改其底層的數據是完全可行的。

String

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

Slice函數,接收一個字節類型指針,以及其長度偏移量,返回其字符串表達形式,過程中不涉及內存拷貝。下面是一個字節切片轉字符串的例子

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

StringDataString在字符串與字節切片的轉換過程中不涉及內存拷貝,性能比直接類型轉換要好,不過只適用於只讀的情況下,如果你打算修改數據,就最好別用這個。

Golang學習網由www.golangdev.cn整理維護