unsafe.Pointer其實就是類似C的void *,在golang中是用於各種指標相互轉換的橋樑。uintptr是golang的內建類型,是能儲存指標的整型,uintptr的底層類型是int,它和unsafe.Pointer可相互轉換。uintptr和unsafe.Pointer的區別就是:unsafe.Pointer只是單純的通用指標類型,用於轉換不同類型指標,它不可以參與指標運算;而uintptr是用於指標運算的,GC 不把 uintptr 當指標,也就是說 uintptr 無法持有對象,uintptr類型的目標會被回收。golang的unsafe包很強大,基本上很少會去用它。它可以像C一樣去操作記憶體,但由於golang不支援直接進行指標運算,所以用起來稍顯麻煩。
切入正題。利用unsafe包,可操作私人變數(在golang中稱為“未匯出變數”,變數名以小寫字母開始),下面是具體例子。
在$GOPATH/src下建立poit包,並在poit下建立子包p,目錄結構如下:
$GOPATH/src
----poit
--------p
------------v.go
--------main.go
以下是v.go的代碼:
package pimport ( "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 mainimport ( "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 pimport ( "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 mainimport ( "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))))}
關於填充,FastCGI協議就用到了。