Go 普通指針類型、unsafe.Pointer、uintptr之間的關系



Golang指針

  • *類型:普通指針類型,用於傳遞對象地址,不能進行指針運算。
  • unsafe.Pointer:通用指針類型,用於轉換不同類型的指針,不能進行指針運算,不能讀取內存存儲的值(必須轉換到某一類型的普通指針)。
  • uintptr:用於指針運算,GC 不把 uintptr 當指針,uintptr 無法持有對象。uintptr 類型的目標會被回收。

unsafe.Pointer 是橋梁,可以讓任意類型的指針實現相互轉換,也可以將任意類型的指針轉換為 uintptr 進行指針運算。
unsafe.Pointer 不能參與指針運算,比如你要在某個指針地址上加上一個偏移量,Pointer是不能做這個運算的,那么誰可以呢?

就是uintptr類型了,只要將Pointer類型轉換成uintptr類型,做完加減法后,轉換成Pointer,通過*操作,取值,修改值,隨意。

 總結:unsafe.Pointer 可以讓你的變量在不同的普通指針類型轉來轉去,也就是表示為任意可尋址的指針類型。而 uintptr 常用於與 unsafe.Pointer 打配合,用於做指針運算。

 

unsafe.Pointer

unsafe.Pointer稱為通用指針,官方文檔對該類型有四個重要描述:
(1)任何類型的指針都可以被轉化為Pointer
(2)Pointer可以被轉化為任何類型的指針
(3)uintptr可以被轉化為Pointer
(4)Pointer可以被轉化為uintptr
unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的void類型的指針),在golang中是用於各種指針相互轉換的橋梁,它可以包含任意類型變量的地址。
當然,我們不可以直接通過*p來獲取unsafe.Pointer指針指向的真實變量的值,因為我們並不知道變量的具體類型。
和普通指針一樣,unsafe.Pointer指針也是可以比較的,並且支持和nil常量比較判斷是否為空指針。

 

uintptr

uintptr是一個整數類型。

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

 

即使uintptr變量仍然有效,由uintptr變量表示的地址處的數據也可能被GC回收,這個需要注意!。

 

unsafe包

unsafe包只有兩個類型,三個函數,但是功能很強大。

type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

ArbitraryType是int的一個別名,在Go中對ArbitraryType賦予特殊的意義。代表一個任意Go表達式類型。
Pointer是int指針類型的一個別名,在Go中可以把Pointer類型,理解成任何指針的父類型。

三個函數的參數均是ArbitraryType類型,就是接受任何類型的變量。

unsafe.Sizeof接受任意類型的值(表達式),返回其占用的字節數,這和c語言里面不同,c語言里面sizeof函數的參數是類型,而這里是一個表達式,比如一個變量。
unsafe.Offsetof:返回結構體中元素所在內存的偏移量。
Alignof返回變量對齊字節數量Offsetof返回變量指定屬性的偏移量,這個函數雖然接收的是任何類型的變量,但是有一個前提,就是變量要是一個struct類型,且還不能直接將這個struct類型的變量當作參數,只能將這個struct類型變量的屬性當作參數。

關於這三個函數和內存對齊可以看看這篇文章:go語言內存對齊

unsafe.pointer用於普通指針類型轉換

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {

    v1 := uint(12)
    v2 := int(13)

    fmt.Println(reflect.TypeOf(v1)) //uint
    fmt.Println(reflect.TypeOf(v2)) //int

    fmt.Println(reflect.TypeOf(&v1)) //*uint
    fmt.Println(reflect.TypeOf(&v2)) //*int

    p := &v1
    p = (*uint)(unsafe.Pointer(&v2)) //使用unsafe.Pointer進行類型的轉換

    fmt.Println(reflect.TypeOf(p)) // *unit
    fmt.Println(*p) //13
}

unsafe.pointer用於訪問操作結構體的私有變量

利用unsafe包,可操作私有變量(在golang中稱為“未導出變量”,變量名以小寫字母開始),下面是具體例子。

在$GOPATH/src下建立poit包,並在poit下建立子包p,目錄結構如下:

$GOPATH/src

----poit

--------p

------------v.go
--------main.go

以下是v.go的代碼:

package p

import (
    "fmt"
)

type V struct {
    i int32
    j int64
}

func (this V) PutI() {
    fmt.Printf("i=%d\n", this.i)
}

func (this V) PutJ() {
    fmt.Printf("j=%d\n", this.j)
}

意圖很明顯,我是想通過unsafe包來實現對V的成員i和j賦值,然后通過PutI()和PutJ()來打印觀察輸出結果。

以下是main.go源代碼:

package main

import (

    "poit/p"
    "unsafe"
)

func main() {
    var v *p.V = new(p.V)
    var i *int32 = (*int32)(unsafe.Pointer(v))
    *i = int32(98)
    var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
    *j = int64(763)
    v.PutI()
    v.PutJ()
} 

當然會有些限制,比如需要知道結構體V的成員布局,要修改的成員大小以及成員的偏移量。我們的核心思想就是:結構體的成員在內存中的分配是一段連續的內存,結構體中第一個成員的地址就是這個結構體的地址,您也可以認為是相對於這個結構體偏移了0。相同的,這個結構體中的任一成員都可以相對於這個結構體的偏移來計算出它在內存中的絕對地址。

具體來講解下main方法的實現:

var v *p.V = new(p.V)

 

new是golang的內置方法,用來分配一段內存(會按類型的零值來清零),並返回一個指針。所以v就是類型為p.V的一個指針。

var i *int32 = (*int32)(unsafe.Pointer(v))

 

將指針v轉成通用指針,再轉成int32指針。這里就看到了unsafe.Pointer的作用了,您不能直接將v轉成int32類型的指針,那樣將會panic。剛才說了v的地址其實就是它的第一個成員的地址,所以這個i就很顯然指向了v的成員i,通過給i賦值就相當於給v.i賦值了,但是別忘了i只是個指針,要賦值得解引用。

*i = int32(98) 

現在已經成功的改變了v的私有成員i的值,好開心^_^

但是對於v.j來說,怎么來得到它在內存中的地址呢?其實我們可以獲取它相對於v的偏移量(unsafe.Sizeof可以為我們做這個事),但我上面的代碼並沒有這樣去實現。各位別急,一步步來。

var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))

其實我們已經知道v是有兩個成員的,包括i和j,並且在定義中,i位於j的前面,而i是int32類型,也就是說i占4個字節。所以j是相對於v偏移了4個字節。您可以用uintptr(4)或uintptr(unsafe.Sizeof(int32(0)))來做這個事。unsafe.Sizeof方法用來得到一個值應該占用多少個字節空間。注意這里跟C的用法不一樣,C是直接傳入類型,而golang是傳入值。之所以轉成uintptr類型是因為需要做指針運算。v的地址加上j相對於v的偏移地址,也就得到了v.j在內存中的絕對地址,別忘了j的類型是int64,所以現在的j就是一個指向v.j的指針,接下來給它賦值:

*j = int64(763)

好吧,現在貌視一切就緒了,來打印下:

v.PutI()
v.PutJ()

如果您看到了正確的輸出,那恭喜您,您做到了!

但是,別忘了上面的代碼其實是有一些問題的,您發現了嗎?

在p目錄下新建w.go文件,代碼如下:

package p

import (
    "fmt"
    "unsafe"
)

type W struct {
    b byte
    i int32
    j int64
}

func init() {
    var w *W = new(W)
    fmt.Printf("size=%d\n", unsafe.Sizeof(*w))
}

需要修改main.go的代碼嗎?不需要,我們只是來測試一下。w.go里定義了一個特殊方法init,它會在導入p包時自動執行,別忘了我們有在main.go里導入p包。每個包都可定義多個init方法,它們會在包被導入時自動執行(在執行main方法前被執行,通常用於初始化工作),但是,最好在一個包中只定義一個init方法,否則您或許會很難預期它的行為)。我們來看下它的輸出:

size=16

等等,好像跟我們想像的不一致。來手動計算一下:b是byte類型,占1個字節;i是int32類型,占4個字節;j是int64類型,占8個字節,1+4+8=13。這是怎么回事呢?這是因為發生了對齊。在struct中,它的對齊值是它的成員中的最大對齊值。每個成員類型都有它的對齊值,可以用unsafe.Alignof方法來計算,比如unsafe.Alignof(w.b)就可以得到b在w中的對齊值。同理,我們可以計算出w.b的對齊值是1,w.i的對齊值是4,w.j的對齊值也是4。如果您認為w.j的對齊值是8那就錯了,所以我們前面的代碼能正確執行(試想一下,如果w.j的對齊值是8,那前面的賦值代碼就有問題了。也就是說前面的賦值中,如果v.j的對齊值是8,那么v.i跟v.j之間應該有4個字節的填充。所以得到正確的對齊值是很重要的)。對齊值最小是1,這是因為存儲單元是以字節為單位。所以b就在w的首地址,而i的對齊值是4,它的存儲地址必須是4的倍數,因此,在b和i的中間有3個填充,同理j也需要對齊,但因為i和j之間不需要填充,所以w的Sizeof值應該是13+3=16。如果要通過unsafe來對w的三個私有成員賦值,b的賦值同前,而i的賦值則需要跳過3個字節,也就是計算偏移量的時候多跳過3個字節,同理j的偏移可以通過簡單的數學運算就能得到。
比如也可以通過unsafe來靈活取值:

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var b []byte = []byte{'a', 'b', 'c'}
    var c *byte = &b[0]
    fmt.Println(*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(c)) + uintptr(1))))
}

下面這種寫法是錯的(不要試圖引入一個uintptr類型的臨時變量,因為它可能會破壞代碼的安全性(注:這是真正可以體會unsafe包為何不安全的例子)):

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(c)) + uintptr(1)
pb := (*byte)(unsafe.Pointer(tmp))
*pb = 'd'

產生錯誤的原因很微妙。**有時候垃圾回收器會移動一些變量以降低內存碎片等問題。這類垃圾回收器被稱為移動GC。當一個變量被移動,所有的保存改變量舊地址的指針必須同時被更新為變量移動后的新地址。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針,因此當變量被移動是對應的指針也必須被更新;但是uintptr類型的臨時變量只是一個普通的數字,所以其值不應該被改變。上面錯誤的代碼因為引入一個非指針的臨時變量tmp,導致垃圾收集器無法正確識別這個是一個指向變量x的指針。當第二個語句執行時,變量x可能已經被轉移,這時候臨時變量tmp也就不再是現在的&x.b地址。**第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序!

refer:

Go之unsafe.Pointer && uintptr 類型

Go語言 unsafe的妙用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM