Golang標準庫unsafe源碼解讀
引言
當你閱讀Golang源碼時一定遇到過unsafe.Pointer、uintptr、unsafe.Sizeof等,是否很疑惑它們到底在做什么?如果不了解這些底層代碼在發(fā)揮什么作用,一定也無法了解上層應用構建的來由了,本篇我們來剖析下Golang標準庫的底層包unsafe!
unsafe包
我們基于Go1.16版本進行剖析,按照包的簡介內容描述是:unsafe包含的是圍繞Go程序安全相關的操作,導入unsafe包后構建的功能可能不被Go相關兼容性支持。
這里和Java中的unsafe包功能類似,unsafe包中功能主要面向Go語言標準庫內部使用,一般業(yè)務開發(fā)中很少用到,除非是要做基礎能力的鋪建,對該包的使用應當是非常熟悉它的特性,對使用不當帶來的負面影響也要非常清晰。
unsafe構成
type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
可以看到,包的構成比較簡單,下面我們主要結合源碼中注釋內容來展開剖析和學習。
type ArbitraryType int
Arbitrary翻譯: 隨心所欲,任意的
type ArbitraryType int
ArbitraryType沒有什么實質作用,它表示任意一種類型,實際上不是unsafe包的一部分。它表示任意Go表達式的類型。
type Pointer *ArbitraryType
type Pointer *ArbitraryType
Pointer是unsafe包的核心。
靈活轉換

它表示指向任意類型的指針,有四種特殊操作可用于類型指針,而其他類型不可用,大概的轉換關系如下:
- 任何類型的指針值都可以轉換為
Pointer Pointer可以轉換為任何類型的指針值- 任意
uintptr可以轉換為Pointer Pointer也可以轉換為任意uintptr
潛在的危險性
正是因為它有能力和各種數(shù)據(jù)類型之間建立聯(lián)系完成轉換,Pointer通常被認為是較為危險的,它能允許程序侵入系統(tǒng)并讀取和寫入任意內存,使用時應格外小心?。?!
源碼注釋中列舉了提到了一些正確和錯誤使用的例子。它還提到更為重要的一點是:不使用這些模式的代碼可能現(xiàn)在或者將來變成無效。即使下面的有效模式也有重要的警告。試圖來理解下這句話的核心就是,它不能對你提供什么保證!
對于編碼的正確性還可以通過運行Golang提供的工具“go vet”可以幫助找到不符合這些模式的指針用法,但“go vet”并不能保證代碼一定一定是有效的。
go vet是golang中自帶的靜態(tài)分析工具,可以幫助檢測編寫代碼中一些隱含的錯誤并給出提示。比如下面故意編寫一個帶有錯誤的代碼,fmt.Printf中%d需要填寫數(shù)值類型,為了驗證go vet效果,故意填寫字符串類型看看靜態(tài)分析效果。
代碼樣例:
func TestErr(t *testing.T) {
fmt.Printf("%d","hello world")
}
運行:
`go vet unsafe/unsafe_test.go`
控制臺輸出提示:
unsafe/unsafe_test.go:9:2: Printf format %d has arg "hello world" of wrong type string
? 正確的使用姿勢
以下涉及Pointer的模式是有效的,這里給出幾個例子:
- (1) 指針 *T1 轉化為 指針 *T2. T1、T2兩個變量共享等值的內存空間布局,在不超過數(shù)據(jù)范圍的前提下,可以允許將一種類型的數(shù)據(jù)重新轉換、解釋為其他類型的數(shù)據(jù)。
下面我們操作一個樣例:聲明并開辟一個內存空間,然后基于該內存空間進行不同類型數(shù)據(jù)的轉換。

代碼如下:
// 步驟:
// (1) 聲明為一個int64類型
// (2) int64 -> float32
//(3) float32 -> int32
func TestPointerTypeConvert(t *testing.T) {
// (1) 聲明為一個int64類型
int64Value := int64(20)
// int64數(shù)據(jù)打印
fmt.Println("int64類型的值:", int64Value)
//打?。篿nt64類型的值: 20
fmt.Println("int64類型的指針地址:", &int64Value)
//打?。篿nt64類型的指針地址: 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)))
//打印:int32類型的指針: 0xc000128218
fmt.Println("int32類型的值:", *(*int32)(unsafe.Pointer(float32Ptr)))
//打?。篿nt32類型的值: 20
}
小結 Pointer利用能夠和不同數(shù)據(jù)類型之間進行轉換的靈活特性,可以有效進行完成數(shù)據(jù)轉換、指針復制的功能
(2) Pointer 轉換為 uintptr(不包括返回的轉換)
- 將指針轉換為
uintptr將生成指向的值的內存地址,該地址為整數(shù)。 - 這種
uintptr通常用于打印。將uintptr轉換回指針通常無效,uintptr是整數(shù),而不是引用。 - 將指針轉換為
uintptr將創(chuàng)建一個沒有指針語義的整數(shù)值。即使uintptr包含某個對象的地址,如果對象移動,垃圾收集器不會更新uintptr的值,uintptr也不會阻止對象被回收。 - 其余模式枚舉從
uintptr到指針的唯一有效轉換。

(3) Pointer 轉換為 uintptr(包含返回的轉換,使用算術) 如果變量p指向一個分配的對象,它可以通過該對象轉換為uintptr,添加偏移量,并轉換回指針。
// (1) 聲明一個數(shù)組,持有兩個元素
// (2) 輸出第1個元素指針信息
// (3) 輸出第2個元素指針信息
// (4) 通過第一個元素指針地址加上偏移量可以得到第二個元素地址
// (5) 還原第二個元素的值
func TestUintptrWithOffset(t *testing.T) {
// (1) 聲明一個數(shù)組,持有兩個元素
p := []int{1,2}
// (2) 輸出第1個元素指針信息
fmt.Println("p[0]的指針地址:",&p[0])
// p[0]的指針地址 0xc0000a0160
ptr0 := uintptr(unsafe.Pointer(&p[0]))
fmt.Println(ptr0)
// 824634376544
// (3) 輸出第2個元素指針信息
fmt.Println("p[1]的指針地址:",&p[1])
// p[1]的指針地址 0xc0000a0168
ptr1 := uintptr(unsafe.Pointer(&p[1]))
fmt.Println(ptr1)
// 824634376552
// (4) 通過第一個元素指針地址加上偏移量可以得到第二個元素指針地址
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) 還原第二個元素的值
fmt.Println("通過偏移量得到的指針地址還原值:",*(*int)(ptr1ByOffset))
// 通過偏移量得到的指針地址還原值:2
}
小結
最常見的用途是訪問結構或數(shù)組元素中的字段:
- 從指針添加、減去偏移量都是可操作的
- 使用
&^對指針進行舍入也是有效的,通常用于對齊 - 要保證內存偏移量指向正確,指向有效的原始分配的對象的偏移量上
? 錯誤的使用姿勢
與C中不同的是,將指針指向到其原始分配結束之后是無效的:
//? 無效:分配空間外的端點
func TestOverOffset(t *testing.T) {
// 聲明字符串變量str
str := "abc"
// 在str的內存偏移量基礎上增加了額外的一個偏移量得到一個新的內存偏移量,該內存地址是不存在的
newStr := unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof(str))
// 這里由于不存在該內存偏移量的對象,肯定求不到值,這里的表現(xiàn)是一直阻塞等待
fmt.Println(*(*string)(newStr))
}
注意,兩個轉換必須出現(xiàn)在同一個表達式中,它們之間只有中間的算術運算。
//? 無效:在轉換回指針之前,uintptr不能存儲在變量中 u := uintptr(p) p = unsafe.Pointer(u + offset) //推薦如下這種方式,不要依靠中間變量來傳遞uintptr p = unsafe.Pointer(uintptr(p) + offset)
請注意,指針必須指向已分配的對象,因此它不能是零。
//? 無效:零指針的轉換 u := unsafe.Pointer(nil) p := unsafe.Pointer(uintptr(u) + offset)
- (4) 調用
syscall.Syscall時將指針轉換為uintptrsyscall包中的Syscall函數(shù)將其uintptr參數(shù)直接傳遞給操作系統(tǒng),然后操作系統(tǒng)可能會根據(jù)調用的詳細信息,將其中一些重新解釋為指針。也就是說,系統(tǒng)調用實現(xiàn)隱式地將某些參數(shù)從uintptr轉換回指針。
如果必須將指針參數(shù)轉換為uintptr以用作參數(shù),則該轉換必須出現(xiàn)在調用表達式本身之中:
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
編譯器處理在程序集中實現(xiàn)的函數(shù)調用的參數(shù)列表中轉換為uintptr的指針,方法是安排保留引用的已分配對象(如果有),并在調用完成之前不移動,即使僅從類型來看,調用期間似乎不再需要該對象。
要使編譯器識別此模式,轉換必須出現(xiàn)在參數(shù)列表中:
//? 無效:在系統(tǒng)調用期間隱式轉換回指針之前,uintptr不能存儲在變量中,和上面提到的問題類似 u := uintptr(unsafe.Pointer(p)) syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))
(5) 從uintptr到Pointer,包含反射(Reflect)、反射值指針(Reflect.Value.Pointer)、反射值地址(Reflect.Value.UnsafeAddr)的轉換結果
包reflect的值方法名為Pointer和UnsafeAddr,返回類型為uintptr,而不是unsafe。防止調用者在不首先導入“unsafe”的情況下將結果更改為任意類型的指針。然而,這意味著結果是脆弱的,必須在調用后立即在同一表達式中轉換為Pointer
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
與上述情況一樣,在轉換之前存儲結果是無效的
//? 無效:在轉換回指針之前,uintptr不能存儲在變量中,和上面提到的問題類似 u := reflect.ValueOf(new(int)).Pointer() p := (*int)(unsafe.Pointer(u))
(6)reflect.SliceHeader或reflect.StringHeader的數(shù)據(jù)字段與Pointer的轉換 與前一種情況一樣,reflect.SliceHeader、reflect.StringHeader將字段數(shù)據(jù)聲明為uintptr,以防止調用方在不首先導入“unsafe”的情況下將結果更改為任意類型。
然而,這意味著SliceHeader和StringHeader僅在解釋實際切片(slice)或字符串值(string)的內容時有效。
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實際上是引用字符串頭中底層指針的另一種方式,而不是uintptr變量本身。
一般來說,reflect.SliceHeader和reflect.StringHeader應該僅用作那些指向實際為切片(slice)、字符串(string)的*reflect.SliceHeader和*reflect.StringHeader,而不是普通的結構體。程序不應聲明或分配這些結構類型的變量。
// ? 無效: 直接聲明的Header不會將數(shù)據(jù)作為引用。 var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // p可能已經被回收
func Sizeof(x ArbitraryType) uintptr
Sizeof返回類型v本身數(shù)據(jù)所占用的字節(jié)數(shù)。返回值是“頂層”的數(shù)據(jù)占有的字節(jié)數(shù)。例如,若v是一個切片,它會返回該切片描述符的大小,而非該切片底層引用的內存的大小。
Go語言中非聚合類型通常有一個固定的大小
引用類型或包含引用類型的大小在32位平臺上是4字節(jié),在64位平臺上是8字節(jié)。
| 類型 | 分類 | 大小 |
|---|---|---|
| bool | 非聚合 | 1個字節(jié) |
| intN, uintN, floatN, complexN | 非聚合 | N/8個字節(jié)(例如float64是8個字節(jié)) |
| int, uint, uintptr | 非聚合 | 1個機器字 (32位系統(tǒng):1機器字=4字節(jié); 64位系統(tǒng):1機器字=8字節(jié)) |
| *T | 聚合 | 1個機器字 |
| string | 聚合 | 2個機器字(data,len) |
| []T | 聚合 | 3個機器字(data,len,cap) |
| map | 聚合 | 1個機器字 |
| func | 聚合 | 1個機器字 |
| chan | 聚合 | 1個機器字 |
| interface | 聚合 | 2個機器字(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所代表的結構體字段f在結構體中的偏移量,它必須為結構體類型的字段的形式。換句話說,它返回該結構起始處與該字段起始處之間的字節(jié)數(shù)。
內存對齊 計算機在加載和保存數(shù)據(jù)時,如果內存地址合理地對齊的將會更有效率。由于地址對齊這個因素,一個聚合類型的大小至少是所有字段或元素大小的總和,或者更大因為可能存在內存空洞。\
內存空洞 編譯器自動添加的沒有被使用的內存空間,用于保證后面每個字段或元素的地址相對于結構或數(shù)組的開始地址能夠合理地對齊

下面通過排列bool、string、int16類型字段的不同順序來演示下內存對齊時填充的內存空洞。
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
}
以上是針對單個結構體內的內存對齊的測試演示,當多個結構體組合在一起時還會產生內存對齊,感興趣可以自行實踐并打印內存偏移量來觀察組合后產生的內存空洞。
func Alignof(x ArbitraryType) uintptr
Alignof返回類型v的對齊方式(即類型v在內存中占用的字節(jié)數(shù));若是結構體類型的字段的形式,它會返回字段f在該結構體中的對齊方式。
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
}
不同類型有著不同的內存對齊方式,總體上都是以最小可容納單位進行對齊的,這樣可以在兼顧以最小的內存空間填充來換取內存計算的高效性。
參考
以上就是Golang標準庫unsafe源碼解讀的詳細內容,更多關于Golang標準庫unsafe的資料請關注腳本之家其它相關文章!
相關文章
go語言VScode?see?'go?help?modules'?(exit?statu
最近上手學習go語言,準備在VSCode上寫程序的時候卻發(fā)現(xiàn)出了一點問題,下面這篇文章主要給大家介紹了關于go語言VScode?see?'go?help?modules'(exit?status?1)問題的解決過程,需要的朋友可以參考下2022-07-07

