Golang 語言中的非類型安全指針
01
介紹
Golang 語言中的 unsafe 包中包含的操作會繞過 Golang 程序的類型安全檢查,直接操作內存,從而達到提升性能的目的。導入 unsafe 包可能是不可移植(non-portable)的(隨着 Golang 的版本迭代,可能會失效),並且不受 Go 1 兼容性準則的保護,所以我們應該謹慎使用。
本文主要介紹 unsafe 包的 unsafe.Pointer
,它表示任意類型的指針,它類似於 C 語言中的無類型指針 void*
,可以作爲指針類型 *T
和 uintptr 類型值之間互相轉換的中轉站。
我們知道 Golang 語言中的指針類型 *T
,表示一個指向 T 類型變量的指針,因爲 Golang 語言是強類型的靜態語言,爲了安全考慮,規定兩個不同的指針類型之間不可以互相轉換,比如 *int
不能與 *float64
互相轉換。但是,實際上是可以使用 unsafe.Pointer
進行轉換。
Golang 語言中的內置數據類型 uintptr
也可以表示任何指針,它實際是數值類型,可以用於存儲內存地址。它和 unsafe.Pointer
最大的區別是 unsafe.Pointer
不支持指針運算,比如 +
運算符,但 uintptr
可以支持。以下是 uintptr
的源碼:
1// uintptr is an integer type that is large enough to hold the bit pattern of
2// any pointer.
3type uintptr uintptr
4
5
02
unsafe.Ponter
類型
有了前面內容的鋪墊,我們開始介紹 unsafe.Ponter
,它表示指向任意類型的指針。以下是 unsafe 包的源碼:
1// ArbitraryType is here for the purposes of documentation only and is not actually
2// part of the unsafe package. It represents the type of an arbitrary Go expression.
3type ArbitraryType int
4type Pointer *ArbitraryType
5
6
unsafe.Ponter
類型有四個轉換規則:
-
任何類型的指針值
*T
都可以轉換爲unsafe.Pointer
。 -
unsafe.Pointer
可以轉換爲任何類型的指針值*T
。 -
uintptr 可以轉換爲
unsafe.Pointer
。 -
unsafe.Pointer
可以轉換爲 uintptr。
unsafe.Pointer
允許程序繞過類型安全檢查讀寫任意內存,所以使用時應格外小心。
unsafe.Pointer
包含 6 個使用模式:
-
使用
unsafe.Pointer
作爲中轉,將一個指針類型*T
轉換爲另外一個指針類型*T
。 -
將
unsafe.Pointer
轉換爲 uintptr(但不返回給unsafe.Pointer
),然後使用 uintptr 值。 -
將
unsafe.Pointer
轉換爲 uintptr,然後使用 uintptr 值進行算術運算,最後將運算結果 uintptr 值再轉換爲unsafe.Pointer
。 -
調用
syscall.Syscall
時,將unsafe.Pointer
轉換爲 uintptr 值,作爲參數傳遞。 -
將
reflect.Value.Pointer
或reflect.Value.UnsafeAddr
的返回結果 uintptr 值,從 uintptr 轉換爲unsafe.Pointer
。 -
將
reflect.SliceHeader
或reflect.StringHeader
值的 Data 字段與unsafe.Pointer
進行轉換。
03
unsafe.Pointer
和 uintptr 使用示例
因爲 unsafe.Pointer
不支持運算,所以如果需要指針運算,還需要藉助 uintptr 實現。以下示例是通過指針偏移對 struct 結構體中的字段進行指針運算操作,從而找到該字段的內存地址。
1package main
2
3import (
4 "fmt"
5 "unsafe"
6)
7
8type user struct {
9 name string
10 age uint
11}
12func main () {
13 // 定義一個指針變量
14 student := new(user)
15 // user 結構體中的 name 字段是第一個字段,可以直接通過指針修改,不需要使用偏移
16 studentName := (*string)(unsafe.Pointer(student))
17 *studentName = "lucy"
18 // user 結構體中的 age 字段不是第一個字段,所以需要使用偏移才能找到 age 字段的內存地址,修改值
19 studentAge := (*uint)(unsafe.Pointer(uintptr(unsafe.Pointer(student)) + unsafe.Offsetof(student.age)))
20 *studentAge = 18
21 fmt.Println(*student)
22}
23
24
閱讀上面這段代碼,我們使用 new 函數定義了一個 *user
類型的指針變量 student,然後使用 unsafe.Pointer
將 *user
類型的指針變量 student 轉換爲 *string
類型的指針變量 studentName,然後修改 studentName 的值,實際上就是修改 name 字段的值。
因爲 age 字段不是 user 結構體的第一個字段,所以需要先使用偏移量找到 age 字段的內存地址,具體操作步驟是:先將 student 指針變量通過 unsafe.Pointer
和 uintptr 轉換爲 uintptr,然後就可以使用函數 unsafe.Offsetof
計算出 age 字段的偏移量,該函數返回結果也是 uintptr 類型,因爲 uintptr 支持運算,最後使用 +
運算符獲取 age 字段的內存地址。
找到 age 字段的內存地址之後,還要使用 unsafe.Pointer
轉換爲 *uint
指針類型,纔可以對該內存地址進行讀寫操作。
該示例主要是爲了方便介紹 uintptr 指針運算,沒有實際意義。
04
總結
本文介紹了非類型安全指針,它可用於指針類型之間互相轉換,但是它繞開了類型安全檢查,同時隨着 Golang 的版本迭代,unsafe 包可能會失效,並且 unsafe 包不受 Go 1 兼容性準則的保護,所以我們應該謹慎使用。
參考資料:
https://golang.org/pkg/unsafe/#Pointer
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/MvULt7x0m4IBmz1bNzLvCQ