溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Golang中的unsafe包有什么用

發(fā)布時(shí)間:2023-04-24 14:58:46 來源:億速云 閱讀:103 作者:iii 欄目:編程語言

今天小編給大家分享一下Golang中的unsafe包有什么用的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

unsafe 包提供了一些操作可以繞過 go 的類型安全檢查, 從而直接操作內(nèi)存地址, 做一些 tricky 操作。示例代碼運(yùn)行環(huán)境是 go version go1.18 darwin/amd64

內(nèi)存對(duì)齊

unsafe 包提供了 Sizeof 方法獲取變量占用內(nèi)存大小「不包含指針指向變量的內(nèi)存大小」, Alignof 獲取內(nèi)存對(duì)齊系數(shù), 具體內(nèi)存對(duì)齊規(guī)則可以自行 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 位操作系統(tǒng), 字長(zhǎng) 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}復(fù)制代碼

從上面 case 可以看到 demo1 和 demo2 包含相同的屬性, 只是定義的屬性順序不同, 卻導(dǎo)致變量的內(nèi)存大小不同。這里是因?yàn)榘l(fā)生了內(nèi)存對(duì)齊。

計(jì)算機(jī)在處理任務(wù)時(shí), 會(huì)按照特定的字長(zhǎng)「例如:32 位操作系統(tǒng), 字長(zhǎng)為 4; 64 位操作系統(tǒng), 字長(zhǎng)為 8」為單位處理數(shù)據(jù)。那么, 在讀取數(shù)據(jù)的時(shí)候也是按照字長(zhǎng)為單位。例如: 對(duì)于 64 位操作系統(tǒng), 程序一次讀取的字節(jié)數(shù)為 8 的倍數(shù)。下面是 demo1 在非內(nèi)存對(duì)齊和內(nèi)存對(duì)齊下的布局:

非內(nèi)存對(duì)齊:

變量 c 會(huì)被放在不同的字長(zhǎng)里面, cpu 在讀取的時(shí)候需要同時(shí)讀取兩次, 同時(shí)對(duì)兩次的結(jié)果做處理, 才能拿到 c 的值。這種方式雖然節(jié)省了內(nèi)存空間, 但是會(huì)增加處理時(shí)間。

內(nèi)存對(duì)齊:

內(nèi)存對(duì)齊采用了一種方案, 可以避免同一個(gè)非內(nèi)存對(duì)齊的這種情況, 但是會(huì)額外占用一些空間「空間換時(shí)間」。具體內(nèi)存對(duì)齊規(guī)則可以自行 google。

Golang中的unsafe包有什么用

Unsafe Pointer

在 go 中可以聲明一個(gè)指針類型, 這里的類型是 safe pointer, 即要明確指針指向的類型, 如果類型不匹配將會(huì)在編譯時(shí)報(bào)錯(cuò)。如下面的示例, 編譯器會(huì)認(rèn)為 MyString 和 string 是不同的類型, 無法進(jìn)行賦值。

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 的聲明, 可以知道它是一個(gè)指針類型, 指向變量所在的地址。具體的地址對(duì)應(yīng)的值可以通過 uinptr 進(jìn)行轉(zhuǎn)換。Pointer 有以下四種特殊的操作:

  • 任意類型的指針都可以轉(zhuǎn)換成 Pointer 類型

  • Pointer 類型的變量可以轉(zhuǎn)換成任意類型的指針

  • uintptr 類型的變量可以轉(zhuǎn)換成 Pointer 類型

  • Pointer 類型的變量可以轉(zhuǎn)換成 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)                // 任意類型的指針可以轉(zhuǎn)換為 Pointer 類型
   pa := (*demo1)(p)                      // Pointer 類型變量可以轉(zhuǎn)換成 demo1 類型的指針
   up := uintptr(p)                       // Pointer 類型的變量可以轉(zhuǎn)換成 uintprt 類型
   pu := unsafe.Pointer(up)               // uintptr 類型的變量可以轉(zhuǎn)換成 Pointer 類型; 當(dāng) GC 時(shí), d 的地址可能會(huì)發(fā)生變更, 因此, 這里的 up 可能會(huì)失效
   fmt.Println(d.a, pa.a, (*demo1)(pu).a) // true true true

}

Pointer 的六種使用方式

在官方文檔中給出了 Pointer 的六種使用姿勢(shì)。

  1. 通過 Pointer 將 *T1 轉(zhuǎn)換為 *T2

Pointer 直接指向一塊內(nèi)存, 因此可以將這塊內(nèi)存地址轉(zhuǎn)為任意類型。這里需要注意, T1 和 T2 需要有相同的內(nèi)存布局, 會(huì)有異常數(shù)據(jù)。

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 指向的內(nèi)存地址直接轉(zhuǎn)換成 *[]string
   fmt.Println(ms, ss)
}

Golang中的unsafe包有什么用

如果 T1 和 T2 的內(nèi)存布局不同, 會(huì)發(fā)生什么呢?在下面的示例子中, demo1 和 demo2 雖然包含相同的結(jié)構(gòu)體, 由于內(nèi)存對(duì)齊, 導(dǎo)致兩者是不同的內(nèi)存布局。將 Pointer 轉(zhuǎn)換時(shí), 會(huì)從 demo1 的地址開始讀取 24「sizeof」 個(gè)字節(jié), 按照demo2 內(nèi)存對(duì)齊規(guī)則進(jìn)行轉(zhuǎn)換, 將第一個(gè)字節(jié)轉(zhuǎn)換為 a:true, 8-16 個(gè)字節(jié)轉(zhuǎn)換為 c:2, 16-24 個(gè)字節(jié)超出了 demo1 的范圍, 但仍可以直接讀取, 獲取了非預(yù)期的值 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 類型變量可以轉(zhuǎn)換成 demo2 類型的指針
   fmt.Println(pa.a, pa.b, pa.c) // true, 17368000, 2, 
}

Golang中的unsafe包有什么用

  1. 將 Pointer 類型轉(zhuǎn)換為 uintptr 類型「不應(yīng)該將 uinptr 轉(zhuǎn)為 Pointer」

Pointer 是一個(gè)指針類型, 可以指向任意變量, 可以通過將 Pointer 轉(zhuǎn)換為 uintptr 來打印 Pointer 指向變量的地址。此外:不應(yīng)該將 uintptr 轉(zhuǎn)換為 Pointer。如下面的例子: 當(dāng)發(fā)生 GC 時(shí), d 的地址可能會(huì)發(fā)生變更, 那么 up 由于未同步更新而指向錯(cuò)誤的內(nèi)存。

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)))    // 不允許
}

  1. 通過算數(shù)計(jì)算將 Pointer 轉(zhuǎn)換為 uinptr 再轉(zhuǎn)換回 Pointer

當(dāng) Piointer 指向一個(gè)結(jié)構(gòu)體時(shí), 可以通過此方式獲取到結(jié)構(gòu)體內(nèi)部特定屬性的 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)
}

  1. 當(dāng)調(diào)用 syscall.Syscall 的時(shí)候, 可以講 Pointer 轉(zhuǎn)換為 uintptr

前面說過, 由于 GC 會(huì)導(dǎo)致變量的地址發(fā)生變更, 因此不可以直接處理 uintptr。但是, 在調(diào)用 syscall.Syscall 時(shí)候可以允許傳遞一個(gè) uintptr, 這里可以簡(jiǎn)單理解為是編譯器做了特殊處理, 來保證 uintptr 是安全的。

  • 調(diào)用方式:

  • syscall.Syscall(SYS_READ, uintptr( fd ), uintptr(unsafe.Pointer(p)), uintptr(n))

下面這種方式是不允許的:

u := uintptr(unsafe.Pointer(p)) // 不應(yīng)該保存到一個(gè)變量上 syscall.Syscall(SYS_READ, uintptr( fd ), u, uintptr(n))

  1. 可以將 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的結(jié)果「uintptr」轉(zhuǎn)換為 Pointer

在 reflect 包中的 Value.Pointer 和 Value.UnsafeAddr 直接返回了地址對(duì)應(yīng)的值「uintptr」, 可以直接將其結(jié)果轉(zhuǎn)為 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); 不安全, 不應(yīng)存儲(chǔ)到變量中
   pc := unsafe.Pointer(reflect.ValueOf(&d.b).Pointer())
   fmt.Println(pb, pc)
}

  1. 可以將 reflect.SliceHeader 或者 reflect.StringHeader 的 Data 字段與 Pointer 相互轉(zhuǎn)換

SliceHeader 和 StringHeader 其實(shí)是 slice 和 string 的內(nèi)部實(shí)現(xiàn), 里面都包含了一個(gè)字段 Data「uintptr」, 存儲(chǔ)的是指向 []T 的地址, 這里之所以使用 uinptr 是為了不依賴 unsafe 包。

func main() {
   s := "a"
   hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // *string to *StringHeader
   fmt.Println(*(*[1]byte)(unsafe.Pointer(hdr.Data))) // 底層存儲(chǔ)的是 utf 編碼后的 byte 數(shù)組
   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
}

應(yīng)用

string、byte 轉(zhuǎn)換

在業(yè)務(wù)上, 經(jīng)常遇到 string 和 []byte 的相互轉(zhuǎn)換。我們知道, string 底層其實(shí)也是存儲(chǔ)的一個(gè) byte 數(shù)組, 可以通過 reflect 直接獲取 string 指向的 byte 數(shù)組, 賦值給 byte 切片, 避免內(nèi)存拷貝。

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 的內(nèi)存布局我們可以知道, 無法直接將 string 轉(zhuǎn)為 []byte 「確實(shí) cap 字段」, 但是可以直接將 []byte 轉(zhuǎn)為 string

Golang中的unsafe包有什么用

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

以上就是“Golang中的unsafe包有什么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI