《Go語(yǔ)言四十二章經(jīng)》第二十八章 unsafe包
作者:李驍
28.1 unsafe 包
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType int
type Pointer *ArbitraryType
在unsafe包中,只提供了3個(gè)函數(shù),兩個(gè)類(lèi)型。就這么少的量,卻有著超級(jí)強(qiáng)悍的功能。一般我們?cè)贑語(yǔ)言中通過(guò)指針,在知道變量在內(nèi)存中占用的字節(jié)數(shù)情況下,就可以通過(guò)指針加偏移量的操作,直接在地址中,修改,訪問(wèn)變量的值。在Go 語(yǔ)言中不支持指針運(yùn)算,那怎么辦呢?其實(shí)通過(guò)unsafe包,我們可以完成類(lèi)似的操作。
ArbitraryType 是以int為基礎(chǔ)定義的一個(gè)新類(lèi)型,但是Go 語(yǔ)言u(píng)nsafe包中,對(duì)ArbitraryType賦予了特殊的意義,通常,我們把interface{}看作是任意類(lèi)型,那么ArbitraryType這個(gè)類(lèi)型,在Go 語(yǔ)言系統(tǒng)中,比interface{}還要隨意。
Pointer 是ArbitraryType指針類(lèi)型為基礎(chǔ)的新類(lèi)型,在Go 語(yǔ)言系統(tǒng)中,可以把Pointer類(lèi)型,理解成任何指針的親爹。
Go 語(yǔ)言的指針類(lèi)型長(zhǎng)度與int類(lèi)型長(zhǎng)度,在內(nèi)存中占用的字節(jié)數(shù)是一樣的。ArbitraryType類(lèi)型的變量也可以是指針。
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
通過(guò)分析發(fā)現(xiàn),這三個(gè)函數(shù)的參數(shù)均是ArbitraryType類(lèi)型。
- Alignof返回變量對(duì)齊字節(jié)數(shù)量
- Offsetof返回變量指定屬性的偏移量,所以如果變量是一個(gè)struct類(lèi)型,不能直接將這個(gè)struct類(lèi)型的變量當(dāng)作參數(shù),只能將這個(gè)struct類(lèi)型變量的屬性當(dāng)作參數(shù)。
- Sizeof 返回變量在內(nèi)存中占用的字節(jié)數(shù),切記,如果是slice,則不會(huì)返回這個(gè)slice在內(nèi)存中的實(shí)際占用長(zhǎng)度。
unsafe中,通過(guò)ArbitraryType 、Pointer 這兩個(gè)類(lèi)型,可以將其他類(lèi)型都轉(zhuǎn)換過(guò)來(lái),然后通過(guò)這三個(gè)函數(shù),分別能取長(zhǎng)度,偏移量,對(duì)齊字節(jié)數(shù),就可以在內(nèi)存地址映射中,來(lái)回游走。
28.2 指針運(yùn)算
uintptr這個(gè)基礎(chǔ)類(lèi)型,在Go 語(yǔ)言中,字節(jié)長(zhǎng)度是與int一致。通常Pointer不能參與指針運(yùn)算,比如你要在某個(gè)指針地址上加上一個(gè)偏移量,Pointer是不能做這個(gè)運(yùn)算的,那么誰(shuí)可以呢?這里要靠uintptr類(lèi)型了,只有將Pointer類(lèi)型先轉(zhuǎn)換成uintptr類(lèi)型,做完地址加減法運(yùn)算后,再轉(zhuǎn)換成Pointer類(lèi)型,通過(guò)*操作達(dá)到取值、修改值的目的。
unsafe.Pointer其實(shí)就是類(lèi)似C的void *,在Go 語(yǔ)言中是用于各種指針相互轉(zhuǎn)換的橋梁,也即是通用指針。它可以讓任意類(lèi)型的指針實(shí)現(xiàn)相互轉(zhuǎn)換,也可以將任意類(lèi)型的指針轉(zhuǎn)換為 uintptr 進(jìn)行指針運(yùn)算。
uintptr是Go 語(yǔ)言的內(nèi)置類(lèi)型,是能存儲(chǔ)指針的整型, uintptr 的底層類(lèi)型是int,它和unsafe.Pointer可相互轉(zhuǎn)換。
uintptr和unsafe.Pointer的區(qū)別就是:
unsafe.Pointer只是單純的通用指針類(lèi)型,用于轉(zhuǎn)換不同類(lèi)型指針,它不可以參與指針運(yùn)算;
而uintptr是用于指針運(yùn)算的,GC 不把 uintptr 當(dāng)指針,也就是說(shuō) uintptr 無(wú)法持有對(duì)象, uintptr 類(lèi)型的目標(biāo)會(huì)被回收;
unsafe.Pointer 可以和 普通指針 進(jìn)行相互轉(zhuǎn)換;
unsafe.Pointer 可以和 uintptr 進(jìn)行相互轉(zhuǎn)換。
Go 語(yǔ)言的unsafe包很強(qiáng)大,基本上很少會(huì)去用它。它可以像C一樣去操作內(nèi)存,但由于Go 語(yǔ)言不支持直接進(jìn)行指針運(yùn)算,所以用起來(lái)稍顯麻煩。
uintptr和intptr是無(wú)符號(hào)和有符號(hào)的指針類(lèi)型,并且確保在64位平臺(tái)上是8個(gè)字節(jié),在32位平臺(tái)上是4個(gè)字節(jié),uintptr主要用于Go 語(yǔ)言中的指針運(yùn)算。
通過(guò)unsafe包來(lái)實(shí)現(xiàn)對(duì)V的成員i和j賦值,然后通過(guò)GetI()和GetJ()來(lái)打印觀察輸出結(jié)果。
以下是main.go源代碼:
package main
import (
"fmt"
"unsafe"
)
type V struct {
i int32
j int64
}
func (v V) GetI() {
fmt.Printf("i=%d\n", v.i)
}
func (v V) GetJ() {
fmt.Printf("j=%d\n", v.j)
}
func main() {
// 定義指針類(lèi)型變量
var v *V = &V{199, 299}
// 取得v的指針并轉(zhuǎn)為*int32的值,對(duì)應(yīng)結(jié)構(gòu)體的i。
var i *int32 = (*int32)(unsafe.Pointer(v))
fmt.Println("指針地址:", i)
fmt.Println("指針uintptr值:", uintptr(unsafe.Pointer(i)))
*i = int32(98)
// 根據(jù)v的基準(zhǔn)地址加上偏移量進(jìn)行指針運(yùn)算,運(yùn)算后的值為j的地址,使用unsafe.Pointer轉(zhuǎn)為指針
var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))
*j = int64(763)
v.GetI()
v.GetJ()
}
指針地址: 0xc00000c180
指針uintptr值: 824633770368
i=98
j=763
要修改struct字段的值,需要提前知道結(jié)構(gòu)體V的成員布局,然后根據(jù)字段計(jì)算偏移量,以及考慮對(duì)齊值,最后通過(guò)指針運(yùn)算得到成員指針,利用指針達(dá)到修改成員值得目的。由于結(jié)構(gòu)體的成員在內(nèi)存中的分配是一段連續(xù)的內(nèi)存,因此結(jié)構(gòu)體中第一個(gè)成員的地址就是這個(gè)結(jié)構(gòu)體的地址,我們也可以認(rèn)為是相對(duì)于這個(gè)結(jié)構(gòu)體偏移了0。相同的,這個(gè)結(jié)構(gòu)體中的任一成員都可以相對(duì)于這個(gè)結(jié)構(gòu)體的偏移來(lái)計(jì)算出它在內(nèi)存中的絕對(duì)地址。
具體來(lái)講解下main方法的實(shí)現(xiàn):
var v *V = &V{199, 299}
通過(guò)&來(lái)分配一段內(nèi)存(并按類(lèi)型初始化),返回一個(gè)指針。所以v就是類(lèi)型為V的一個(gè)指針。和new函數(shù)的作用類(lèi)似。
var i *int32 = (*int32)(unsafe.Pointer(v))
將指針v轉(zhuǎn)成通用指針,再轉(zhuǎn)成int32指針類(lèi)型。這里就看到了unsafe.Pointer的作用了,您不能直接將v轉(zhuǎn)成int32類(lèi)型的指針,那樣將會(huì)panic,但是unsafe.Pointer是可以轉(zhuǎn)為任何指針。剛才說(shuō)了v的地址其實(shí)就是它的第一個(gè)成員的地址,所以這個(gè)i就很顯然指向了v的成員i,通過(guò)給i賦值就相當(dāng)于給v.i賦值了,但是別忘了i只是個(gè)指針,要賦值得解引用。
*i = int32(98)
現(xiàn)在已經(jīng)成功的改變了v的私有成員i的值。
但是對(duì)于v.j來(lái)說(shuō),怎么來(lái)得到它在內(nèi)存中的地址呢?其實(shí)我們可以獲取它相對(duì)于v的偏移量(unsafe.Sizeof可以為我們做這個(gè)事),但上面的代碼并沒(méi)有這樣去實(shí)現(xiàn)。各位別急,一步步來(lái)。
var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))
其實(shí)我們已經(jīng)知道v是有兩個(gè)成員的,包括i和j,并且在定義中,i位于j的前面,而i是int32類(lèi)型,也就是說(shuō)i占4個(gè)字節(jié)。所以j是相對(duì)于v偏移了4個(gè)字節(jié)。您可以用uintptr(4)或uintptr(unsafe.Sizeof(int64(0)))來(lái)做這個(gè)事。unsafe.Sizeof方法用來(lái)得到一個(gè)值應(yīng)該占用多少個(gè)字節(jié)空間。注意這里跟C的用法不一樣,C是直接傳入類(lèi)型,而Go 語(yǔ)言是傳入值。
之所以轉(zhuǎn)成uintptr類(lèi)型是因?yàn)樾枰鲋羔樳\(yùn)算。v的地址加上j相對(duì)于v的偏移地址,也就得到了v.j在內(nèi)存中的絕對(duì)地址,然后通過(guò)unsafe.Pointer轉(zhuǎn)為指針,別忘了j的類(lèi)型是int64,所以現(xiàn)在的j就是一個(gè)指向v.j的指針,接下來(lái)給它賦值:
*j = int64(763)
另外,我們可以看到兩種地址表示上的差異:
指針地址: 0xc00000c180
指針uintptr值: 824633770368
上面結(jié)構(gòu)體V中,定義了2個(gè)成員屬性,如果我們定義一個(gè)byte類(lèi)型的成員屬性。我們來(lái)看下它的輸出:
package main
import (
"fmt"
"unsafe"
)
type V struct {
b byte
i int32
j int64
}
func (v V) GetI() {
fmt.Printf("i=%d\n", v.i)
}
func (v V) GetJ() {
fmt.Printf("j=%d\n", v.j)
}
func main() {
// 定義指針類(lèi)型變量
var v *V = new(V)
// v的長(zhǎng)度
fmt.Printf("size=%d\n", unsafe.Sizeof(*v))
// 取得v的指針考慮對(duì)齊值計(jì)算偏移量,然后轉(zhuǎn)為*int32的值,對(duì)應(yīng)結(jié)構(gòu)體的i。
var i *int32 = (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(4*unsafe.Sizeof(byte(0)))))
fmt.Println("指針地址:", i)
fmt.Println("指針uintptr值:", uintptr(unsafe.Pointer(i)))
*i = int32(98)
// 根據(jù)v的基準(zhǔn)地址加上偏移量進(jìn)行指針運(yùn)算,運(yùn)算后的值為j的地址,使用unsafe.Pointer轉(zhuǎn)為指針
var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))
*j = int64(763)
fmt.Println("指針uintptr值:", uintptr(unsafe.Pointer(&v.b)))
fmt.Println("指針uintptr值:", uintptr(unsafe.Pointer(&v.i)))
fmt.Println("指針uintptr值:", uintptr(unsafe.Pointer(&v.j)))
v.GetI()
v.GetJ()
}
程序輸出:
size=16
指針地址: 0xc000050084
指針uintptr值: 824634048644
指針uintptr值: 824634048640
指針uintptr值: 824634048644
指針uintptr值: 824634048648
i=98
j=763
新結(jié)構(gòu)體的長(zhǎng)度為size=16,好像跟我們想像的不一致。我們計(jì)算一下:b是byte類(lèi)型,占1個(gè)字節(jié);i是int32類(lèi)型,占4個(gè)字節(jié);j是int64類(lèi)型,占8個(gè)字節(jié),1+4+8=13。這是怎么回事呢?
這是因?yàn)榘l(fā)生了對(duì)齊。在struct中,它的對(duì)齊值是它的成員中的最大對(duì)齊值。
每個(gè)成員類(lèi)型都有它的對(duì)齊值,可以用unsafe.Alignof方法來(lái)計(jì)算,比如unsafe.Alignof(v.b)就可以得到b的對(duì)齊值為1 。但這個(gè)對(duì)齊值是其值類(lèi)型的長(zhǎng)度或引用的地址長(zhǎng)度(32位或者64位),和其在結(jié)構(gòu)體中的size不是簡(jiǎn)單相加的問(wèn)題。經(jīng)過(guò)在64位機(jī)器上測(cè)試,發(fā)現(xiàn)地址(uintptr)如下:
unsafe.Pointer(b): %s 824634048640
unsafe.Pointer(i): %s 824634048644
unsafe.Pointer(j): %s 824634048648
可以初步推斷,也經(jīng)過(guò)測(cè)試驗(yàn)證,取i值使用uintptr(4*unsafe.Sizeof(byte(0)))是準(zhǔn)確的。至于size其實(shí)也和對(duì)齊值有關(guān),也不是簡(jiǎn)單相加每個(gè)字段的長(zhǎng)度。
unsafe.Offsetof 可以在實(shí)際中使用,如果改變私有的字段,需要程序員認(rèn)真考慮后,按照上面的方法仔細(xì)確認(rèn)好對(duì)齊值再進(jìn)行操作。
本書(shū)《Go語(yǔ)言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書(shū)《Go語(yǔ)言四十二章經(jīng)》內(nèi)容在簡(jiǎn)書(shū)同步地址: http://www.itdecent.cn/nb/29056963雖然本書(shū)中例子都經(jīng)過(guò)實(shí)際運(yùn)行,但難免出現(xiàn)錯(cuò)誤和不足之處,煩請(qǐng)您指出;如有建議也歡迎交流。
聯(lián)系郵箱:roteman@163.com