Golang標(biāo)準(zhǔn)庫unsafe源碼解讀
引言
當(dāng)你閱讀Golang
源碼時(shí)一定遇到過unsafe.Pointer
、uintptr
、unsafe.Sizeof
等,是否很疑惑它們到底在做什么?如果不了解這些底層代碼在發(fā)揮什么作用,一定也無法了解上層應(yīng)用構(gòu)建的來由了,本篇我們來剖析下Golang
標(biāo)準(zhǔn)庫的底層包unsafe
!
unsafe包
我們基于Go1.16版本進(jìn)行剖析,按照包的簡(jiǎn)介內(nèi)容描述是:unsafe
包含的是圍繞Go程序安全相關(guān)的操作,導(dǎo)入unsafe
包后構(gòu)建的功能可能不被Go
相關(guān)兼容性支持。
這里和Java
中的unsafe
包功能類似,unsafe
包中功能主要面向Go
語言標(biāo)準(zhǔn)庫內(nèi)部使用,一般業(yè)務(wù)開發(fā)中很少用到,除非是要做基礎(chǔ)能力的鋪建,對(duì)該包的使用應(yīng)當(dāng)是非常熟悉它的特性,對(duì)使用不當(dāng)帶來的負(fù)面影響也要非常清晰。
unsafe構(gòu)成
type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
可以看到,包的構(gòu)成比較簡(jiǎn)單,下面我們主要結(jié)合源碼中注釋內(nèi)容來展開剖析和學(xué)習(xí)。
type ArbitraryType int
Arbitrary
翻譯: 隨心所欲,任意的
type ArbitraryType int
ArbitraryType
沒有什么實(shí)質(zhì)作用,它表示任意一種類型,實(shí)際上不是unsafe
包的一部分。它表示任意Go
表達(dá)式的類型。
type Pointer *ArbitraryType
type Pointer *ArbitraryType
Pointer
是unsafe
包的核心。
靈活轉(zhuǎn)換
它表示指向任意類型的指針,有四種特殊操作可用于類型指針,而其他類型不可用,大概的轉(zhuǎn)換關(guān)系如下:
- 任何類型的指針值都可以轉(zhuǎn)換為
Pointer
Pointer
可以轉(zhuǎn)換為任何類型的指針值- 任意
uintptr
可以轉(zhuǎn)換為Pointer
Pointer
也可以轉(zhuǎn)換為任意uintptr
潛在的危險(xiǎn)性
正是因?yàn)樗心芰透鞣N數(shù)據(jù)類型之間建立聯(lián)系完成轉(zhuǎn)換,Pointer
通常被認(rèn)為是較為危險(xiǎn)的,它能允許程序侵入系統(tǒng)并讀取和寫入任意內(nèi)存,使用時(shí)應(yīng)格外小心?。?!
源碼注釋中列舉了提到了一些正確和錯(cuò)誤使用的例子。它還提到更為重要的一點(diǎn)是:不使用這些模式的代碼可能現(xiàn)在或者將來變成無效。即使下面的有效模式也有重要的警告。試圖來理解下這句話的核心就是,它不能對(duì)你提供什么保證!
對(duì)于編碼的正確性還可以通過運(yùn)行Golang提供的工具“go vet
”可以幫助找到不符合這些模式的指針用法,但“go vet
”并不能保證代碼一定一定是有效的。
go vet
是golang
中自帶的靜態(tài)分析工具,可以幫助檢測(cè)編寫代碼中一些隱含的錯(cuò)誤并給出提示。比如下面故意編寫一個(gè)帶有錯(cuò)誤的代碼,fmt.Printf
中%d
需要填寫數(shù)值類型,為了驗(yàn)證go vet
效果,故意填寫字符串類型看看靜態(tài)分析效果。
代碼樣例:
func TestErr(t *testing.T) {
fmt.Printf("%d","hello world")
}
運(yùn)行:
`go vet unsafe/unsafe_test.go`
控制臺(tái)輸出提示:
unsafe/unsafe_test.go:9:2: Printf format %d has arg "hello world" of wrong type string
? 正確的使用姿勢(shì)
以下涉及Pointer
的模式是有效的,這里給出幾個(gè)例子:
- (1) 指針 *T1 轉(zhuǎn)化為 指針 *T2. T1、T2兩個(gè)變量共享等值的內(nèi)存空間布局,在不超過數(shù)據(jù)范圍的前提下,可以允許將一種類型的數(shù)據(jù)重新轉(zhuǎn)換、解釋為其他類型的數(shù)據(jù)。
下面我們操作一個(gè)樣例:聲明并開辟一個(gè)內(nèi)存空間,然后基于該內(nèi)存空間進(jìn)行不同類型數(shù)據(jù)的轉(zhuǎn)換。
代碼如下:
// 步驟: // (1) 聲明為一個(gè)int64類型 // (2) int64 -> float32 //(3) float32 -> int32 func TestPointerTypeConvert(t *testing.T) { // (1) 聲明為一個(gè)int64類型 int64Value := int64(20) // int64數(shù)據(jù)打印 fmt.Println("int64類型的值:", int64Value) //打?。篿nt64類型的值: 20 fmt.Println("int64類型的指針地址:", &int64Value) //打印:int64類型的指針地址: 0xc000128218 // (2) int64 -> float32 float32Ptr := (*float32)(unsafe.Pointer(&int64Value)) fmt.Println("float32類型的值:", *(*float32)(unsafe.Pointer(&int64Value))) //打?。篺loat32類型的值: 2.8e-44 fmt.Println("float32類型的指針地址:", (*float32)(unsafe.Pointer(&int64Value))) //打?。篺loat32類型的指針地址: 0xc000128218 // (3) float32 -> int32 fmt.Println("int32類型的指針:", (*int32)(unsafe.Pointer(float32Ptr))) //打?。篿nt32類型的指針: 0xc000128218 fmt.Println("int32類型的值:", *(*int32)(unsafe.Pointer(float32Ptr))) //打?。篿nt32類型的值: 20 }
小結(jié) Pointer
利用能夠和不同數(shù)據(jù)類型之間進(jìn)行轉(zhuǎn)換的靈活特性,可以有效進(jìn)行完成數(shù)據(jù)轉(zhuǎn)換、指針復(fù)制的功能
(2) Pointer 轉(zhuǎn)換為 uintptr(不包括返回的轉(zhuǎn)換)
- 將指針轉(zhuǎn)換為
uintptr
將生成指向的值的內(nèi)存地址,該地址為整數(shù)。 - 這種
uintptr
通常用于打印。將uintptr
轉(zhuǎn)換回指針通常無效,uintptr
是整數(shù),而不是引用。 - 將指針轉(zhuǎn)換為
uintptr
將創(chuàng)建一個(gè)沒有指針語義的整數(shù)值。即使uintptr
包含某個(gè)對(duì)象的地址,如果對(duì)象移動(dòng),垃圾收集器不會(huì)更新uintptr
的值,uintptr
也不會(huì)阻止對(duì)象被回收。 - 其余模式枚舉從
uintptr
到指針的唯一有效轉(zhuǎn)換。
(3) Pointer 轉(zhuǎn)換為 uintptr(包含返回的轉(zhuǎn)換,使用算術(shù)) 如果變量p指向一個(gè)分配的對(duì)象,它可以通過該對(duì)象轉(zhuǎn)換為uintptr
,添加偏移量,并轉(zhuǎn)換回指針。
// (1) 聲明一個(gè)數(shù)組,持有兩個(gè)元素 // (2) 輸出第1個(gè)元素指針信息 // (3) 輸出第2個(gè)元素指針信息 // (4) 通過第一個(gè)元素指針地址加上偏移量可以得到第二個(gè)元素地址 // (5) 還原第二個(gè)元素的值 func TestUintptrWithOffset(t *testing.T) { // (1) 聲明一個(gè)數(shù)組,持有兩個(gè)元素 p := []int{1,2} // (2) 輸出第1個(gè)元素指針信息 fmt.Println("p[0]的指針地址:",&p[0]) // p[0]的指針地址 0xc0000a0160 ptr0 := uintptr(unsafe.Pointer(&p[0])) fmt.Println(ptr0) // 824634376544 // (3) 輸出第2個(gè)元素指針信息 fmt.Println("p[1]的指針地址:",&p[1]) // p[1]的指針地址 0xc0000a0168 ptr1 := uintptr(unsafe.Pointer(&p[1])) fmt.Println(ptr1) // 824634376552 // (4) 通過第一個(gè)元素指針地址加上偏移量可以得到第二個(gè)元素指針地址 offset := uintptr(unsafe.Pointer(&p[0])) + 8 //int類型占8字節(jié) ptr1ByOffset := unsafe.Pointer(offset) fmt.Println("p[0]的指針地址 + offset偏移量可以得到p[1]的指針地址:",ptr1ByOffset) // p[0]的指針地址 + offset偏移量可以得到p[1]的指針地址 0xc0000a0168 // (5) 還原第二個(gè)元素的值 fmt.Println("通過偏移量得到的指針地址還原值:",*(*int)(ptr1ByOffset)) // 通過偏移量得到的指針地址還原值:2 }
小結(jié)
最常見的用途是訪問結(jié)構(gòu)或數(shù)組元素中的字段:
- 從指針添加、減去偏移量都是可操作的
- 使用
&^
對(duì)指針進(jìn)行舍入也是有效的,通常用于對(duì)齊 - 要保證內(nèi)存偏移量指向正確,指向有效的原始分配的對(duì)象的偏移量上
? 錯(cuò)誤的使用姿勢(shì)
與C中不同的是,將指針指向到其原始分配結(jié)束之后是無效的:
//? 無效:分配空間外的端點(diǎn) func TestOverOffset(t *testing.T) { // 聲明字符串變量str str := "abc" // 在str的內(nèi)存偏移量基礎(chǔ)上增加了額外的一個(gè)偏移量得到一個(gè)新的內(nèi)存偏移量,該內(nèi)存地址是不存在的 newStr := unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof(str)) // 這里由于不存在該內(nèi)存偏移量的對(duì)象,肯定求不到值,這里的表現(xiàn)是一直阻塞等待 fmt.Println(*(*string)(newStr)) }
注意,兩個(gè)轉(zhuǎn)換必須出現(xiàn)在同一個(gè)表達(dá)式中,它們之間只有中間的算術(shù)運(yùn)算。
//? 無效:在轉(zhuǎn)換回指針之前,uintptr不能存儲(chǔ)在變量中 u := uintptr(p) p = unsafe.Pointer(u + offset) //推薦如下這種方式,不要依靠中間變量來傳遞uintptr p = unsafe.Pointer(uintptr(p) + offset)
請(qǐng)注意,指針必須指向已分配的對(duì)象,因此它不能是零。
//? 無效:零指針的轉(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)可能會(huì)根據(jù)調(diào)用的詳細(xì)信息,將其中一些重新解釋為指針。也就是說,系統(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))
編譯器處理在程序集中實(shí)現(xiàn)的函數(shù)調(diào)用的參數(shù)列表中轉(zhuǎn)換為uintptr
的指針,方法是安排保留引用的已分配對(duì)象(如果有),并在調(diào)用完成之前不移動(dòng),即使僅從類型來看,調(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) 從uintptr
到Pointer
,包含反射(Reflect
)、反射值指針(Reflect.Value.Pointer
)、反射值地址(Reflect.Value.UnsafeAddr
)的轉(zhuǎn)換結(jié)果
包reflect
的值方法名為Pointer
和UnsafeAddr
,返回類型為uintptr
,而不是unsafe
。防止調(diào)用者在不首先導(dǎo)入“unsafe
”的情況下將結(jié)果更改為任意類型的指針。然而,這意味著結(jié)果是脆弱的,必須在調(diào)用后立即在同一表達(dá)式中轉(zhuǎn)換為Pointer
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
與上述情況一樣,在轉(zhuǎn)換之前存儲(chǔ)結(jié)果是無效的
//? 無效:在轉(zhuǎn)換回指針之前,uintptr不能存儲(chǔ)在變量中,和上面提到的問題類似 u := reflect.ValueOf(new(int)).Pointer() p := (*int)(unsafe.Pointer(u))
(6)reflect.SliceHeader
或reflect.StringHeader
的數(shù)據(jù)字段與Pointer
的轉(zhuǎn)換 與前一種情況一樣,reflect.SliceHeader
、reflect.StringHeader
將字段數(shù)據(jù)聲明為uintptr
,以防止調(diào)用方在不首先導(dǎo)入“unsafe
”的情況下將結(jié)果更改為任意類型。
然而,這意味著SliceHeader
和StringHeader
僅在解釋實(shí)際切片(slice
)或字符串值(string
)的內(nèi)容時(shí)有效。
var s string hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1 hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case) hdr.Len = n
在此用法中,hdr.Data
實(shí)際上是引用字符串頭中底層指針的另一種方式,而不是uintptr
變量本身。
一般來說,reflect.SliceHeader
和reflect.StringHeader
應(yīng)該僅用作那些指向?qū)嶋H為切片(slice
)、字符串(string
)的*reflect.SliceHeader
和*reflect.StringHeader
,而不是普通的結(jié)構(gòu)體。程序不應(yīng)聲明或分配這些結(jié)構(gòu)類型的變量。
// ? 無效: 直接聲明的Header不會(huì)將數(shù)據(jù)作為引用。 var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // p可能已經(jīng)被回收
func Sizeof(x ArbitraryType) uintptr
Sizeof
返回類型v
本身數(shù)據(jù)所占用的字節(jié)數(shù)。返回值是“頂層”的數(shù)據(jù)占有的字節(jié)數(shù)。例如,若v
是一個(gè)切片,它會(huì)返回該切片描述符的大小,而非該切片底層引用的內(nèi)存的大小。
Go語言中非聚合類型通常有一個(gè)固定的大小
引用類型或包含引用類型的大小在32位平臺(tái)上是4字節(jié),在64位平臺(tái)上是8字節(jié)。
類型 | 分類 | 大小 |
---|---|---|
bool | 非聚合 | 1個(gè)字節(jié) |
intN, uintN, floatN, complexN | 非聚合 | N/8個(gè)字節(jié)(例如float64是8個(gè)字節(jié)) |
int, uint, uintptr | 非聚合 | 1個(gè)機(jī)器字 (32位系統(tǒng):1機(jī)器字=4字節(jié); 64位系統(tǒng):1機(jī)器字=8字節(jié)) |
*T | 聚合 | 1個(gè)機(jī)器字 |
string | 聚合 | 2個(gè)機(jī)器字(data,len) |
[]T | 聚合 | 3個(gè)機(jī)器字(data,len,cap) |
map | 聚合 | 1個(gè)機(jī)器字 |
func | 聚合 | 1個(gè)機(jī)器字 |
chan | 聚合 | 1個(gè)機(jī)器字 |
interface | 聚合 | 2個(gè)機(jī)器字(type,value) |
type Model struct { //Field... } func TestSizeOf(t *testing.T) { boolSize := false intSize := 1 int8Size := int8(1) int16Size := int16(1) int32Size := int32(1) int64Size := int64(1) arrSize := make([]int, 0) mapSize := make(map[string]string, 0) structSize := &Model{} funcSize := func() {} chanSize := make(chan int, 10) stringSize := "abcdefg" fmt.Println("bool sizeOf:", unsafe.Sizeof(boolSize)) //bool sizeOf: 1 fmt.Println("int sizeOf:", unsafe.Sizeof(intSize)) //int sizeOf: 8 fmt.Println("int8 sizeOf:", unsafe.Sizeof(int8Size)) //int8 sizeOf: 1 fmt.Println("int16 sizeOf:", unsafe.Sizeof(int16Size)) //int16 sizeOf: 2 fmt.Println("int32 sizeOf:", unsafe.Sizeof(int32Size)) //int32 sizeOf: 4 fmt.Println("int64 sizeOf:", unsafe.Sizeof(int64Size)) //int64 sizeOf: 8 fmt.Println("arrSize sizeOf:", unsafe.Sizeof(arrSize)) //arrSize sizeOf: 24 fmt.Println("structSize sizeOf:", unsafe.Sizeof(structSize)) //structSize sizeOf: 8 fmt.Println("mapSize sizeOf:", unsafe.Sizeof(mapSize)) //mapSize sizeOf: 8 fmt.Println("funcSize sizeOf:", unsafe.Sizeof(funcSize)) //funcSize sizeOf: 8 fmt.Println("chanSize sizeOf:", unsafe.Sizeof(chanSize)) //chanSize sizeOf: 8 fmt.Println("stringSize sizeOf:", unsafe.Sizeof(stringSize)) //stringSize sizeOf: 16 }
func Offsetof(x ArbitraryType) uintptr
Offsetof
返回類型v
所代表的結(jié)構(gòu)體字段f
在結(jié)構(gòu)體中的偏移量,它必須為結(jié)構(gòu)體類型的字段的形式。換句話說,它返回該結(jié)構(gòu)起始處與該字段起始處之間的字節(jié)數(shù)。
內(nèi)存對(duì)齊 計(jì)算機(jī)在加載和保存數(shù)據(jù)時(shí),如果內(nèi)存地址合理地對(duì)齊的將會(huì)更有效率。由于地址對(duì)齊這個(gè)因素,一個(gè)聚合類型的大小至少是所有字段或元素大小的總和,或者更大因?yàn)榭赡艽嬖趦?nèi)存空洞。\
內(nèi)存空洞 編譯器自動(dòng)添加的沒有被使用的內(nèi)存空間,用于保證后面每個(gè)字段或元素的地址相對(duì)于結(jié)構(gòu)或數(shù)組的開始地址能夠合理地對(duì)齊
下面通過排列bool、string、int16
類型字段的不同順序來演示下內(nèi)存對(duì)齊時(shí)填充的內(nèi)存空洞。
type BoolIntString struct { A bool B int16 C string } type StringIntBool struct { A string B int16 C bool } type IntStringBool struct { A int16 B string C bool } type StringBoolInt struct { A string B bool C int16 } func TestOffsetOf(t *testing.T) { bis := &BoolIntString{} isb := &IntStringBool{} sbi := &StringBoolInt{} sib := &StringIntBool{} fmt.Println(unsafe.Offsetof(bis.A)) // 0 fmt.Println(unsafe.Offsetof(bis.B)) // 2 fmt.Println(unsafe.Offsetof(bis.C)) // 8 fmt.Println("") fmt.Println(unsafe.Offsetof(isb.A)) // 0 fmt.Println(unsafe.Offsetof(isb.B)) // 8 fmt.Println(unsafe.Offsetof(isb.C)) // 24 fmt.Println("") fmt.Println(unsafe.Offsetof(sbi.A)) // 0 fmt.Println(unsafe.Offsetof(sbi.B)) // 16 fmt.Println(unsafe.Offsetof(sbi.C)) // 18 fmt.Println("") fmt.Println(unsafe.Offsetof(sib.A)) // 0 fmt.Println(unsafe.Offsetof(sib.B)) // 16 fmt.Println(unsafe.Offsetof(sib.C)) // 18 }
以上是針對(duì)單個(gè)結(jié)構(gòu)體內(nèi)的內(nèi)存對(duì)齊的測(cè)試演示,當(dāng)多個(gè)結(jié)構(gòu)體組合在一起時(shí)還會(huì)產(chǎn)生內(nèi)存對(duì)齊,感興趣可以自行實(shí)踐并打印內(nèi)存偏移量來觀察組合后產(chǎn)生的內(nèi)存空洞。
func Alignof(x ArbitraryType) uintptr
Alignof
返回類型v
的對(duì)齊方式(即類型v
在內(nèi)存中占用的字節(jié)數(shù));若是結(jié)構(gòu)體類型的字段的形式,它會(huì)返回字段f
在該結(jié)構(gòu)體中的對(duì)齊方式。
type Fields struct { Bool bool String string Int int Int8 int8 Int16 int16 Int32 int32 Float32 float32 Float64 float64 } func TestAlignof(t *testing.T) { fields := &Fields{} fmt.Println(unsafe.Alignof(fields.Bool)) // 1 fmt.Println(unsafe.Alignof(fields.String))// 8 fmt.Println(unsafe.Alignof(fields.Int)) // 8 fmt.Println(unsafe.Alignof(fields.Int8)) // 1 fmt.Println(unsafe.Alignof(fields.Int16)) // 2 fmt.Println(unsafe.Alignof(fields.Int32)) // 4 fmt.Println(unsafe.Alignof(fields.Float32)) // 4 fmt.Println(unsafe.Alignof(fields.Float64)) // 8 }
不同類型有著不同的內(nèi)存對(duì)齊方式,總體上都是以最小可容納單位進(jìn)行對(duì)齊的,這樣可以在兼顧以最小的內(nèi)存空間填充來換取內(nèi)存計(jì)算的高效性。
參考
以上就是Golang標(biāo)準(zhǔn)庫unsafe源碼解讀的詳細(xì)內(nèi)容,更多關(guān)于Golang標(biāo)準(zhǔn)庫unsafe的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go語言VScode?see?'go?help?modules'?(exit?statu
最近上手學(xué)習(xí)go語言,準(zhǔn)備在VSCode上寫程序的時(shí)候卻發(fā)現(xiàn)出了一點(diǎn)問題,下面這篇文章主要給大家介紹了關(guān)于go語言VScode?see?'go?help?modules'(exit?status?1)問題的解決過程,需要的朋友可以參考下2022-07-07Golang之casbin權(quán)限管理的實(shí)現(xiàn)
這篇文章主要介紹了Golang之casbin權(quán)限管理的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10詳解Go中g(shù)in框架如何實(shí)現(xiàn)帶顏色日志
當(dāng)我們?cè)诮K端上(比如Goland)運(yùn)行g(shù)in框架搭建的服務(wù)時(shí),會(huì)發(fā)現(xiàn)輸出的日志是可以帶顏色的,那這是如何實(shí)現(xiàn)的呢?本文就來和大家簡(jiǎn)單講講2023-04-04