欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解go unsafe用法及注意事項(xiàng)

 更新時(shí)間:2024年01月09日 14:48:05   作者:rubys007  
go雖然是一種高級(jí)語言,但是也還是給開發(fā)者提供了指針的類型unsafe.Pointer,我們可以通過它來直接讀寫變量的內(nèi)存,本文來了解一下?unsafe?里所能提供的關(guān)于指針的一些功能,以及使用unsafe.Pointer的一些注意事項(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 *intSizeof(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 => uintptruintptr => 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語言實(shí)現(xiàn)類似c++中的多態(tài)功能實(shí)例

    Go本身不具有多態(tài)的特性,不能夠像Java、C++那樣編寫多態(tài)類、多態(tài)方法。但是,使用Go可以編寫具有多態(tài)功能的類綁定的方法。下面來一起看看吧
    2016-09-09
  • Go語言中的方法定義用法分析

    Go語言中的方法定義用法分析

    這篇文章主要介紹了Go語言中的方法定義用法,實(shí)例分析了方法的定義及使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • Golang處理parquet文件實(shí)戰(zhàn)指南

    Golang處理parquet文件實(shí)戰(zhàn)指南

    這篇文章主要給大家介紹了關(guān)于Golang處理parquet文件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2023-03-03
  • go xorm存庫處理null值問題

    go xorm存庫處理null值問題

    這篇文章主要介紹了go xorm存庫處理null值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Go語言基礎(chǔ)枚舉的用法及示例詳解

    Go語言基礎(chǔ)枚舉的用法及示例詳解

    這篇文章主要為大家介紹了Go語言基礎(chǔ)枚舉的用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2021-11-11
  • go-zero服務(wù)部署配置及源碼解讀

    go-zero服務(wù)部署配置及源碼解讀

    這篇文章主要為大家介紹了go-zero服務(wù)部署配置及源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • GO語言操作Elasticsearch示例分享

    GO語言操作Elasticsearch示例分享

    這篇文章主要介紹了GO語言操作Elasticsearch示例分享的相關(guān)資料,需要的朋友可以參考下
    2023-01-01
  • Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型

    Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型

    這篇文章主要介紹了Golang 實(shí)現(xiàn)interface類型轉(zhuǎn)string類型的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Hugo 游樂場(chǎng)內(nèi)容初始化示例詳解

    Hugo 游樂場(chǎng)內(nèi)容初始化示例詳解

    這篇文章主要為大家介紹了Hugo 游樂場(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)操作

    這篇文章主要介紹了golang DNS服務(wù)器的簡(jiǎn)單實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04

最新評(píng)論