<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
當你閱讀Golang
原始碼時一定遇到過unsafe.Pointer
、uintptr
、unsafe.Sizeof
等,是否很疑惑它們到底在做什麼?如果不瞭解這些底層程式碼在發揮什麼作用,一定也無法瞭解上層應用構建的來由了,本篇我們來剖析下Golang
標準庫的底層包unsafe
!
我們基於Go1.16版本進行剖析,按照包的簡介內容描述是:unsafe
包含的是圍繞Go程式安全相關的操作,匯入unsafe
包後構建的功能可能不被Go
相關相容性支援。
這裡和Java
中的unsafe
包功能類似,unsafe
包中功能主要面向Go
語言標準庫內部使用,一般業務開發中很少用到,除非是要做基礎能力的鋪建,對該包的使用應當是非常熟悉它的特性,對使用不當帶來的負面影響也要非常清晰。
type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
可以看到,包的構成比較簡單,下面我們主要結合原始碼中註釋內容來展開剖析和學習。
Arbitrary
翻譯: 隨心所欲,任意的
type ArbitraryType int
ArbitraryType
沒有什麼實質作用,它表示任意一種型別,實際上不是unsafe
包的一部分。它表示任意Go
表示式的型別。
type Pointer *ArbitraryType
Pointer
是unsafe
包的核心。
它表示指向任意型別的指標,有四種特殊操作可用於型別指標,而其他型別不可用,大概的轉換關係如下:
Pointer
Pointer
可以轉換為任何型別的指標值uintptr
可以轉換為Pointer
Pointer
也可以轉換為任意uintptr
正是因為它有能力和各種資料型別之間建立聯絡完成轉換,Pointer
通常被認為是較為危險的,它能允許程式侵入系統並讀取和寫入任意記憶體,使用時應格外小心!!!
原始碼註釋中列舉了提到了一些正確和錯誤使用的例子。它還提到更為重要的一點是:不使用這些模式的程式碼可能現在或者將來變成無效。即使下面的有效模式也有重要的警告。試圖來理解下這句話的核心就是,它不能對你提供什麼保證!
對於編碼的正確性還可以通過執行Golang提供的工具“go vet
”可以幫助找到不符合這些模式的指標用法,但“go vet
”並不能保證程式碼一定一定是有效的。
go vet
是golang
中自帶的靜態分析工具,可以幫助檢測編寫程式碼中一些隱含的錯誤並給出提示。比如下面故意編寫一個帶有錯誤的程式碼,fmt.Printf
中%d
需要填寫數值型別,為了驗證go vet
效果,故意填寫字串型別看看靜態分析效果。
程式碼樣例:
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) 宣告為一個int64型別 // (2) int64 -> float32 //(3) float32 -> int32 func TestPointerTypeConvert(t *testing.T) { // (1) 宣告為一個int64型別 int64Value := int64(20) // int64資料列印 fmt.Println("int64型別的值:", int64Value) //列印:int64型別的值: 20 fmt.Println("int64型別的指標地址:", &int64Value) //列印:int64型別的指標地址: 0xc000128218 // (2) int64 -> float32 float32Ptr := (*float32)(unsafe.Pointer(&int64Value)) fmt.Println("float32型別的值:", *(*float32)(unsafe.Pointer(&int64Value))) //列印:float32型別的值: 2.8e-44 fmt.Println("float32型別的指標地址:", (*float32)(unsafe.Pointer(&int64Value))) //列印:float32型別的指標地址: 0xc000128218 // (3) float32 -> int32 fmt.Println("int32型別的指標:", (*int32)(unsafe.Pointer(float32Ptr))) //列印:int32型別的指標: 0xc000128218 fmt.Println("int32型別的值:", *(*int32)(unsafe.Pointer(float32Ptr))) //列印:int32型別的值: 20 }
小結 Pointer
利用能夠和不同資料型別之間進行轉換的靈活特性,可以有效進行完成資料轉換、指標複製的功能
(2) Pointer 轉換為 uintptr(不包括返回的轉換)
uintptr
將生成指向的值的記憶體地址,該地址為整數。uintptr
通常用於列印。將uintptr
轉換回指標通常無效,uintptr
是整數,而不是參照。uintptr
將建立一個沒有指標語意的整數值。即使uintptr
包含某個物件的地址,如果物件移動,垃圾收集器不會更新uintptr
的值,uintptr
也不會阻止物件被回收。uintptr
到指標的唯一有效轉換。(3) Pointer 轉換為 uintptr(包含返回的轉換,使用算術) 如果變數p指向一個分配的物件,它可以通過該物件轉換為uintptr
,新增偏移量,並轉換回指標。
// (1) 宣告一個陣列,持有兩個元素 // (2) 輸出第1個元素指標資訊 // (3) 輸出第2個元素指標資訊 // (4) 通過第一個元素指標地址加上偏移量可以得到第二個元素地址 // (5) 還原第二個元素的值 func TestUintptrWithOffset(t *testing.T) { // (1) 宣告一個陣列,持有兩個元素 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位元組 ptr1ByOffset := unsafe.Pointer(offset) fmt.Println("p[0]的指標地址 + offset偏移量可以得到p[1]的指標地址:",ptr1ByOffset) // p[0]的指標地址 + offset偏移量可以得到p[1]的指標地址 0xc0000a0168 // (5) 還原第二個元素的值 fmt.Println("通過偏移量得到的指標地址還原值:",*(*int)(ptr1ByOffset)) // 通過偏移量得到的指標地址還原值:2 }
小結
最常見的用途是存取結構或陣列元素中的欄位:
&^
對指標進行舍入也是有效的,通常用於對齊與C中不同的是,將指標指向到其原始分配結束之後是無效的:
//❌ 無效:分配空間外的端點 func TestOverOffset(t *testing.T) { // 宣告字串變數str str := "abc" // 在str的記憶體偏移量基礎上增加了額外的一個偏移量得到一個新的記憶體偏移量,該記憶體地址是不存在的 newStr := unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof(str)) // 這裡由於不存在該記憶體偏移量的物件,肯定求不到值,這裡的表現是一直阻塞等待 fmt.Println(*(*string)(newStr)) }
注意,兩個轉換必須出現在同一個表示式中,它們之間只有中間的算術運算。
//❌ 無效:在轉換回指標之前,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)
syscall.Syscall
時將指標轉換為uintptr
syscall
包中的Syscall
函數將其uintptr
引數直接傳遞給作業系統,然後作業系統可能會根據呼叫的詳細資訊,將其中一些重新解釋為指標。也就是說,系統呼叫實現隱式地將某些引數從uintptr
轉換回指標。如果必須將指標引數轉換為uintptr
以用作引數,則該轉換必須出現在呼叫表示式本身之中:
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
編譯器處理在程式集中實現的函數呼叫的參數列中轉換為uintptr
的指標,方法是安排保留參照的已分配物件(如果有),並在呼叫完成之前不移動,即使僅從型別來看,呼叫期間似乎不再需要該物件。
要使編譯器識別此模式,轉換必須出現在參數列中:
//❌ 無效:在系統呼叫期間隱式轉換回指標之前,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
的資料欄位與Pointer
的轉換 與前一種情況一樣,reflect.SliceHeader
、reflect.StringHeader
將欄位資料宣告為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不會將資料作為參照。 var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // p可能已經被回收
Sizeof
返回型別v
本身資料所佔用的位元組數。返回值是“頂層”的資料佔有的位元組數。例如,若v
是一個切片,它會返回該切片描述符的大小,而非該切片底層參照的記憶體的大小。
Go語言中非聚合型別通常有一個固定的大小
參照型別或包含參照型別的大小在32位元平臺上是4位元組,在64位元平臺上是8位元組。
型別 | 分類 | 大小 |
---|---|---|
bool | 非聚合 | 1個位元組 |
intN, uintN, floatN, complexN | 非聚合 | N/8個位元組(例如float64是8個位元組) |
int, uint, uintptr | 非聚合 | 1個機器字 (32位元系統:1機器字=4位元組; 64位元系統:1機器字=8位元組) |
*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 }
Offsetof
返回型別v
所代表的結構體欄位f
在結構體中的偏移量,它必須為結構體型別的欄位的形式。換句話說,它返回該結構起始處與該欄位起始處之間的位元組數。
記憶體對齊 計算機在載入和儲存資料時,如果記憶體地址合理地對齊的將會更有效率。由於地址對齊這個因素,一個聚合型別的大小至少是所有欄位或元素大小的總和,或者更大因為可能存在記憶體空洞。
記憶體空洞 編譯器自動新增的沒有被使用的記憶體空間,用於保證後面每個欄位或元素的地址相對於結構或陣列的開始地址能夠合理地對齊
下面通過排列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 }
以上是針對單個結構體內的記憶體對齊的測試演示,當多個結構體組合在一起時還會產生記憶體對齊,感興趣可以自行實踐並列印記憶體偏移量來觀察組合後產生的記憶體空洞。
Alignof
返回型別v
的對齊方式(即型別v
在記憶體中佔用的位元組數);若是結構體型別的欄位的形式,它會返回欄位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的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45