深入理解go unsafe用法及注意事項(xiàng)
學(xué)過(guò) C 的朋友應(yīng)該知道,有一種類型是指針類型,指針類型存儲(chǔ)的是一個(gè)內(nèi)存地址,通過(guò)這個(gè)內(nèi)存地址可以找到它指向的變量。go 雖然是一種高級(jí)語(yǔ)言,但是也還是給開(kāi)發(fā)者提供了指針的類型 unsafe.Pointer,我們可以通過(guò)它來(lái)直接讀寫(xiě)變量的內(nèi)存。
正因?yàn)槿绱?,如果我們操作不?dāng),極有可能會(huì)導(dǎo)致程序崩潰。今天就來(lái)了解一下 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í)并沒(méi)有什么特殊的含義。
它們的具體含義完全取決于我們?cè)趺慈ダ斫馑鼈儯热?nbsp;0010 0000,如果我們將其看作是一個(gè)十進(jìn)制數(shù)字,那么它就是 32,如果我們將其看作是字符,那么他就是一個(gè)空格(具體可參考 ASCII 碼表)。
對(duì)應(yīng)到編程語(yǔ)言層面,其實(shí)我們的變量存儲(chǔ)在內(nèi)存里面也是 01 表示的二進(jìn)制,這些二進(jìn)制數(shù)表示是什么類型都是語(yǔ)言層面的事,更準(zhǔn)確來(lái)說(shuō),是編譯器來(lái)處理的,我們寫(xiě)代碼的時(shí)候?qū)⒆兞柯暶鳛檎麛?shù),那么我們?nèi)〕鰜?lái)的時(shí)候也會(huì)表示成一個(gè)整數(shù)。
這跟本文有什么關(guān)系呢?我們下面會(huì)講到很多關(guān)于類型轉(zhuǎn)換的內(nèi)容,如果我們理解了這一節(jié)說(shuō)的內(nèi)容,下面的內(nèi)容會(huì)更容易理解
在我們做類型轉(zhuǎn)換的時(shí)候,實(shí)際上底層的二進(jìn)制表示是沒(méi)有變的,變的只是我們所看到的表面的東西。
內(nèi)存布局
有點(diǎn)想直接開(kāi)始講 unsafe 里的 Pointer 的,但是如果讀者對(duì)計(jì)算機(jī)內(nèi)存怎么存儲(chǔ)變量不太熟悉的話,看起來(lái)可能會(huì)比較費(fèi)解,所以在文章開(kāi)頭會(huì)花比較大的篇幅來(lái)講述計(jì)算機(jī)是怎么存儲(chǔ)數(shù)據(jù)的,相信讀完會(huì)再閱讀后面的內(nèi)容(比如指針的算術(shù)運(yùn)算、通過(guò)指針修改結(jié)構(gòu)體字段)會(huì)沒(méi)有那么多障礙。
變量在內(nèi)存中是怎樣的?
我們先來(lá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é)來(lái)存儲(chǔ)整型數(shù)據(jù)
// int16 意味著,用 16 位,也就是兩個(gè)字節(jié)來(lái)存儲(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è)賮?lái)看看結(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ǔ)空間的,具體來(lái)說(shuō)是占用了連續(xù)的兩個(gè)字節(jié)。
指針在內(nèi)存中是怎么存儲(chǔ)的?
在下面的代碼中,我們定義了一個(gè) a 變量,大小為 1 字節(jié),然后我們定義了一個(gè)指向 a 的指針 p:
需要先說(shuō)明的是,下面有兩個(gè)操作符,一個(gè)是 &,這個(gè)是取地址的操作符,var p = &a 意味著,取得 a 的內(nèi)存地址,將其存儲(chǔ)在變量 p 中,另一個(gè)操作符是 *,這個(gè)操作符的意思是解指針,*p 就是通過(guò) 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)存地址,通過(guò)這個(gè)地址我們可以找到它實(shí)際存儲(chǔ)的內(nèi)容。
結(jié)構(gòu)體的內(nèi)存布局真的是我們上面說(shuō)的那樣嗎?
上面我們說(shuō)了,下面這個(gè)結(jié)構(gòu)體占用了兩個(gè)字節(jié),結(jié)構(gòu)體里面的一個(gè)字段占用一個(gè)字節(jié):
type Person struct {
age int8
score int8
}
然后我們?cè)賮?lái)看看下面這個(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é)不是嗎?說(shuō)實(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ù)的過(guò)程是按字讀取的,如果我們數(shù)據(jù)的內(nèi)存沒(méi)有對(duì)齊,則可能會(huì)導(dǎo)致 CPU 本來(lái)一次可以讀取完的數(shù)據(jù)現(xiàn)在需要多次讀取,這樣就會(huì)造成效率的下降。
關(guān)于內(nèi)存對(duì)齊,是一個(gè)比較龐大的話題,這里不展開(kāi)了,我們需要明確的是,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ì)說(shuō)到。
uintptr 是什么意思?
在開(kāi)始下文之前,還是得啰嗦一句,uintptr 這種命名方式是 C 語(yǔ)言里面的一種類型命名的慣例,u 前綴表示是無(wú)符號(hào)數(shù)(unsigned),ptr 是指針(pointer)的縮寫(xiě),這個(gè) uintptr按這個(gè)命名慣例解析的話,就是一個(gè)指向無(wú)符號(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 類型,但是從名字上我們可以看到,它被命名為任意類型,也就是說(shuō),他會(huì)被我們用來(lái)表示任意的類型,具體怎么用,是下面說(shuō)的 unsafe.Pointer 用的。
IntegerType
type IntegerType int,它表示的是一個(gè)任意的整數(shù),在 unsafe 包中它被用來(lái)作為表示切片或者指針加減的長(zhǎng)度。
Pointer
type Pointer *ArbitraryType,這個(gè)就是我們上一節(jié)提到的指針了,它可以指向任何類型的數(shù)據(jù)(*ArbitraryType)。
內(nèi)存地址實(shí)際上就是計(jì)算機(jī)內(nèi)存的編號(hào),是一個(gè)整數(shù),所以我們才可以使用
int來(lái)表示指針。
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)容的大小。
有幾種常見(jiàn)的情況(沒(mé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è)用來(lái)存儲(chǔ)字符串內(nèi)容的unsafe.Pointer指針和一個(gè)表示長(zhǎng)度的int,所以是 16。
這個(gè)方法返回的大小跟機(jī)器密切相關(guān),但一般開(kāi)發(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)存地址的偏移。具體來(lái)說(shuō)就是,我們可以通過(guò) &(取地址)操作符獲取結(jié)構(gòu)體地址。
實(shí)際上,結(jié)構(gòu)體地址就是結(jié)構(gòu)體中第一個(gè)字段的地址。
拿到了結(jié)構(gòu)體的地址之后,我們可以通過(guò) Offsetof 方法來(lái)獲取結(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,也就是說(shuō)從第二個(gè)字節(jié)開(kāi)始才是 person.score
fmt.Println(unsafe.Offsetof(person.age), unsafe.Offsetof(person.score))
}
我們上面也說(shuō)了,編譯器會(huì)對(duì)結(jié)構(gòu)體做一些內(nèi)存對(duì)齊的操作,這會(huì)導(dǎo)致結(jié)構(gòu)體底層字段占用的內(nèi)存大小會(huì)比實(shí)際需要的大小要大。
因此,我們?cè)谌〗Y(jié)構(gòu)體字段地址的時(shí)候,最好是通過(guò)結(jié)構(gòu)體地址加上 unsafe.Offsetof(x.y) 拿到的地址來(lái)操作。如下:
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 字段的指針
// 通過(guò)結(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è)例子看起來(lái)有點(diǎn)復(fù)雜,但是沒(méi)關(guān)系,后面會(huì)詳細(xì)展開(kāi)的,這里主要要說(shuō)明的是:
我們通過(guò) unsafe.Pointer 來(lái)操作結(jié)構(gòu)體底層字段的時(shí)候,我們是通過(guò) unsafe.Offsetof 來(lái)獲取結(jié)構(gòu)體字段地址偏移量的,因?yàn)槲覀兛吹降念愋痛笮〔⒉皇莾?nèi)存實(shí)際占用的大小,通過(guò) 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ì)開(kāi)發(fā)者而言意義不是非常大,go 里面只有 WaitGroup 用到了一下,沒(méi)有看到其他地方有用到這個(gè)方法,所以本文不展開(kāi)了,有興趣的自行了解。
unsafe.Pointer 是什么?
讓我們?cè)賮?lái)回顧一下,Pointer 的定義是 type Pointer *ArbitraryType,也就是一個(gè)指向任意類型的指針類型。
首先它是指針類型,所以我們初始化 unsafe.Pointer 的時(shí)候,需要通過(guò) & 操作符來(lái)將變量的地址傳遞進(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è)類型來(lái)配合,那就是 uintptr,這個(gè) uintptr 在文檔里面的描述是:uintptr 是一種整數(shù)類型,其大小足以容納任何指針的位模式。這里的關(guān)鍵是 “任何指針”,也就是說(shuō),它設(shè)計(jì)出來(lái)是被用來(lái)存儲(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)存不被垃圾回收器回收。
我們先來(lái)看看與 unsafe.Pointer 相關(guān)的幾種類型轉(zhuǎn)換,這在我們下文幾乎所有地方都會(huì)用到:
- 任何類型的指針值都能轉(zhuǎn)換為
unsafe.Pointer unsafe.Pointer可以轉(zhuǎn)換為一個(gè)指向任何類型的指針值unsafe.Pointer可以轉(zhuǎn)換為uintptruintptr可以轉(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,沒(méi)有 0x 前綴
// 4. uintptr 可以轉(zhuǎn)換為 unsafe.Pointer
p2 := unsafe.Pointer(p1)
fmt.Println(p2) // 0xc0000240a8
}
如何正確地使用指針?
指針允許我們忽略類型系統(tǒng)而對(duì)任意內(nèi)存進(jìn)行讀寫(xiě),這是非常危險(xiǎn)的,所以我們?cè)谑褂弥羔樀臅r(shí)候要格外的小心。
我們使用 Pointer 的模式有以下幾種,如果我們不是按照以下模式來(lái)使用 Pointer 的話,那使用的方式很可能是無(wú)效的,或者在將來(lái)變得無(wú)效,但就算是下面的幾種使用模式,也有需要注意的地方。
運(yùn)行 go vet 可以幫助查找不符合這些模式的 Pointer 的用法,但 go vet 沒(méi)有警告也并不能保證代碼有效。
以下我們就來(lái)詳細(xì)學(xué)習(xí)一下使用 Pointer 的幾種正確的模式:
1. 將 *T1 轉(zhuǎn)換為指向 *T2 的 Pointer
前提條件:
T2類型所需要的大小不大于T1類型的大小。(大小大的類型轉(zhuǎn)換為占用空間更小的類型)T1和T2的內(nèi)存布局一樣。
這是因?yàn)槿绻苯訉⒄加每臻g小的類型轉(zhuǎn)換為占用空間更大的類型的話,多出來(lái)的部分是不確定的內(nèi)容,當(dāng)然我們也可以通過(guò)
unsafe.Pointer來(lái)修改這部分內(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)換而來(lái)
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) 類型。
覺(jué)得代碼不好理解的可以看下圖:

在上圖,我們實(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ā)生什么呢?我們來(lái)看看下面這個(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)換過(guò)程可以表示為下圖:

在這個(gè)過(guò)程中,其實(shí) a 和 b 都沒(méi)有改變,本質(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。
也就是說(shuō),最終的內(nèi)存布局是下圖這樣的:

小轉(zhuǎn)大:*A 轉(zhuǎn)換為 *B 的具體轉(zhuǎn)換過(guò)程可以表示為下圖:

注意:這是錯(cuò)誤的用法。(當(dāng)然也不是完全不行)
在 *A 轉(zhuǎn)換為 *B 的過(guò)程中,因?yàn)?nbsp;B 需要 2 個(gè)字節(jié)空間,所以我們拿到的 pb 實(shí)際上是包含了 a 后面的 1 個(gè)字節(jié),但是這個(gè)字節(jié)本來(lái)是屬于 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 通常無(wú)效。uintptr 是一個(gè)整數(shù),而不是一個(gè)引用。將指針轉(zhuǎn)換為 uintptr 會(huì)創(chuàng)建一個(gè)沒(méi)有指針語(yǔ)義的整數(shù)值。
即使 uintptr 持有某個(gè)對(duì)象的地址,如果該對(duì)象移動(dòng),垃圾收集器也不會(huì)更新該 uintotr 的值,也不會(huì)阻止該對(duì)象被回收。
如下面這種,我們?nèi)〉昧俗兞康牡刂?nbsp;p,然后做了一些其他操作,最后再?gòu)倪@個(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àn)的用法是訪問(wèn)結(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ì)說(shuō)明一下:
第一小節(jié)我們說(shuō)過(guò)了,結(jié)構(gòu)體字段的內(nèi)存布局是連續(xù)的。上面沒(méi)有說(shuō)的是,其實(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)體suintptr(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,然后我們可以通過(guò) (*int8)(f) 將 unsafe.Pointer 轉(zhuǎn)換為 *int8 類型指針,最后通過(guò) * 操作符取得這個(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 開(kāi)始算的):

代碼中的 2 可以是其他任何有效的數(shù)組下標(biāo)。
&s取得了數(shù)組x的地址unsafe.Pointer(&x)轉(zhuǎn)換為Pointer對(duì)象,這個(gè)指針對(duì)象指向的是數(shù)組xuintptr(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)。
最終,我們可以通過(guò) (*int8) 將 e 轉(zhuǎn)換為 *int8 類型的指針,最后通過(guò) * 操作符獲取其指向的內(nèi)容,也就是 6。
以這種方式對(duì)指針進(jìn)行加減偏移量的運(yùn)算都是有效的。(em…這里說(shuō)的是寫(xiě)在同一行的這種方式)。這種情況下使用 &^ 這兩個(gè)操作符也是有效的(通常用于內(nèi)存對(duì)齊)。
在所有情況下,得到的結(jié)果必須指向原始分配的對(duì)象。
不像 C 語(yǔ)言,將指針加上一個(gè)超出其原始分配的內(nèi)存區(qū)域的偏移量是無(wú)效的:
// 無(wú)效: end 指向了分配的空間以外的區(qū)域 var s thing end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

下面對(duì)切片的這種操作也跟上圖類似。
// 無(wú)效: 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 都指向了地址的邊界,這是無(wú)效的。
當(dāng)然,除了邊界上,邊界以外都是無(wú)效的。(end 指向的內(nèi)存不是屬于那個(gè)變量的)
注意:兩個(gè)轉(zhuǎn)換(Pointer => uintptr, uintptr => Pointer)必須出現(xiàn)在同一個(gè)表達(dá)式中,只有中間的算術(shù)運(yùn)算:
// 無(wú)效: uintptr 在轉(zhuǎn)換回 Pointer 之前不能存儲(chǔ)在變量中 // 原因上面也說(shuō)過(guò)了,就是 p 指向的內(nèi)容可能會(huì)被垃圾回收器回收。 u := uintptr(p) p = unsafe.Pointer(u + offset)
注意:指針必須指向已分配的對(duì)象,因此它不能是 nil。
// 無(wú)效: nil 指針轉(zhuǎn)換 u := unsafe.Pointer(nil) p := unsafe.Pointer(uintptr(u) + offset)
4. 調(diào)用 syscall.Syscall 時(shí)將指針轉(zhuǎn)換為 uintptr
覺(jué)得文字太啰嗦可以直接看圖:

syscall 包中的 Syscall 函數(shù)將其 uintptr 參數(shù)直接傳遞給操作系統(tǒng),然后操作系統(tǒng)可以根據(jù)調(diào)用的細(xì)節(jié)將其中一些參數(shù)重新解釋為指針。
也就是說(shuō),系統(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))
編譯器通過(guò)安排被引用的分配對(duì)象(如果有的話)被保留,并且在調(diào)用完成之前不移動(dòng),來(lái)處理在調(diào)用程序集中實(shí)現(xiàn)的函數(shù)的參數(shù)列表中轉(zhuǎn)換為 uintptr 的指針,
即使僅從類型來(lái)看,在調(diào)用期間似乎不再需要對(duì)象。
為了使編譯器識(shí)別該模式,轉(zhuǎn)換必須出現(xiàn)在參數(shù)列表中:
// 無(wú)效:在系統(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é)果更改為任意類型。(這是為了防止開(kāi)發(fā)者對(duì) Pointer 的誤操作。)
然而,這也意味著這個(gè)返回的結(jié)果是脆弱的,我們必須在調(diào)用之后立即轉(zhuǎn)換為 Pointer(如果我們確切的需要一個(gè) Pointer):
其實(shí)就是為了讓開(kāi)發(fā)者明確自己知道在干啥,要不然寫(xiě)出了 bug 都不知道。
// 在調(diào)用了 reflect.Value 的 Pointer 方法后, // 立即轉(zhuǎn)換為 unsafe.Pointer。 p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
與上述情況一樣,在轉(zhuǎn)換之前存儲(chǔ)結(jié)果是無(wú)效的:
// 無(wú)效: uintptr 在轉(zhuǎn)換回 Pointer 之前不能保存在變量中 u := reflect.ValueOf(new(int)).Pointer() // uintptr 保存到了 u 中 p := (*int)(unsafe.Pointer(u))
原因上面也說(shuō)了,因?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í)有效。
我們先來(lái)看看這兩個(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)存空間大,也就是說(shuō),這是一種大小更大的類型轉(zhuǎn)換為大小更小的類型,這會(huì)丟失 SliceHeader 的一部分?jǐn)?shù)據(jù),但是丟失的那部分對(duì)我們程序正常運(yùn)行是沒(méi)有任何影響的。
在這個(gè)用法中,hdr.Data 實(shí)際上是引用字符串頭中的基礎(chǔ)指針的另一種方式,而不是 uintptr 變量本身。
(我們這里也是使用了 uintptr 表達(dá)式,而不是一個(gè)存儲(chǔ)了 uintptr 類型的變量)
通常來(lái)說(shuō),reflect.SliceHeader 和 reflect.StringHeader 通常用在指向?qū)嶋H切片或者字符串的*reflect.SliceHeader 和 *reflect.StringHeader,永遠(yuǎn)不會(huì)被當(dāng)作普通結(jié)構(gòu)體使用。
程序不應(yīng)該聲明或者分配這些結(jié)構(gòu)體類型的變量,下面的寫(xiě)法是有風(fēng)險(xiǎn)的。
// 無(wú)效: 直接聲明的 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)來(lái)說(shuō),就是對(duì) unsafe.Pointer 做算術(shù)運(yùn)算的,上面我們說(shuō)過(guò) unsafe.Pointer 是不能直接進(jìn)行算術(shù)運(yùn)算的,因此需要先轉(zhuǎn)換為 uintptr 然后再進(jìn)行算術(shù)運(yùn)算,算完再轉(zhuǎn)換回 unsafe.Pointer 類型,所以會(huì)很繁瑣。
有了 Add 方法,我們可以寫(xiě)得簡(jiǎn)單一些,不用做 uintptr 的轉(zhuǎn)換。
有了 Add,我們可以簡(jiǎn)化一下上面那個(gè)通過(guò)數(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è)例子中,我們先是通過(guò) unsafe.Pointer(&x) 獲取到了一個(gè)指向 x 的 unsafe.Pointer 對(duì)象,然后通過(guò) 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 開(kāi)頭,長(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è)地址開(kāi)始取了 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。
如果我們按照下面這樣寫(xiě),得到的結(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è)很常見(jiàn)的用法
在實(shí)際應(yīng)用中,很多框架為了提高性能,在做 []byte 和 string 的切換的時(shí)候,往往會(huì)使用 unsafe.Pointer 來(lái)實(shí)現(xiàn)(比如 gin 框架):
下面這個(gè)例子實(shí)現(xiàn)了 []byte 到 string 的轉(zhuǎn)換,而且避免了內(nèi)存分配。這是因?yàn)?,切片和字符串的?nèi)存布局是一致的,只不過(guò)切片比字符串占用
的空間多了一點(diǎn),還有一個(gè) cap 容量字段,用來(lái)表示切片的容量是多少。具體我們可以再看看上面的 reflect.SliceHeader 和 reflect.StringHeader,
在下面這個(gè)字節(jié)切片到字符串的轉(zhuǎn)換過(guò)程中,是從占用空間更大的類型轉(zhuǎn)換為占用空間更小的類型,所以是安全的,丟失的那個(gè) cap 對(duì)我們程序正常運(yùn)行無(wú)影響。
先看看 []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)換過(guò)程中,其實(shí)只是將 b 表示的類型轉(zhuǎn)由 []byte 轉(zhuǎn)換為了 string,之所以可以這么轉(zhuǎn),是因?yàn)?nbsp;[]byte 的內(nèi)存布局跟 string 的內(nèi)存布局是一樣的,但是由于字符串實(shí)際占用空間比切片類型要小(不包括其底層指針指向的內(nèi)容),所以在轉(zhuǎn)換過(guò)程中,cap 字段丟失了,但是 strin 也不需要這個(gè)字段,所以對(duì)程序運(yùn)行沒(méi)影響。
同時(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è)過(guò)程只是需要分配很小一部分內(nèi)存就可以完成了,效率比 go 自帶的轉(zhuǎn)換高。
go 里面字符串是不可變的,但 go 為了維持字符串不可變的特性,在字符串和字節(jié)切片之間轉(zhuǎn)換一般都是通過(guò)數(shù)據(jù)拷貝的方式實(shí)現(xiàn)的。
因?yàn)檫@樣就不會(huì)影響到原來(lái)的字符串或者字節(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è)類型是用來(lái)表示指針的。unsafe定義的Pointer類型是一種可以指向任何類型的指針,ArbitraryType可用于表示任意類型。- 我們通過(guò)
unsafe.Pointer修改結(jié)構(gòu)體字段的時(shí)候,要使用unsafe.Offsetof獲取結(jié)構(gòu)體的偏移量。 - 通過(guò)
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)換回去(需要注意不能使用中間變量來(lái)保存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)算,不用來(lái)回轉(zhuǎn)換類型(比如unsafe.Pointer轉(zhuǎn)換為uintptr,然后再轉(zhuǎn)換為unsafe.Pointer)。Slice函數(shù)可以獲取指針指向內(nèi)存的一部分。- 最后介紹了
string和[]byte之間通過(guò)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語(yǔ)言實(shí)現(xiàn)類似c++中的多態(tài)功能實(shí)例
Go本身不具有多態(tài)的特性,不能夠像Java、C++那樣編寫(xiě)多態(tài)類、多態(tài)方法。但是,使用Go可以編寫(xiě)具有多態(tài)功能的類綁定的方法。下面來(lái)一起看看吧2016-09-09
Golang處理parquet文件實(shí)戰(zhàn)指南
這篇文章主要給大家介紹了關(guān)于Golang處理parquet文件的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03
Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型
這篇文章主要介紹了Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
Hugo 游樂(lè)場(chǎng)內(nèi)容初始化示例詳解
這篇文章主要為大家介紹了Hugo 游樂(lè)場(chǎng)內(nèi)容初始化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
golang DNS服務(wù)器的簡(jiǎn)單實(shí)現(xiàn)操作
這篇文章主要介紹了golang DNS服務(wù)器的簡(jiǎn)單實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04

