如果你看go的源碼,尤其是runtime的部分的源碼,你一定經常會發現unsafe.Pointer和uintptr這兩個函數,例如下面就是runtime裏面的map源碼實現裏面的一個函數:
func (b *bmap) overflow(t *maptype) *bmap {
return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))
}
那麼這兩個方法有什麼用呢?下面我們來重點介紹一下。
Go中的指針及與指針對指針的操作主要有以下三種:
一普通的指針類型,例如 var intptr *T,定義一個T類型指針變量。
二內置類型uintptr,本質是一個無符號的整型,它的長度是跟平臺相關的,它的長度可以用來保存一個指針地址。
三是unsafe包提供的Pointer,表示可以指向任意類型的指針。
1.普通的指針類型
count := 1
Counter(&count)
fmt.Println(count)
func Counter(count *int) {
*count++
}
普通指針可以通過引用來修改變量的值,這個跟C語言指針有點像。
2.uintptr類型
uintptr用來進行指針計算,因爲它是整型,所以很容易計算出下一個指針所指向的位置。uintptr在builtin包中定義,定義如下:
// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
// uintptr是一個能足夠容納指針位數大小的整數類型
type uintptr uintptr
雖然uintpr保存了一個指針地址,但它只是一個值,不引用任何對象。因此使用的時候要注意以下情況:
1.如果uintptr地址相關聯對象移動,則其值也不會更新。例如goroutine的堆棧信息發生變化
2.uintptr地址關聯的對象可以被垃圾回收。GC不認爲uintptr是活引用,因此unitptr地址指向的對象可以被垃圾收集。
一個uintptr可以被轉換成unsafe.Pointer,同時unsafe.Pointer也可以被轉換爲uintptr。可以使用使用uintptr + offset計算出地址,然後使用unsafe.Pointer進行轉換,格式如下:p = unsafe.Pointer(uintptr(p) + offset)
n := 10
b := make([]int, n)
for i:= 0;i< n;i++ {
b[i] = i
}
fmt.Println(b)
// [0 1 2 3 4 5 6 7 8 9]
// 取slice的最後的一個元素
end := unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + 9 * unsafe.Sizeof(b[0]))
// 等價於unsafe.Pointer(&b[9])
fmt.Println(*(*int)(end))
// 9
3.unsafe.Pointer
unsafe.Pointer是特別定義的一種指針類型,它可以包含任意類型變量的地址。Pointer在unsafe包中定義,定義如下:
package unsafe
// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
// ArbitraryType在這裏不是unsafe包的實際的一部分,僅僅是爲了文檔記錄
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
官方文檔unsafe.Pointer的使用有以下說明:
Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types: // Pointer代表了一個任意類型的指針。Pointer類型有四種特殊的操作是其他類型不能使用的: - A pointer value of any type can be converted to a Pointer. // 任意類型的指針可以被轉換爲Pointer - A Pointer can be converted to a pointer value of any type. // Pointer可以被轉換爲任務類型的值的指針 - A uintptr can be converted to a Pointer. // uintptr可以被轉換爲Pointer - A Pointer can be converted to a uintptr. // Pointer可以被轉換爲uintptr Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care. // 因此Pointer允許程序不按類型系統的要求來讀寫任意的內存,應該非常小心地使用它。
所以unsafe.Pointer做的主要是用來進行橋接,用於不同類型的指針進行互相轉換。
在任何情況下,結果都必須繼續指向原分配的對象。
4.unsafe.Pointer,uintptr與普通指針的互相轉換
- unsafe.Pointer和普通指針的相互轉換
var f float64 = 1.0 fmt.Println(Float64bits(f)) // 4607182418800017408 func Float64bits(f float64) uint64 { return *((*uint64)(unsafe.Pointer(&f))) }
藉助unsafe.Pointer指針,實現float64轉換爲uint64類型。當然,我們不可以直接通過
*p
來獲取unsafe.Pointer指針指向的真實變量的值,因爲我們並不知道變量的具體類型。另外一個重要的要注意的是,在進行普通類型轉換的時候,要注意轉換的前後的類型要有相同的內存佈局,下面兩個結構也能完成轉換,就因爲他們有相同的內存佈局type s1 struct { id int name string } type s2 struct { field1 *[5]byte filed2 int } b := s1{name:"123"} var j s2 j = *(*s2)(unsafe.Pointer(&b)) fmt.Println(j)
- unsafe.Pointer和uintrptr的互相轉換及配合
uintptr類型的主要是用來與unsafe.Pointer配合使用來訪問和操作unsafe的內存。unsafe.Pointer不能執行算術操作。要想對指針進行算術運算必須這樣來做:
1.將unsafe.Pointer轉換爲uintptr
2.對uintptr執行算術運算
3.將uintptr轉換回unsafe.Pointer,然後訪問uintptr地址指向的對象
需要小心的是,上面的步驟對於垃圾收集器來說應該是原子的,否則可能會導致問題。
例如,在第1步之後,引用的對象可能被收集。如果在步驟3之後發生這種情況,指針將是一個無效的Go指針,並可能導致程序崩潰
package main
import (
"fmt"
"unsafe"
)
type Person struct {
age int
name string
}
func main() {
p := &Person{age: 30, name: "Bob"}
//獲取到struct s中b字段的地址
p := unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.name))
//將其轉換爲一個string的指針,並且打印該指針的對應的值
fmt.Println(*(*string)(p))
}