golang如何使用指標靈活操作記憶體?unsafe包原理解析

golang架构师k哥發表於2024-06-22

Hi 你好,我是k哥。一個大廠工作6年,還在繼續搬磚的後端程式設計師。

我們都知道,C/C++提供了強大的萬能指標void*,任何型別的指標都可以和萬能指標相互轉換。並且指標還可以進行加減等算數操作。那麼在Golang中,是否有類似的功能呢?答案是有的,這就是我們今天要探討的unsafe包。

本文將深入探討unsafe包的功能和原理。同時,我們學習某種東西,一方面是為了實踐運用,另一方面則是出於功利性面試的目的。所以,本文還會為大家介紹unsafe 包的典型應用以及高頻面試題。

功能

為了實現靈活操作記憶體的目的,unsafe包主要提供了4個功能:

  1. 定義了Pointer型別,任何型別的指標都可和Pointer互相轉換,類似於c語言中的void*
var a int = 1
p := unsafe.Pointer(&a) // 其它型別指標轉Pointer
b := (*int)(p) // Pointer型別轉其它型別指標
fmt.Println(*b) // 輸出1
  1. 定義了uintptr型別,Pointer和uintptr可以互相轉換, 從而實現指標的加減等算數運算。
type Person struct {
    age int
    name string
}
person := Person{age:18,name:"k哥"}
p := unsafe.Pointer(&person) // 其它型別指標轉Pointer
u := uintptr(p) // Pointer型別轉為uintptr
u=u+8 // uintptr加減操作
pName := unsafe.Pointer(u) // uintptr轉換為Pointer
name := *(*string)(pName)
fmt.Println(name) // 輸出k哥

uintptr是用於指標運算的,它只是一個儲存一個 指標地址int 型別,GC 不把 uintptr 當指標,因此, uintptr 型別的目標可能會被回收

  1. 獲取任意型別記憶體對齊、偏移量和記憶體大小。
func Alignof(x ArbitraryType) uintptr // 記憶體對齊
func Offsetof(x ArbitraryType) uintptr // 記憶體偏移量
func Sizeof(x ArbitraryType) uintptr // 記憶體大小
  • Alignof 返回型別x的記憶體地址對齊值m,這個型別在記憶體中的地址必須是m的倍數(基於記憶體讀寫效能的考慮)。
  • Offsetof 返回結構體成員x在記憶體中的位置離結構體起始處(結構體的第一個欄位的偏移量都是0)的位元組數,即偏移量。
  • Sizeof 返回型別 x 所佔據的位元組數,如果型別x結構有指標,Sizeof不包含 x 指標成員所指向內容的大小。

ArbitraryType是佔位符,golang編譯器在編譯時會替換為具體型別

  1. 高效能型別轉換。
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string 
func StringData(str string) *byte
  • Slice 傳入任意型別的指標和長度,返回該型別slice變數
  • SliceData 傳入任意型別的slice變數,返回該slice底層陣列的指標。
  • String 從一個byte指標派生出一個指定長度的字串。
  • StringData 用來獲取一個字串底層位元組序列中的第一個byte的指標。

高效能型別轉換原理

為什麼說Slice、SliceData、String、StringData是高效能型別轉換函式呢?下面我們就來剖析下它們的實現原理。

本文以String和StringData函式為例,Slice和SliceData函式實現原理類似。在介紹函式實現原理之前,先認識下string型別的底層資料結構StringHeader。string型別會被Golang編譯器編譯成此結構,其中Data是byte陣列地址,Len是字串長度。

type StringHeader struct {
        Data uintptr // byte陣列地址
        Len  int // 字串長度
}

String函式會被Go編譯成下面的函式實現邏輯。我們可以發現,ptr指標轉換為string型別,是直接將ptr賦值給StringHeader的成員Data,而不需要重新複製ptr指向的byte陣列。從而透過零複製實現高效能型別轉換。

import (
    "fmt"
    "reflect"
    "unsafe"
)

func String(ptr *byte, len int) string {
    p := (uintptr)(unsafe.Pointer(ptr))
    hdr := &reflect.StringHeader{
        Data: p,
        Len:  len,
    }
    // 將 StringHeader 轉為 string
    str := *(*string)(unsafe.Pointer(hdr))
    return str
}

func main() {
    bytes := []byte{'h', 'e', 'l', 'l', 'o'}
    ptr := &bytes[0]
    len := 5
    str := String(ptr, len)
    fmt.Println(str) // 輸出hello
}

StringData函式會被Go編譯成下面的函式實現邏輯。同理,我們可以發現,string型別轉換為byte,是直接取StringHeader的uintptr型別成員Data,並將其轉換為byte。不需要複製整個string,重新生成byte陣列。從而透過零複製實現高效能型別轉換。

import (
    "fmt"
    "reflect"
    "unsafe"
)

func StringData(str string) *byte {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&str))
    data := hdr.Data
    return (*byte)(unsafe.Pointer(data))
}

func main() {
    str := "hello"
    data := StringData(str)
    fmt.Println(string(*data)) // 輸出h
}

回到問題,為什麼說Slice、SliceData、String、StringData是高效能型別轉換函式呢?透過String和StringData函式的實現邏輯,我們可以知道,String和StringData利用unsafe包,透過零複製,實現了高效能型別轉換。

典型應用

在實踐中,常見使用unsafe包的場景有2個:

  1. 與作業系統以及非go編寫(cgo)的程式碼通訊。
func SetData(bytes []byte) { 
    cstr := (*C.char)(unsafe.Pointer(&bytes[0])) // 轉換成一個C char型別
    C.setData(cstr, (C.int)(len(bytes))) // 呼叫C語言函式
}
  1. 高效能型別轉換。
func Bytes2String(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

func String2Bytes(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data: sh.Data,
        Len:  sh.Len,
        Cap:  sh.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

高頻面試題

  1. 能說說uintptr和unsafe.Pointer的區別嗎?
  2. 字串轉成byte陣列,會發生記憶體複製嗎?

歡迎大家關注我的公粽號【golang架構師k哥】,每週分享golang和架構師技能。

相關文章