在一些底層的庫中, 經常會看到使用 unsafe 套件的地方。這篇文章就來帶大家了解Golang中的unsafe包,介紹一下unsafe 包的功能和Pointer的使用方式,希望對大家有幫助!
unsafe 套件提供了一些操作可以繞過 go 的類型安全檢查, 從而直接操作記憶體位址, 做一些 tricky 操作。範例程式碼運行環境是
go version go1.18 darwin/amd64
##unsafe 套件提供了Sizeof 方法取得變數佔用記憶體大小「不包含指標指向變數的記憶體大小」, Alignof 取得記憶體對齊係數, 特定記憶體對齊規則可以自行google.type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8 } type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4 } type demo3 struct { // 64 位操作系统, 字长 8 a *demo1 // 8 b *demo2 // 8 } func MemAlign() { fmt.Println(unsafe.Sizeof(demo1{}), unsafe.Alignof(demo1{}), unsafe.Alignof(demo1{}.a), unsafe.Alignof(demo1{}.b), unsafe.Alignof(demo1{}.c)) // 16,8,1,4,8 fmt.Println(unsafe.Sizeof(demo2{}), unsafe.Alignof(demo2{}), unsafe.Alignof(demo2{}.a), unsafe.Alignof(demo2{}.b), unsafe.Alignof(demo2{}.c)) // 24,8,1,4,8 fmt.Println(unsafe.Sizeof(demo3{})) // 16 } // 16}复制代码
從上面case 可以看到demo1 和demo2 包含相同的屬性, 只是定義的屬性順序不同, 卻導致變數的記憶體大小不同。這裡是因為發生了記憶體對齊。 電腦在處理任務時, 會依照特定的字長「例如:32 位元作業系統, 字長為 4; 64 位元作業系統, 字長為 8」為單位處理資料。那麼, 在讀取資料的時候也是按照字長為單位。例如: 對於 64 位元作業系統, 程式一次讀取的位元組數為 8 的倍數。下面是demo1 在非記憶體對齊和記憶體對齊下的版面:非記憶體對齊:變數c 會被放在不同的字長裡面, cpu 在讀取的時候需要同時讀取兩次, 同時對兩次的結果做處理, 才能拿到c 的值。這種方式雖然節省了記憶體空間, 但是會增加處理時間。 記憶體對齊:記憶體對齊採用了一種方案, 可以避免同一個非記憶體對齊的這種情況, 但是會額外佔用一些空間「空間換時間」。特定記憶體對齊規則可以自行 google。Unsafe Pointer
#在go 中可以宣告一個指標型別, 這裡的型別是safe pointer, 即要明確指標指向的類型, 如果類型不匹配將會在編譯時報錯。如下面的範例, 編譯器會認為 MyString 和 string 是不同的型別, 無法進行賦值。func main() { type MyString string s := "test" var ms MyString = s // Cannot use 's' (type string) as the type MyString fmt.Println(ms) }
那有沒有一種型別, 可以指向任意型別的變數呢?可以使用 unsfe.Pointer, 它可以指向任意類型的變數。透過Pointer 的宣告, 可以知道它是一個指標類型, 指向變數所在的位址。具體的位址對應的值可以透過 uinptr 進行轉換。 Pointer 有以下四個特殊的操作:Pointer 的六種使用方式任意型別的指標都可以轉換成Pointer 類型
- Pointer 類型的變數可以轉換成任意型別的指標
- #uintptr 類型的變數可以轉換成Pointer 類型
- Pointer 類型的變數可以轉換成uintprt 類型
type Pointer *ArbitraryType // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr func main() { d := demo1{true, 1, 2} p := unsafe.Pointer(&d) // 任意类型的指针可以转换为 Pointer 类型 pa := (*demo1)(p) // Pointer 类型变量可以转换成 demo1 类型的指针 up := uintptr(p) // Pointer 类型的变量可以转换成 uintprt 类型 pu := unsafe.Pointer(up) // uintptr 类型的变量可以转换成 Pointer 类型; 当 GC 时, d 的地址可能会发生变更, 因此, 这里的 up 可能会失效 fmt.Println(d.a, pa.a, (*demo1)(pu).a) // true true true }
#在官方文件中給出了Pointer 的六種使用姿勢。
Pointer 直接指向一塊記憶體, 因此可以將此區塊記憶體位址轉為任意型別。這裡需要注意, T1 和 T2 需要有相同的記憶體佈局, 會有異常資料。func main() { type myStr string ms := []myStr{"1", "2"} //ss := ([]string)(ms) Cannot convert an expression of the type '[]myStr' to the type '[]string' ss := *(*[]string)(unsafe.Pointer(&ms)) // 将 pointer 指向的内存地址直接转换成 *[]string fmt.Println(ms, ss) }
如果 T1 和 T2 的記憶體佈局不同, 會發生什麼事呢?在下面的範例子中, demo1 和 demo2 雖然包含相同的結構體, 由於記憶體對齊, 導致兩者是不同的記憶體佈局。將Pointer 轉換時, 會從demo1 的位址開始讀取24「sizeof」 個位元組, 依照demo2 記憶體對齊規則轉換, 將第一個位元組轉換為a:true, 8-16 個位元組轉換為c :2, 16-24 個位元組超出了demo1 的範圍, 但仍可以直接讀取, 取得了非預期的值b:17368000。type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8 } type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4 } func main() { d := demo1{true, 1, 2} pa := (*demo2)(unsafe.Pointer(&d)) // Pointer 类型变量可以转换成 demo2 类型的指针 fmt.Println(pa.a, pa.b, pa.c) // true, 17368000, 2, }
#Pointer 是一個指標型別, 可以指向任意變數, 可以透過將Pointer 轉換為uintptr 來列印Pointer 指向變數的位址。此外:不應該將 uintptr 轉換為 Pointer。如下面的範例: 當發生 GC 時, d 的位址可能會發生變更, 那麼 up 由於未同步更新而指向錯誤的記憶體。
func main() { d := demo1{true, 1, 2} p := unsafe.Pointer(&d) up := uintptr(p) fmt.Printf("uintptr: %x, ptr: %p \n", up, &d) // uintptr: c00010c010, ptr: 0xc00010c010 fmt.Println(*(*demo1)(unsafe.Pointer(up))) // 不允许 }
当 Piointer 指向一个结构体时, 可以通过此方式获取到结构体内部特定属性的 Pointer。
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b)) pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b)) fmt.Println(pb) }
前面说过, 由于 GC 会导致变量的地址发生变更, 因此不可以直接处理 uintptr。但是, 在调用 syscall.Syscall 时候可以允许传递一个 uintptr, 这里可以简单理解为是编译器做了特殊处理, 来保证 uintptr 是安全的。
- 调用方式:
- syscall.Syscall(SYS_READ, uintptr( fd ), uintptr(unsafe.Pointer(p)), uintptr(n))
下面这种方式是不允许的:
u := uintptr(unsafe.Pointer(p)) // 不应该保存到一个变量上 syscall.Syscall(SYS_READ, uintptr( fd ), u, uintptr(n))
在 reflect 包中的 Value.Pointer 和 Value.UnsafeAddr 直接返回了地址对应的值「uintptr」, 可以直接将其结果转为 Pointer
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b)) pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b)) // up := reflect.ValueOf(&d.b).Pointer(), pc := unsafe.Pointer(up); 不安全, 不应存储到变量中 pc := unsafe.Pointer(reflect.ValueOf(&d.b).Pointer()) fmt.Println(pb, pc) }
SliceHeader 和 StringHeader 其实是 slice 和 string 的内部实现, 里面都包含了一个字段 Data「uintptr」, 存储的是指向 []T 的地址, 这里之所以使用 uinptr 是为了不依赖 unsafe 包。
func main() { s := "a" hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // *string to *StringHeader fmt.Println(*(*[1]byte)(unsafe.Pointer(hdr.Data))) // 底层存储的是 utf 编码后的 byte 数组 arr := [1]byte{65} hdr.Data = uintptr(unsafe.Pointer(&arr)) hdr.Len = len(arr) ss := *(*string)(unsafe.Pointer(hdr)) fmt.Println(ss) // A arr[0] = 66 fmt.Println(ss) //B }
在业务上, 经常遇到 string 和 []byte 的相互转换。我们知道, string 底层其实也是存储的一个 byte 数组, 可以通过 reflect 直接获取 string 指向的 byte 数组, 赋值给 byte 切片, 避免内存拷贝。
func StrToByte(str string) []byte { return []byte(str) } func StrToByteV2(str string) (b []byte) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := (*reflect.StringHeader)(unsafe.Pointer(&str)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b } // go test -bench . func BenchmarkStrToArr(b *testing.B) { for i := 0; i < b.N; i++ { StrToByte(`{"f": "v"}`) } } func BenchmarkStrToArrV2(b *testing.B) { for i := 0; i < b.N; i++ { StrToByteV2(`{"f": "v"}`) } } //goos: darwin //goarch: amd64 //pkg: github.com/demo/lsafe //cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz //BenchmarkStrToArr-12 264733503 4.311 ns/op //BenchmarkStrToArrV2-12 1000000000 0.2528 ns/op
通过观察 string 和 byte 的内存布局我们可以知道, 无法直接将 string 转为 []byte 「确实 cap 字段」, 但是可以直接将 []byte 转为 string
func ByteToStr(b []byte) string { return string(b) } func ByteToStrV2(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } // go test -bench . func BenchmarkArrToStr(b *testing.B) { for i := 0; i < b.N; i++ { ByteToStr([]byte{65}) } } func BenchmarkArrToStrV2(b *testing.B) { for i := 0; i < b.N; i++ { ByteToStrV2([]byte{65}) } } //goos: darwin //goarch: amd64 //pkg: github.com/demo/lsafe //cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz //BenchmarkArrToStr-12 536188455 2.180 ns/op //BenchmarkArrToStrV2-12 1000000000 0.2526 ns/op
本文介绍了如何使用 unsafe 包绕过类型检查, 直接操作内存。正如 go 作者对包的命名一样, 它是 unsafe 的, 随着 go 版本的迭代, 有些机制可能会发生变更。如无必要, 不应使用这个包。如果要使用 unsafe 包, 一定要理解清楚Pointer、uinptr、对齐系数等概念。
推荐学习:Golang教程
以上是了解一下Golang中的unsafe包的詳細內容。更多資訊請關注PHP中文網其他相關文章!