《Go語(yǔ)言四十二章經(jīng)》第二十八章 unsafe包

《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)型。

  1. Alignof返回變量對(duì)齊字節(jié)數(shù)量
  2. Offsetof返回變量指定屬性的偏移量,所以如果變量是一個(gè)struct類(lèi)型,不能直接將這個(gè)struct類(lèi)型的變量當(dāng)作參數(shù),只能將這個(gè)struct類(lèi)型變量的屬性當(dāng)作參數(shù)。
  3. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay閱讀 6,474評(píng)論 1 5
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,051評(píng)論 0 9
  • 一 云,本貧州山阡人,出生時(shí),正黃昏,天雨,云蓋霞光,嬰啼,云飛,放晴,眾人驚,遂名云。 年幼,好動(dòng),常奔山野,樹(shù)...
    抓星星的小超閱讀 570評(píng)論 0 1
  • 標(biāo)兵校園創(chuàng)建中,工作壓頭難放松; 各級(jí)指標(biāo)很詳明,忙把痕跡資料尋; 回想每次來(lái)迎檢,團(tuán)結(jié)協(xié)作效果顯; 意識(shí)提升是關(guān)...
    豪甯爸爸閱讀 286評(píng)論 0 4

友情鏈接更多精彩內(nèi)容