深入理解go unsafe用法及注意事項(xiàng)
學(xué)過 C 的朋友應(yīng)該知道,有一種類型是指針類型,指針類型存儲(chǔ)的是一個(gè)內(nèi)存地址,通過這個(gè)內(nèi)存地址可以找到它指向的變量。go 雖然是一種高級(jí)語言,但是也還是給開發(fā)者提供了指針的類型 unsafe.Pointer
,我們可以通過它來直接讀寫變量的內(nèi)存。
正因?yàn)槿绱?,如果我們操作不?dāng),極有可能會(huì)導(dǎo)致程序崩潰。今天就來了解一下 unsafe
里所能提供的關(guān)于指針的一些功能,以及使用 unsafe.Pointer
的一些注意事項(xiàng)。
內(nèi)存里面的二進(jìn)制數(shù)據(jù)表示什么?
我們知道,計(jì)算機(jī)存儲(chǔ)數(shù)據(jù)的時(shí)候是以二進(jìn)制的方式存儲(chǔ)的,當(dāng)然,內(nèi)存里面存儲(chǔ)的數(shù)據(jù)也是二進(jìn)制的。二進(jìn)制的 01 本身其實(shí)并沒有什么特殊的含義。
它們的具體含義完全取決于我們?cè)趺慈ダ斫馑鼈?,比?nbsp;0010 0000
,如果我們將其看作是一個(gè)十進(jìn)制數(shù)字,那么它就是 32,如果我們將其看作是字符,那么他就是一個(gè)空格(具體可參考 ASCII 碼表)。
對(duì)應(yīng)到編程語言層面,其實(shí)我們的變量存儲(chǔ)在內(nèi)存里面也是 01 表示的二進(jìn)制,這些二進(jìn)制數(shù)表示是什么類型都是語言層面的事,更準(zhǔn)確來說,是編譯器來處理的,我們寫代碼的時(shí)候?qū)⒆兞柯暶鳛檎麛?shù),那么我們?nèi)〕鰜淼臅r(shí)候也會(huì)表示成一個(gè)整數(shù)。
這跟本文有什么關(guān)系呢?我們下面會(huì)講到很多關(guān)于類型轉(zhuǎn)換的內(nèi)容,如果我們理解了這一節(jié)說的內(nèi)容,下面的內(nèi)容會(huì)更容易理解
在我們做類型轉(zhuǎn)換的時(shí)候,實(shí)際上底層的二進(jìn)制表示是沒有變的,變的只是我們所看到的表面的東西。
內(nèi)存布局
有點(diǎn)想直接開始講 unsafe
里的 Pointer
的,但是如果讀者對(duì)計(jì)算機(jī)內(nèi)存怎么存儲(chǔ)變量不太熟悉的話,看起來可能會(huì)比較費(fèi)解,所以在文章開頭會(huì)花比較大的篇幅來講述計(jì)算機(jī)是怎么存儲(chǔ)數(shù)據(jù)的,相信讀完會(huì)再閱讀后面的內(nèi)容(比如指針的算術(shù)運(yùn)算、通過指針修改結(jié)構(gòu)體字段)會(huì)沒有那么多障礙。
變量在內(nèi)存中是怎樣的?
我們先來看一段代碼:
package main import ( "fmt" "unsafe" ) func main() { var a int8 = 1 var b int16 = 2 // unsafe.Sizeof() 可以獲取存儲(chǔ)變量需要的內(nèi)存大小,單位為字節(jié) // 輸出:1 2 // int8 意味著,用 8 位,也就是一個(gè)字節(jié)來存儲(chǔ)整型數(shù)據(jù) // int16 意味著,用 16 位,也就是兩個(gè)字節(jié)來存儲(chǔ)整型數(shù)據(jù) fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) }
在這段代碼中我們定義了兩個(gè)變量,占用一個(gè)字節(jié)的 a
和占用兩個(gè)字節(jié)的 b
,在內(nèi)存中它們大概如下圖:
我們可以看到,在圖中,a
存儲(chǔ)在低地址,占用一個(gè)字節(jié),而 b
存儲(chǔ)在 a
相鄰的地方,占用兩個(gè)字節(jié)。
結(jié)構(gòu)體在內(nèi)存中是怎樣的?
我們?cè)賮砜纯唇Y(jié)構(gòu)體在內(nèi)存中的存儲(chǔ):
package main import ( "fmt" "unsafe" ) type Person struct { age int8 score int8 } func main() { var p Person // 輸出:2 1 1 // 意味著 p 占用兩個(gè)字節(jié), // 其中 age 占用一個(gè)字節(jié),score 占用一個(gè)字節(jié) fmt.Println(unsafe.Sizeof(p), unsafe.Sizeof(p.age), unsafe.Sizeof(p.score)) }
這段代碼中,我們定義了一個(gè) Person
結(jié)構(gòu)體,其中兩個(gè)字段 age
和 score
都是 int8
類型,都是只占用一個(gè)字節(jié)的,它的內(nèi)存布局大概如下圖:
我們可以看到,在內(nèi)存中,結(jié)構(gòu)體字段是占用了內(nèi)存中連續(xù)的一段存儲(chǔ)空間的,具體來說是占用了連續(xù)的兩個(gè)字節(jié)。
指針在內(nèi)存中是怎么存儲(chǔ)的?
在下面的代碼中,我們定義了一個(gè) a
變量,大小為 1 字節(jié),然后我們定義了一個(gè)指向 a
的指針 p
:
需要先說明的是,下面有兩個(gè)操作符,一個(gè)是 &
,這個(gè)是取地址的操作符,var p = &a
意味著,取得 a
的內(nèi)存地址,將其存儲(chǔ)在變量 p
中,另一個(gè)操作符是 *
,這個(gè)操作符的意思是解指針,*p
就是通過 p
的地址取得 p
指向的內(nèi)容(也就是 a
)然后進(jìn)行操作。
*p = 4
意味著,將 p
指向的 a
修改為 4。
package main import ( "fmt" "unsafe" ) func main() { var a int8 = 3 // ... 其他變量 var p = &a fmt.Println(unsafe.Sizeof(p)) fmt.Println(*p) // 3 *p = 4 fmt.Println(a) // 4 }
需要注意的是,這里面不再是一個(gè)單元格一個(gè)字節(jié)了,p
(指針變量)是要占用 8 個(gè)字節(jié)的(這個(gè)跟機(jī)器有關(guān),我的是 64 位的 CPU,所以是 8 個(gè)字節(jié))。
從這個(gè)圖,我們可以得知,指針實(shí)際上存儲(chǔ)的是一個(gè)內(nèi)存地址,通過這個(gè)地址我們可以找到它實(shí)際存儲(chǔ)的內(nèi)容。
結(jié)構(gòu)體的內(nèi)存布局真的是我們上面說的那樣嗎?
上面我們說了,下面這個(gè)結(jié)構(gòu)體占用了兩個(gè)字節(jié),結(jié)構(gòu)體里面的一個(gè)字段占用一個(gè)字節(jié):
type Person struct { age int8 score int8 }
然后我們?cè)賮砜纯聪旅孢@個(gè)結(jié)構(gòu)體,它會(huì)占用多少字節(jié)呢?
type Person struct { age int8 score int16 // 類型由 int8 改為了 int16 }
也許我們這個(gè)時(shí)候已經(jīng)算好了 1 + 2 = 3
,3 個(gè)字節(jié)不是嗎?說實(shí)話,真的不是,它會(huì)占用 4 個(gè)字節(jié),這可能會(huì)有點(diǎn)反常理,但是這跟計(jì)算機(jī)的體系結(jié)構(gòu)有著密切的關(guān)系,先看具體的運(yùn)行結(jié)果:
package main import ( "fmt" "unsafe" ) type Person struct { age int8 score int16 } func main() { var p Person // 輸出:4 1 2 // 意味著 p 占用 4 個(gè)字節(jié), // 其中 age 占用 2 個(gè)字節(jié),score 占用 2 個(gè)字節(jié) fmt.Println(unsafe.Sizeof(p), unsafe.Sizeof(p.age), unsafe.Sizeof(p.score)) }
為什么會(huì)這樣呢?因?yàn)?CPU 運(yùn)行的時(shí)候,需要從內(nèi)存讀取數(shù)據(jù),而從內(nèi)存取數(shù)據(jù)的過程是按字讀取的,如果我們數(shù)據(jù)的內(nèi)存沒有對(duì)齊,則可能會(huì)導(dǎo)致 CPU 本來一次可以讀取完的數(shù)據(jù)現(xiàn)在需要多次讀取,這樣就會(huì)造成效率的下降。
關(guān)于內(nèi)存對(duì)齊,是一個(gè)比較龐大的話題,這里不展開了,我們需要明確的是,go 編譯器會(huì)對(duì)我們的結(jié)構(gòu)體字段進(jìn)行內(nèi)存對(duì)齊。
內(nèi)存對(duì)我們的影響就是,它可能會(huì)導(dǎo)致結(jié)構(gòu)體所占用的空間比它字段類型所需要的空間大(所以我們做指針的算術(shù)運(yùn)算的時(shí)候需要非常注意),
具體大多少其實(shí)我們其實(shí)不需要知道,因?yàn)橛蟹椒梢灾?,哪就?nbsp;unsafe.Offsetof
,下面會(huì)說到。
uintptr 是什么意思?
在開始下文之前,還是得啰嗦一句,uintptr
這種命名方式是 C 語言里面的一種類型命名的慣例,u
前綴表示是無符號(hào)數(shù)(unsigned),ptr
是指針(pointer)的縮寫,這個(gè) uintptr
按這個(gè)命名慣例解析的話,就是一個(gè)指向無符號(hào)整數(shù)的指針。
另外,還有另外一種命名慣例,就是在整型類型的后面加上一個(gè)表示占用 bit 數(shù)的數(shù)字,(1字節(jié)=8bit)
比如 int8
表示一個(gè)占用 8 位的整數(shù),只可以存儲(chǔ) 1 個(gè)字節(jié)的數(shù)據(jù),然后 int64
表示的是一個(gè) 8 字節(jié)數(shù)(64位)。
unsafe 包定義的三個(gè)新類型
ArbitraryType
type ArbitraryType int
,這個(gè)類型實(shí)際上是一個(gè) int
類型,但是從名字上我們可以看到,它被命名為任意類型,也就是說,他會(huì)被我們用來表示任意的類型,具體怎么用,是下面說的 unsafe.Pointer
用的。
IntegerType
type IntegerType int
,它表示的是一個(gè)任意的整數(shù),在 unsafe
包中它被用來作為表示切片或者指針加減的長(zhǎng)度。
Pointer
type Pointer *ArbitraryType
,這個(gè)就是我們上一節(jié)提到的指針了,它可以指向任何類型的數(shù)據(jù)(*ArbitraryType
)。
內(nèi)存地址實(shí)際上就是計(jì)算機(jī)內(nèi)存的編號(hào),是一個(gè)整數(shù),所以我們才可以使用
int
來表示指針。
unsafe 包計(jì)算內(nèi)存的三個(gè)方法
這幾個(gè)方法在我們對(duì)內(nèi)存進(jìn)行操作的時(shí)候會(huì)非常有幫助,因?yàn)楦鶕?jù)這幾個(gè)方法,我們才可以得知底層數(shù)據(jù)類型的實(shí)際大小。
Sizeof
計(jì)算 x
所需要的內(nèi)存大?。▎挝粸樽止?jié)),如果其中包含了引用類型,Sizeof
不會(huì)計(jì)算引用指向的內(nèi)容的大小。
有幾種常見的情況(沒有涵蓋全部情況):
- 基本類型,如
int8
、int
,Sizeof
返回的是這個(gè)類型本身的大小,如unsafe.Sizeof(int8(x))
為 1,因?yàn)?nbsp;int8
只占用一個(gè)字節(jié)。 - 引用類型,如
var x *int
,Sizeof(x)
會(huì)返回 8(在我的機(jī)器上,不同機(jī)器可能不一樣),另外就算引用指向了一個(gè)復(fù)合類型,比如結(jié)構(gòu)體,返回的還是 8(因?yàn)樽兞勘旧泶鎯?chǔ)的只是內(nèi)存地址)。 - 結(jié)構(gòu)體類型,如果是結(jié)構(gòu)體,那么
Sizeof
返回的大小包含了用于內(nèi)存對(duì)齊的內(nèi)存(所以可能會(huì)比結(jié)構(gòu)體底層類型所需要的實(shí)際大小要大) - 切片,
Sizeof
返回的是 24(返回的是切片這個(gè)類型所需要占用空間的大小,我們需要知道,切片底層是slice
結(jié)構(gòu)體,里面三個(gè)字段分別是array unsafe.Pointer
、len int
和cap int
,這三個(gè)字段所需要的大小為 24) - 字符串,跟切片類似,
Sizeof
會(huì)返回 16,因?yàn)樽址讓邮且粋€(gè)用來存儲(chǔ)字符串內(nèi)容的unsafe.Pointer
指針和一個(gè)表示長(zhǎng)度的int
,所以是 16。
這個(gè)方法返回的大小跟機(jī)器密切相關(guān),但一般開發(fā)者的電腦都是 64 位的,調(diào)用這個(gè)函數(shù)的值應(yīng)該跟我的機(jī)器上得到的一樣。
例子:
package main import ( "fmt" "unsafe" ) type Person struct { age int8 score int16 } type School struct { students []Person } func main() { var x int8 var y int // 1 8 // int8 占用 1 個(gè)字節(jié),int 占用 8 個(gè)字節(jié) fmt.Println(unsafe.Sizeof(x), unsafe.Sizeof(y)) var p *int // 8 // 指針變量占用 8 個(gè)字節(jié) fmt.Println(unsafe.Sizeof(p)) var person Person // 4 // age 內(nèi)存對(duì)齊需要 2 個(gè)字節(jié) // score 也需要兩個(gè)字節(jié) fmt.Println(unsafe.Sizeof(person)) var school School // 24 // 只有一個(gè)切片字段,切片需要 24 個(gè)字節(jié) // 不管這個(gè)切片里面有多少數(shù)據(jù),school 所需要占用的內(nèi)存空間都是 24 字節(jié) fmt.Println(unsafe.Sizeof(school)) var s string // 16 // 字符串底層是一個(gè) unsafe.Pointer 和一個(gè) int fmt.Println(unsafe.Sizeof(s)) }
Offsetof 方法
這個(gè)方法用于計(jì)算結(jié)構(gòu)體字段的內(nèi)存地址相對(duì)于結(jié)構(gòu)體內(nèi)存地址的偏移。具體來說就是,我們可以通過 &
(取地址)操作符獲取結(jié)構(gòu)體地址。
實(shí)際上,結(jié)構(gòu)體地址就是結(jié)構(gòu)體中第一個(gè)字段的地址。
拿到了結(jié)構(gòu)體的地址之后,我們可以通過 Offsetof
方法來獲取結(jié)構(gòu)體其他字段的偏移量,下面是一個(gè)例子:
package main import ( "fmt" "unsafe" ) type Person struct { age int8 score int16 } func main() { var person Person // 0 2 // person.age 是第一個(gè)字段,所以是 0 // person.score 是第二個(gè)字段,因?yàn)樾枰獌?nèi)存對(duì)齊,實(shí)際上 age 占用了 2 個(gè)字節(jié), // 因此 unsafe.Offsetof(person.score) 是 2,也就是說從第二個(gè)字節(jié)開始才是 person.score fmt.Println(unsafe.Offsetof(person.age), unsafe.Offsetof(person.score)) }
我們上面也說了,編譯器會(huì)對(duì)結(jié)構(gòu)體做一些內(nèi)存對(duì)齊的操作,這會(huì)導(dǎo)致結(jié)構(gòu)體底層字段占用的內(nèi)存大小會(huì)比實(shí)際需要的大小要大。
因此,我們?cè)谌〗Y(jié)構(gòu)體字段地址的時(shí)候,最好是通過結(jié)構(gòu)體地址加上 unsafe.Offsetof(x.y)
拿到的地址來操作。如下:
package main import ( "fmt" "unsafe" ) type Person struct { age int8 score int16 } func main() { var person = Person{ age: 10, score: 20, } // {10 20} fmt.Println(person) // 取得 score 字段的指針 // 通過結(jié)構(gòu)體地址,加上 score 字段的偏移量,得到 score 字段的地址 score := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&person)) + unsafe.Offsetof(person.score))) *score = 30 // {10 30} fmt.Println(person) }
這個(gè)例子看起來有點(diǎn)復(fù)雜,但是沒關(guān)系,后面會(huì)詳細(xì)展開的,這里主要要說明的是:
我們通過 unsafe.Pointer
來操作結(jié)構(gòu)體底層字段的時(shí)候,我們是通過 unsafe.Offsetof
來獲取結(jié)構(gòu)體字段地址偏移量的,因?yàn)槲覀兛吹降念愋痛笮〔⒉皇莾?nèi)存實(shí)際占用的大小,通過 Offsetof
拿到的結(jié)果是已經(jīng)將內(nèi)存對(duì)齊等因素考慮在內(nèi)的了。
(如果我們錯(cuò)誤的認(rèn)為 age
只占用一個(gè)字節(jié),然后將 unsafe.Offsetof(person.score)
替換為 1,那么我們就修改不了 score 字段了)
Alignof 方法
這個(gè)方法用以獲取某一個(gè)類型的對(duì)齊系數(shù),就是對(duì)齊一個(gè)類型的時(shí)候需要多少個(gè)字節(jié)。
這個(gè)對(duì)開發(fā)者而言意義不是非常大,go 里面只有 WaitGroup
用到了一下,沒有看到其他地方有用到這個(gè)方法,所以本文不展開了,有興趣的自行了解。
unsafe.Pointer 是什么?
讓我們?cè)賮砘仡櫼幌拢?code>Pointer 的定義是 type Pointer *ArbitraryType
,也就是一個(gè)指向任意類型的指針類型。
首先它是指針類型,所以我們初始化 unsafe.Pointer
的時(shí)候,需要通過 &
操作符來將變量的地址傳遞進(jìn)去。我們可以將其想象為指針類型的包裝類型。
例子:
package main import ( "fmt" "unsafe" ) func main() { var a int // 打印出 a 的地址:0xc0000240a8 fmt.Println(unsafe.Pointer(&a)) }
unsafe.Pointer 類型轉(zhuǎn)換
在使用 unsafe.Pointer
的時(shí)候,往往需要另一個(gè)類型來配合,那就是 uintptr
,這個(gè) uintptr
在文檔里面的描述是:uintptr
是一種整數(shù)類型,其大小足以容納任何指針的位模式。這里的關(guān)鍵是 “任何指針”,也就是說,它設(shè)計(jì)出來是被用來存儲(chǔ)指針的,而且其大小保證能存儲(chǔ)下任何指針。
而我們知道 unsafe.Pointer
也是表示指針,那么 uintptr
跟 unsafe.Pointer
有什么區(qū)別呢?
只需要記住最關(guān)鍵的一點(diǎn),uintptr
是內(nèi)存地址的整數(shù)表示,而且可以進(jìn)行算術(shù)運(yùn)算,而 unsafe.Pointer
除了可以表示一個(gè)內(nèi)存地址之外,還能保證其指向的內(nèi)存不會(huì)被垃圾回收器回收,但是 uintptr
這個(gè)地址不能保證其指向的內(nèi)存不被垃圾回收器回收。
我們先來看看與 unsafe.Pointer
相關(guān)的幾種類型轉(zhuǎn)換,這在我們下文幾乎所有地方都會(huì)用到:
- 任何類型的指針值都能轉(zhuǎn)換為
unsafe.Pointer
unsafe.Pointer
可以轉(zhuǎn)換為一個(gè)指向任何類型的指針值unsafe.Pointer
可以轉(zhuǎn)換為uintptr
uintptr
可以轉(zhuǎn)換為unsafe.Pointer
例子(下面這個(gè)例子中輸出的地址都是變量 a
所在的內(nèi)存地址,都是一樣的地址):
package main import ( "fmt" "unsafe" ) func main() { var a int var p = &a // 1. int 類型指針轉(zhuǎn)換為 unsafe.Pointer fmt.Println(unsafe.Pointer(p)) // 0xc0000240a8 // 2. unsafe.Pointer 轉(zhuǎn)換為普通類型的指針 pointer := unsafe.Pointer(&a) var pp *int = (*int)(pointer) // 0xc0000240a8 fmt.Println(pp) // 3. unsafe.Pointer 可以轉(zhuǎn)換為 uintptr var p1 = uintptr(unsafe.Pointer(p)) fmt.Printf("%x\n", p1) // c0000240a8,沒有 0x 前綴 // 4. uintptr 可以轉(zhuǎn)換為 unsafe.Pointer p2 := unsafe.Pointer(p1) fmt.Println(p2) // 0xc0000240a8 }
如何正確地使用指針?
指針允許我們忽略類型系統(tǒng)而對(duì)任意內(nèi)存進(jìn)行讀寫,這是非常危險(xiǎn)的,所以我們?cè)谑褂弥羔樀臅r(shí)候要格外的小心。
我們使用 Pointer
的模式有以下幾種,如果我們不是按照以下模式來使用 Pointer
的話,那使用的方式很可能是無效的,或者在將來變得無效,但就算是下面的幾種使用模式,也有需要注意的地方。
運(yùn)行 go vet
可以幫助查找不符合這些模式的 Pointer
的用法,但 go vet
沒有警告也并不能保證代碼有效。
以下我們就來詳細(xì)學(xué)習(xí)一下使用 Pointer
的幾種正確的模式:
1. 將 *T1
轉(zhuǎn)換為指向 *T2
的 Pointer
前提條件:
T2
類型所需要的大小不大于T1
類型的大小。(大小大的類型轉(zhuǎn)換為占用空間更小的類型)T1
和T2
的內(nèi)存布局一樣。
這是因?yàn)槿绻苯訉⒄加每臻g小的類型轉(zhuǎn)換為占用空間更大的類型的話,多出來的部分是不確定的內(nèi)容,當(dāng)然我們也可以通過
unsafe.Pointer
來修改這部分內(nèi)容。
這種轉(zhuǎn)換允許將一種類型的數(shù)據(jù)重新解釋為另外一種數(shù)據(jù)類型。下面是一個(gè)例子(為了方便演示用了 int32
和 int8
類型):
在這個(gè)例子中,
int8
類型不大于int32
類型,而且它們的內(nèi)存布局是一樣的,所以可以轉(zhuǎn)換。
package main import ( "fmt" "unsafe" ) func main() { var a int32 = 2 // p 是 *int8 類型,由 *int32 轉(zhuǎn)換而來 var p = (*int8)(unsafe.Pointer(&a)) var b int8 = *p fmt.Println(b) // 2 }
unsafe.Pointer(&a)
是指向 a
的 unsafe.Pointer
(本質(zhì)上是指向 int32
的指針),(*int8)
表示類型轉(zhuǎn)換,將這個(gè) unsafe.Pointer
轉(zhuǎn)換為 (*int8)
類型。
覺得代碼不好理解的可以看下圖:
在上圖,我們實(shí)際上是創(chuàng)建了一個(gè)指向了 a
最低位那 1 字節(jié)的指針,然后取出了這個(gè)字節(jié)里面存儲(chǔ)的內(nèi)容,將其存入了 b
中。
上面提到有一個(gè)比較重要的地方,那就是:轉(zhuǎn)換的時(shí)候是占用空間大的類型,轉(zhuǎn)換為占用空間小的類型,比如 int32
轉(zhuǎn) int8
就是符合這個(gè)條件的,那么如果我們將一個(gè)小的類型轉(zhuǎn)換為大的類型會(huì)發(fā)生什么呢?我們來看看下面這個(gè)例子:
package main import ( "fmt" "unsafe" ) type A struct { a int8 } type B struct { b int8 c int8 } func main() { var a = A{1} var b = B{2, 3} // 1. 大轉(zhuǎn)小 var pa = (*A)(unsafe.Pointer(&b)) fmt.Println(*pa) // {2} // 2. 錯(cuò)誤示例:小轉(zhuǎn)大(危險(xiǎn),A 里面 a 后面的內(nèi)存其實(shí)是未知的) var pb = (*B)(unsafe.Pointer(&a)) fmt.Println(*pb) // {1 2} }
大轉(zhuǎn)小:*B
轉(zhuǎn)換為 *A
的具體轉(zhuǎn)換過程可以表示為下圖:
在這個(gè)過程中,其實(shí) a
和 b
都沒有改變,本質(zhì)上我們只是創(chuàng)建了一個(gè) A
類型的指針,這個(gè)指針指向變量 b
的地址(但是 *pa
會(huì)被看作是 A
類型),所以 pa
實(shí)際上是跟 b
共享了內(nèi)存。
我們可以嘗試修改 (*pa).a = 3
,我們就會(huì)發(fā)現(xiàn) b.b
也變成了 3。
也就是說,最終的內(nèi)存布局是下圖這樣的:
小轉(zhuǎn)大:*A
轉(zhuǎn)換為 *B
的具體轉(zhuǎn)換過程可以表示為下圖:
注意:這是錯(cuò)誤的用法。(當(dāng)然也不是完全不行)
在 *A
轉(zhuǎn)換為 *B
的過程中,因?yàn)?nbsp;B
需要 2 個(gè)字節(jié)空間,所以我們拿到的 pb
實(shí)際上是包含了 a
后面的 1 個(gè)字節(jié),但是這個(gè)字節(jié)本來是屬于 b
變量的,這個(gè)時(shí)候 b
和 *pb
都引用了第 2 個(gè)字節(jié),這樣依賴它們?cè)谛薷倪@個(gè)字節(jié)的時(shí)候,會(huì)相互影響,這可能不是我們想要的結(jié)果,而且這種操作非常危險(xiǎn)。
2. 將 Pointer 轉(zhuǎn)換為 uintptr(但不轉(zhuǎn)換回 Pointer)
將 Pointer
轉(zhuǎn)換為 uintptr
會(huì)得到 Pointer
指向的內(nèi)存地址,是一個(gè)整數(shù)。這種 uintptr
的通常用途是打印它。
但是,將 uintptr
轉(zhuǎn)換回 Pointer
通常無效。uintptr
是一個(gè)整數(shù),而不是一個(gè)引用。將指針轉(zhuǎn)換為 uintptr
會(huì)創(chuàng)建一個(gè)沒有指針語義的整數(shù)值。
即使 uintptr
持有某個(gè)對(duì)象的地址,如果該對(duì)象移動(dòng),垃圾收集器也不會(huì)更新該 uintotr
的值,也不會(huì)阻止該對(duì)象被回收。
如下面這種,我們?nèi)〉昧俗兞康牡刂?nbsp;p
,然后做了一些其他操作,最后再從這個(gè)地址里面讀取數(shù)據(jù):
package main import ( "fmt" "unsafe" ) func main() { var a int = 10 var p = uintptr(unsafe.Pointer(&a)) // ... 其他代碼 // 下面這種轉(zhuǎn)換是危險(xiǎn)的,因?yàn)橛锌赡?p 指向的對(duì)象已經(jīng)被垃圾回收器回收 fmt.Println(*(*int)(unsafe.Pointer(p))) }
具體如下圖:
只有下面的模式中轉(zhuǎn)換 uintptr
到 Pointer
是有效的。
3. 使用算術(shù)運(yùn)算將 Pointer 轉(zhuǎn)換為 uintptr 并轉(zhuǎn)換回去
如果 p
指向一個(gè)已分配的對(duì)象,我們可以將 p
轉(zhuǎn)換為 uintptr
然后加上一個(gè)偏移量,再轉(zhuǎn)換回 Pointer
。如:
p = unsafe.Pointer(uintptr(p) + offset)
這種模式最常見的用法是訪問結(jié)構(gòu)體或者數(shù)組元素中的字段:
// 等價(jià)于 f := unsafe.Pointer(&s.f) f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f)) // 等價(jià)于 e := unsafe.Pointer(&x[i]) e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + i*unsafe.Sizeof(x[0]))
對(duì)于第一個(gè)例子,完整代碼如下:
package main import ( "fmt" "unsafe" ) type S struct { d int8 f int8 } func main() { var s = S{ d: 1, f: 2, } f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f)) fmt.Println(*(*int8)(f)) // 2 }
最終的內(nèi)存布局如下圖(s
的兩個(gè)字段都是 1 字節(jié),所以圖中 d
和 f
都是 1 字節(jié)):
詳細(xì)說明一下:
第一小節(jié)我們說過了,結(jié)構(gòu)體字段的內(nèi)存布局是連續(xù)的。上面沒有說的是,其實(shí)數(shù)組的內(nèi)存布局也是連續(xù)的。這對(duì)理解下面的內(nèi)容很有幫助。
&s
取得了結(jié)構(gòu)體s
的地址unsafe.Pointer(&s)
轉(zhuǎn)換為Pointer
對(duì)象,這個(gè)指針對(duì)象指向的是結(jié)構(gòu)體s
uintptr(unsafe.Pointer(&s))
取得Pointer
對(duì)象的內(nèi)存地址(整數(shù))unsafe.Offsetof(s.f)
取得了f
字段的內(nèi)存偏移地址(相對(duì)地址,相對(duì)于s
的地址)uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f)
就是s.f
的實(shí)際內(nèi)存地址了(絕對(duì)地址)- 最后轉(zhuǎn)換回
unsafe.Pointer
對(duì)象,這個(gè)對(duì)象指向的地址是s.f
的地址
最終 f
指向的地址是 s.f
,然后我們可以通過 (*int8)(f)
將 unsafe.Pointer
轉(zhuǎn)換為 *int8
類型指針,最后通過 *
操作符取得這個(gè)指針指向的值。
對(duì)于第二個(gè)例子,完整代碼如下:
package main import ( "fmt" "unsafe" ) func main() { var x = [3]int8{4, 5, 6} e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0])) fmt.Println(*(*int8)(e)) // 6 }
最終的內(nèi)存布局如下圖,e
指向了數(shù)組的第 3 個(gè)元素(下標(biāo)從 0 開始算的):
代碼中的 2 可以是其他任何有效的數(shù)組下標(biāo)。
&s
取得了數(shù)組x
的地址unsafe.Pointer(&x)
轉(zhuǎn)換為Pointer
對(duì)象,這個(gè)指針對(duì)象指向的是數(shù)組x
uintptr(unsafe.Pointer(&x))
取得Pointer
對(duì)象的內(nèi)存地址(也就是0xab
)unsafe.Sizeof(x[0])
是數(shù)組x
里面每一個(gè)元素所需要的內(nèi)存大小,乘以2
表示是元素x[2]
的地址偏移量(相對(duì)地址,相對(duì)于x[0]
的地址)uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0])
表示的是數(shù)組元素x[2]
的實(shí)際內(nèi)存地址(絕對(duì)地址)- 最后轉(zhuǎn)換回
unsafe.Pointer
對(duì)象,這個(gè)對(duì)象指向的地址是x[2]
的地址(也就是0xab + 2
)。
最終,我們可以通過 (*int8)
將 e
轉(zhuǎn)換為 *int8
類型的指針,最后通過 *
操作符獲取其指向的內(nèi)容,也就是 6。
以這種方式對(duì)指針進(jìn)行加減偏移量的運(yùn)算都是有效的。(em…這里說的是寫在同一行的這種方式)。這種情況下使用 &^
這兩個(gè)操作符也是有效的(通常用于內(nèi)存對(duì)齊)。
在所有情況下,得到的結(jié)果必須指向原始分配的對(duì)象。
不像 C 語言,將指針加上一個(gè)超出其原始分配的內(nèi)存區(qū)域的偏移量是無效的:
// 無效: end 指向了分配的空間以外的區(qū)域 var s thing end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
下面對(duì)切片的這種操作也跟上圖類似。
// 無效: end 指向了分配的空間以外的區(qū)域 b := make([]byte, n) end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
這是因?yàn)?,?nèi)存的地址范圍是 [start, end)
,是不包含終點(diǎn)的那個(gè)地址的,上面的 end
都指向了地址的邊界,這是無效的。
當(dāng)然,除了邊界上,邊界以外都是無效的。(end
指向的內(nèi)存不是屬于那個(gè)變量的)
注意:兩個(gè)轉(zhuǎn)換(Pointer
=> uintptr
, uintptr
=> Pointer
)必須出現(xiàn)在同一個(gè)表達(dá)式中,只有中間的算術(shù)運(yùn)算:
// 無效: uintptr 在轉(zhuǎn)換回 Pointer 之前不能存儲(chǔ)在變量中 // 原因上面也說過了,就是 p 指向的內(nèi)容可能會(huì)被垃圾回收器回收。 u := uintptr(p) p = unsafe.Pointer(u + offset)
注意:指針必須指向已分配的對(duì)象,因此它不能是 nil
。
// 無效: nil 指針轉(zhuǎn)換 u := unsafe.Pointer(nil) p := unsafe.Pointer(uintptr(u) + offset)
4. 調(diào)用 syscall.Syscall 時(shí)將指針轉(zhuǎn)換為 uintptr
覺得文字太啰嗦可以直接看圖:
syscall
包中的 Syscall
函數(shù)將其 uintptr
參數(shù)直接傳遞給操作系統(tǒng),然后操作系統(tǒng)可以根據(jù)調(diào)用的細(xì)節(jié)將其中一些參數(shù)重新解釋為指針。
也就是說,系統(tǒng)調(diào)用實(shí)現(xiàn)隱式地將某些參數(shù)從 uintptr
轉(zhuǎn)換回指針。
如果必須將指針參數(shù)轉(zhuǎn)換為 uintptr
以用作參數(shù),則該轉(zhuǎn)換必須出現(xiàn)在調(diào)用表達(dá)式本身中:
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
編譯器通過安排被引用的分配對(duì)象(如果有的話)被保留,并且在調(diào)用完成之前不移動(dòng),來處理在調(diào)用程序集中實(shí)現(xiàn)的函數(shù)的參數(shù)列表中轉(zhuǎn)換為 uintptr
的指針,
即使僅從類型來看,在調(diào)用期間似乎不再需要對(duì)象。
為了使編譯器識(shí)別該模式,轉(zhuǎn)換必須出現(xiàn)在參數(shù)列表中:
// 無效:在系統(tǒng)調(diào)用期間隱式轉(zhuǎn)換回指針之前, // uintptr 不能存儲(chǔ)在變量中。 u := uintptr(unsafe.Pointer(p)) syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))
5. 將 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的結(jié)果從 uintptr 轉(zhuǎn)換為 Pointer
reflect.Value
的 Pointer
和 UnsafeAddr
方法返回類型 uintptr
而不是 unsafe.Pointer
,從而防止調(diào)用者在未導(dǎo)入 unsafe
包的情況下將結(jié)果更改為任意類型。(這是為了防止開發(fā)者對(duì) Pointer
的誤操作。)
然而,這也意味著這個(gè)返回的結(jié)果是脆弱的,我們必須在調(diào)用之后立即轉(zhuǎn)換為 Pointer
(如果我們確切的需要一個(gè) Pointer
):
其實(shí)就是為了讓開發(fā)者明確自己知道在干啥,要不然寫出了 bug 都不知道。
// 在調(diào)用了 reflect.Value 的 Pointer 方法后, // 立即轉(zhuǎn)換為 unsafe.Pointer。 p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
與上述情況一樣,在轉(zhuǎn)換之前存儲(chǔ)結(jié)果是無效的:
// 無效: uintptr 在轉(zhuǎn)換回 Pointer 之前不能保存在變量中 u := reflect.ValueOf(new(int)).Pointer() // uintptr 保存到了 u 中 p := (*int)(unsafe.Pointer(u))
原因上面也說了,因?yàn)?nbsp;u
指向的內(nèi)存是不受保護(hù)的,可能會(huì)被垃圾回收器收集。
6. 將 reflect.SliceHeader 或 reflect.StringHeader 的 Data 字段跟 Pointer 互相轉(zhuǎn)換
與前面的情況一樣,反射數(shù)據(jù)結(jié)構(gòu) SliceHeader
和 StringHeader
將字段 Data
聲明為 uintptr
,以防止調(diào)用者在不首先導(dǎo)入 unsafe
的情況下將結(jié)果更改為任意類型。
然而,這意味著 SliceHeader
和 StringHeader
僅在解析實(shí)際切片或字符串值的內(nèi)容時(shí)有效。
我們先來看看這兩個(gè)結(jié)構(gòu)體的定義:
// SliceHeader 是切片的運(yùn)行時(shí)表示(內(nèi)存布局跟切片一致) // 它不能安全或可移植地使用,其表示形式可能會(huì)在以后的版本中更改。 // 此外,Data 字段不足以保證它引用的數(shù)據(jù)不會(huì)被垃圾回收器收集, // 因此程序必須保留一個(gè)指向底層數(shù)據(jù)的單獨(dú)的、正確類型的指針。 type SliceHeader struct { Data uintptr Len int Cap int } // StringHeader 字符串的運(yùn)行時(shí)表示(內(nèi)存布局跟字符串一致) // ... 其他注意事項(xiàng)跟 SliceHeader 一樣 type StringHeader struct { Data uintptr Len int }
使用示例:
// 將字符串的內(nèi)容修改為 p 指向的內(nèi)容 var s string hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n
這種轉(zhuǎn)換是有效的,因?yàn)?SliceHeader 的內(nèi)存布局和 StringHeader 的內(nèi)存布局一致,并且 SliceHeader 所占用的內(nèi)存空間比StringHeader 所占用內(nèi)存空間大,也就是說,這是一種大小更大的類型轉(zhuǎn)換為大小更小的類型,這會(huì)丟失 SliceHeader 的一部分?jǐn)?shù)據(jù),但是丟失的那部分對(duì)我們程序正常運(yùn)行是沒有任何影響的。
在這個(gè)用法中,hdr.Data
實(shí)際上是引用字符串頭中的基礎(chǔ)指針的另一種方式,而不是 uintptr 變量本身。
(我們這里也是使用了 uintptr
表達(dá)式,而不是一個(gè)存儲(chǔ)了 uintptr
類型的變量)
通常來說,reflect.SliceHeader
和 reflect.StringHeader
通常用在指向?qū)嶋H切片或者字符串的*reflect.SliceHeader
和 *reflect.StringHeader
,永遠(yuǎn)不會(huì)被當(dāng)作普通結(jié)構(gòu)體使用。
程序不應(yīng)該聲明或者分配這些結(jié)構(gòu)體類型的變量,下面的寫法是有風(fēng)險(xiǎn)的。
// 無效: 直接聲明的 Header 不會(huì)將 Data 作為引用 var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // p 可能已經(jīng)丟失
Add 函數(shù)
函數(shù)原型是:func Add(ptr Pointer, len IntegerType) Pointer
這個(gè)函數(shù)的作用是,可以將 unsafe.Pointer
類型加上一個(gè)偏移量得到一個(gè)指向新地址的 unsafe.Pointer
。
簡(jiǎn)單點(diǎn)來說,就是對(duì) unsafe.Pointer
做算術(shù)運(yùn)算的,上面我們說過 unsafe.Pointer
是不能直接進(jìn)行算術(shù)運(yùn)算的,因此需要先轉(zhuǎn)換為 uintptr
然后再進(jìn)行算術(shù)運(yùn)算,算完再轉(zhuǎn)換回 unsafe.Pointer
類型,所以會(huì)很繁瑣。
有了 Add
方法,我們可以寫得簡(jiǎn)單一些,不用做 uintptr
的轉(zhuǎn)換。
有了 Add
,我們可以簡(jiǎn)化一下上面那個(gè)通過數(shù)組指針加偏移量的例子,示例:
package main import ( "fmt" "unsafe" ) func main() { var x = [3]int8{4, 5, 6} //e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0])) e := unsafe.Add(unsafe.Pointer(&x), 2 * unsafe.Sizeof(x[0])) fmt.Println(*(*int8)(e)) // 6 }
在這個(gè)例子中,我們先是通過 unsafe.Pointer(&x)
獲取到了一個(gè)指向 x
的 unsafe.Pointer
對(duì)象,然后通過 unsafe.Add
加上了 2 個(gè) int8
類型大小的偏移量,最終得到的是一個(gè)指向 x[2]
的 unsafe.Pointer
。
Add 方法可以簡(jiǎn)化我們對(duì)指針的一些操作。
Slice 函數(shù)
Slice
函數(shù)的原型是:func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
函數(shù) Slice
返回一個(gè)切片,其底層數(shù)組以 ptr
開頭,長(zhǎng)度和容量為 len
。
unsafe.Slice(ptr, len)
等價(jià)于:
(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
除了這個(gè),作為一種特殊情況,如果 ptr
為 nil
,len
為零,則 Slice
返回 nil
。
示例:
package main import ( "fmt" "unsafe" ) func main() { var x = [6]int8{4, 5, 6, 7, 8, 9} // 這里取了數(shù)組第一個(gè)元素 x[1] 的地址, // 從這個(gè)地址開始取了 3 個(gè)元素作為新的切片底層數(shù)組, // 返回這個(gè)新的切片 s := unsafe.Slice(&x[1], 3) fmt.Println(s) // [5 6 7] }
需要非常注意的是,第一個(gè)參數(shù)實(shí)際上隱含傳遞了該地址對(duì)應(yīng)的類型信息,上面用了
&x[1]
,傳遞的類型實(shí)際上是int8
。
如果我們按照下面這樣寫,得到的結(jié)果就是錯(cuò)誤的,因?yàn)樗[式傳遞的類型是 [6]int8
(這是一個(gè)數(shù)組),而不是 int8
:
// 錯(cuò)誤示例: package main import ( "fmt" "unsafe" ) func main() { var x = [6]int8{4, 5, 6, 7, 8, 9} // unsafe.Slice 第一個(gè)參數(shù)接收到的類型是 [6]int, // 所以最終返回了一個(gè)切片,這個(gè)切片有三個(gè)元素, // 每一個(gè)元素都是長(zhǎng)度為 6 數(shù)據(jù)類型為 int8 的數(shù)組。 // 也即形如 [[6]int8, [6]int8, [6]int8] 的切片 s := unsafe.Slice(&x, 3) // [[4 5 6 7 8 9] [91 91 52 32 53 32] [54 32 4 5 6 7]] fmt.Println(s) }
這樣顯然不是我們想要的結(jié)果,因?yàn)樗x取到了一部分未知的內(nèi)存,如果我們修改這部分內(nèi)存,可能會(huì)造成程序崩潰。
一個(gè)很常見的用法
在實(shí)際應(yīng)用中,很多框架為了提高性能,在做 []byte
和 string
的切換的時(shí)候,往往會(huì)使用 unsafe.Pointer
來實(shí)現(xiàn)(比如 gin
框架):
下面這個(gè)例子實(shí)現(xiàn)了 []byte
到 string
的轉(zhuǎn)換,而且避免了內(nèi)存分配。這是因?yàn)?,切片和字符串的?nèi)存布局是一致的,只不過切片比字符串占用
的空間多了一點(diǎn),還有一個(gè) cap
容量字段,用來表示切片的容量是多少。具體我們可以再看看上面的 reflect.SliceHeader
和 reflect.StringHeader
,
在下面這個(gè)字節(jié)切片到字符串的轉(zhuǎn)換過程中,是從占用空間更大的類型轉(zhuǎn)換為占用空間更小的類型,所以是安全的,丟失的那個(gè) cap
對(duì)我們程序正常運(yùn)行無影響。
先看看 []byte
和 string
的類型底層定義:
// 字符串 type stringStruct struct { str unsafe.Pointer len int } // 切片,比 string 的結(jié)構(gòu)體多了一個(gè) cap 字段,但是前面的兩個(gè)字段是一樣的 type slice struct { array unsafe.Pointer len int cap int }
[]byte
轉(zhuǎn)字符串的示例:
func BytesToString(b []byte) string { // 將 b 解析為字符串 return *(*string)(unsafe.Pointer(&b)) }
這個(gè)操作如下圖:
在這個(gè)轉(zhuǎn)換過程中,其實(shí)只是將 b
表示的類型轉(zhuǎn)由 []byte
轉(zhuǎn)換為了 string
,之所以可以這么轉(zhuǎn),是因?yàn)?nbsp;[]byte
的內(nèi)存布局跟 string
的內(nèi)存布局是一樣的,但是由于字符串實(shí)際占用空間比切片類型要?。ú话ㄆ涞讓又羔樦赶虻膬?nèi)容),所以在轉(zhuǎn)換過程中,cap
字段丟失了,但是 strin
也不需要這個(gè)字段,所以對(duì)程序運(yùn)行沒影響。
同時(shí)字符串長(zhǎng)度是按照字節(jié)計(jì)算的,所以字節(jié)切片和字符串的 len 字段是一樣的,不需要做額外處理。
字符串轉(zhuǎn) []byte
的示例:
func StringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer( // 定義匿名結(jié)構(gòu)體變量,內(nèi)存布局跟 []byte 一致, // 這樣就可以轉(zhuǎn)換為 []byte 了。 &struct { string Cap int }{s, len(s)}, )) }
這個(gè)操作如下圖:
這個(gè)過程只是需要分配很小一部分內(nèi)存就可以完成了,效率比 go 自帶的轉(zhuǎn)換高。
go 里面字符串是不可變的,但 go 為了維持字符串不可變的特性,在字符串和字節(jié)切片之間轉(zhuǎn)換一般都是通過數(shù)據(jù)拷貝的方式實(shí)現(xiàn)的。
因?yàn)檫@樣就不會(huì)影響到原來的字符串或者字節(jié)切片了,但是這樣做的性能會(huì)非常低。
具體可參考slicebytetostring
和stringtoslicebyte
函數(shù),這兩個(gè)函數(shù)位于runtime/string.go
中。
總結(jié)
本文主要講了如下內(nèi)容:
- 內(nèi)存布局:結(jié)構(gòu)體的字段存儲(chǔ)是占用了連續(xù)的一段內(nèi)存,而且結(jié)構(gòu)體可能會(huì)占用比實(shí)際需要空間更大的內(nèi)存,因?yàn)樾枰獙?duì)齊內(nèi)存。
- 指針存儲(chǔ)了指向變量的地址,對(duì)這個(gè)地址使用
*
操作符可以獲取這個(gè)地址指向的內(nèi)容。 uintptr
是 C 里面的一種命名慣例,u
前綴的意思是unsigned
,int
表示是int
類型,ptr
表示這個(gè)類型是用來表示指針的。unsafe
定義的Pointer
類型是一種可以指向任何類型的指針,ArbitraryType
可用于表示任意類型。- 我們通過
unsafe.Pointer
修改結(jié)構(gòu)體字段的時(shí)候,要使用unsafe.Offsetof
獲取結(jié)構(gòu)體的偏移量。 - 通過
unsafe.Sizeof
可以獲得某一種類型所需要的內(nèi)存空間大小(其中包括了用于內(nèi)存對(duì)齊的內(nèi)存)。 unsafe.Pointer
與uintptr
之間的類型轉(zhuǎn)換。- 幾種使用
unsafe.Pointer
的模式:*T1
到*T2
的轉(zhuǎn)換unsafe.Pointer
轉(zhuǎn)換為uintptr
- 使用算術(shù)運(yùn)算將
unsafe.Pointer
轉(zhuǎn)換為uintptr
并轉(zhuǎn)換回去(需要注意不能使用中間變量來保存uintptr(unsafe.Pointer(p))
) - 調(diào)用
syscall.Syscall
時(shí)將指針轉(zhuǎn)換為uintptr
- 將
reflect.Value
的Pointer
和UnsafeAddr
的結(jié)果從uintptr
轉(zhuǎn)換為unsafe.Pointer
- 將
reflect.SliceHeader
或reflect.StringHeader
的Data
字段跟Pointer
互相轉(zhuǎn)換
Add
函數(shù)可以簡(jiǎn)化指針的算術(shù)運(yùn)算,不用來回轉(zhuǎn)換類型(比如unsafe.Pointer
轉(zhuǎn)換為uintptr
,然后再轉(zhuǎn)換為unsafe.Pointer
)。Slice
函數(shù)可以獲取指針指向內(nèi)存的一部分。- 最后介紹了
string
和[]byte
之間通過unsafe.Pointer
實(shí)現(xiàn)高效轉(zhuǎn)換的方法。
到此這篇關(guān)于深入理解go unsafe用法及注意事項(xiàng)的文章就介紹到這了,更多相關(guān)go unsafe用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言實(shí)現(xiàn)類似c++中的多態(tài)功能實(shí)例
Go本身不具有多態(tài)的特性,不能夠像Java、C++那樣編寫多態(tài)類、多態(tài)方法。但是,使用Go可以編寫具有多態(tài)功能的類綁定的方法。下面來一起看看吧2016-09-09Golang處理parquet文件實(shí)戰(zhàn)指南
這篇文章主要給大家介紹了關(guān)于Golang處理parquet文件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型
這篇文章主要介紹了Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04golang DNS服務(wù)器的簡(jiǎn)單實(shí)現(xiàn)操作
這篇文章主要介紹了golang DNS服務(wù)器的簡(jiǎn)單實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04