go 語言源碼閱讀 unsafe 包和 unsafe-Pointer 以及 go 指針運算

一、認識指針與指針類型

一個指針變量可以指向任何一個值的內存地址,它所指向的值的內存地址在 32 和 64 位機器上分別佔用 4 或 8 個字節,佔用字節的大小與所指向的值的大小無關。當一個指針被定義後沒有分配到任何變量時,它的默認值爲 nil。

每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go 語言中使用在變量名前面添加 & 操作符(前綴)來獲取變量的內存地址(取地址操作),格式如下:

ptr := &v    // v 的類型爲 T

其中 v 代表被取地址的變量,變量 v 的地址使用變量 ptr 進行接收,v 的類型爲 T,ptr 的類型爲 _T,稱做 T 的指針類型,_代表指針。

func TestPtr(t *testing.T) {
    s := "hello ptr"
    fmt.Printf("s的地址爲%p ,地址的10進製表示爲%d 值爲%s \n", &s, &s, s)
    fmt.Printf("地址的2進製表示爲%b", &s)
}

輸出:

s的地址爲0xc0000484e0 ,地址的10進製表示爲824634016992 值爲hello ptr
地址的2進製表示爲1100000000000000000001001000010011100000

問題 1:s 地址佔用了幾個字節?

如果我們數一下大約有 40 位二進制,那麼是 5 個字節?
正確答案是 8 個字節,我們可以轉爲 unsafe.Pointer 然後使用 size 方法打印出來

func TestPtr(t *testing.T) {
    s := "hello ptr"
    fmt.Printf("s的地址爲%p ,地址的10進製表示爲%d 值爲%s \n", &s, &s, s)
    fmt.Printf("s地址的2進製表示爲%b \n", &s)

    var p unsafe.Pointer
    p = unsafe.Pointer(&s)
    fmt.Printf("p地址的2進製表示爲%b \n", p)
    size := unsafe.Sizeof(p)
    fmt.Printf("p地址的大小爲幾個字節 %d \n", size)
}

輸出爲:

s的地址爲0xc00008e4d0 ,地址的10進製表示爲824634303696 值爲hello ptr 
s地址的2進製表示爲1100000000000000000010001110010011010000 
p地址的2進製表示爲1100000000000000000010001110010011010000 
p地址的大小爲幾個字節 8

二、unsafe 源碼介紹

unsafe 包常用方法

type ArbitraryType int
type Pointer *ArbitraryType
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
  1. Alignof 返回變量對齊字節數量

  2. Offsetof 返回變量指定屬性的偏移量,所以如果變量是一個 struct 類型,不能直接將這個 struct 類型的變量當作參數,只能將這個 struct 類型變量的屬性當作參數。

  3. Sizeof 返回變量在內存中佔用的字節數,切記,如果是 slice,則不會返回這個 slice 在內存中的實際佔用長度。

關於返回值 uintptr 類型 在 go 源代碼裏

type uintptr uintptr

uintptr 是一個整數類型,它足夠大,可以存儲. 只有將 Pointer 轉換成 uintptr 才能進行指針的相關操作。

uintptr 是可以用於指針運算的,但是 GC 並不把 uintptr 當做指針,所以 uintptr 不能持有對象, 可能會被 GC 回收, 導致出現無法預知的錯誤. Pointer 指向一個對象時, GC 是不會回收這個內存的。

2.1 ArbitraryType 任意類型

ArbitraryType 表示任意類型,如同 interface{},因爲用來存儲指針地址的,所以相當於存儲任意類型。

type ArbitraryType int

2.2 Pointer 指針類型

unsafe 中,ArbitraryType 任意類型的的指針類型就是 Pointer 類型。
可以將其他類型都轉換過來,然後通過這三個函數,分別能取長度,偏移量,對齊字節數,就可以在內存地址映射中,來回遊走。
我們可以用強制類型轉化 type(a) 語法把任意一個指針類型轉成 unsafe.Pointer
語法如下:

unsafe.Pointer(a)
func TestPointer(t *testing.T) {
    //把一個int類型強制轉成 unsafe.Pointer 任意type指針類型
    var i int = 10
    fmt.Println(unsafe.Pointer(&i)) //0xc0000a61a8

    //把一個string類型強制轉成 unsafe.Pointer 任意type指針類型
    var s string = "hello"
    fmt.Println(unsafe.Pointer(&s)) //0xc00008e4f0

    //把一個array類型強制轉成 unsafe.Pointer 任意type指針類型
    var a [5]int = [5]int{0, 1, 2, 3, 4}
    fmt.Println(unsafe.Pointer(&a)) //0xc0000b0030

    //把一個map類型強制轉成 unsafe.Pointer 任意type指針類型
    var m map[string]int8 = map[string]int8{"a": 1, "b": 10, "c": 20, "d": 30}
    fmt.Println(unsafe.Pointer(&m)) //0xc0000a0028

    //把一個slice類型強制轉成 unsafe.Pointer 任意type指針類型
    var sli []int8 = []int8{0, 1, 3, 4, 5, 6}
    fmt.Println(unsafe.Pointer(&sli)) //0xc0000b40a0

    //把一個struct類型強制轉成 unsafe.Pointer 任意type指針類型
    type st struct {
        w int8
        h int8
    }
    var st1 = st{
        w: 40,
        h: 50,
    }
    fmt.Println(unsafe.Pointer(&st1)) //0xc0000a61b6
}

2.3 Sizeof 佔用的內存大小

2.3.1、int 等數字類型佔用的內存大小
func TestSizeofInt(t *testing.T) {
    //int8,int16,int32,int64,int類型佔用的內存地址
    var i8 int8 = 10
    var i16 int16 = 10
    var i32 int32 = 10
    var i64 int64 = 10
    var i int = 10
    fmt.Println(unsafe.Sizeof(i8), unsafe.Sizeof(i16), unsafe.Sizeof(i32), unsafe.Sizeof(i64), unsafe.Sizeof(i))
    //輸出爲 1 2 4 8 8

    //uint8,uint16,uint32,uint64,uint類型佔用的內存地址
    var u8 uint8 = 10
    var u16 uint16 = 10
    var u32 uint32 = 10
    var u64 uint64 = 10
    var u uint = 10
    fmt.Println(unsafe.Sizeof(u8), unsafe.Sizeof(u16), unsafe.Sizeof(u32), unsafe.Sizeof(u64), unsafe.Sizeof(u))
    //輸出爲 1 2 4 8 8
}

vN7KAm

注意:int,uint 是根據 cpu 來的,我這裏是 64 位的 cpu,所以這裏佔用了 64bit 內存,也有 32 位,16 位的。

2.3.2、string 類型佔用的內存大小

func TestSizeofString(t *testing.T) {
    //string類型佔用的內存地址
    var s string = "a"
    fmt.Println(unsafe.Sizeof(s)) //16

    //string類型編譯後的存儲類型爲StringHeader,我們可以強制轉化看一下
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    fmt.Println(unsafe.Sizeof(stringHeader.Data)) //8
    fmt.Println(unsafe.Sizeof(stringHeader.Len))  //8

    byteStr := (*byte)(unsafe.Pointer(stringHeader.Data))
    fmt.Println(byteStr)                 //0x11435f6 存儲"a"的地址
    fmt.Println(*byteStr)                //97 十進制97
    fmt.Println(string(*byteStr))        // "a" 字符串a
    fmt.Printf("%b", *byteStr)           //1100001 二進制標識的97
    fmt.Println(unsafe.Sizeof(*byteStr)) //1 "a"存儲佔用的內存地址爲1個字節
}

我們保存一個 string 類型的變量 s,值爲”a“
問題:此時使用 unsafe.Sizeof(s) 函數,得出內存佔用爲 16 個字節,爲什麼不是 1?
答案:因爲 s 在編譯後的類型爲 reflect.StringHeader 如下結構

type StringHeader struct {
    Data uintptr
    Len  int
}

此時我們 unsafe.Sizeof(s),其實是 unsafe.Sizeof(StringHeader)
因爲 StringHeader.Data 爲 uintptr 類型佔用 8 個字節,存儲的字符串值的內存地址
StringHeader.Len 爲 int 類型也佔用 8 個字節,所以總共 16 字節。

2.3.3 slice 類型佔用內存大小

func TestSizeofSlice(t *testing.T) {
    //slice類型佔用內存地址
    var sli []int8 = []int8{0, 1, 2, 3, 4, 5}
    fmt.Println(unsafe.Sizeof(sli)) //24

    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&sli))
    fmt.Println(unsafe.Sizeof(sliceHeader.Data)) //8 uintptr類型
    fmt.Println(unsafe.Sizeof(sliceHeader.Len))  //8 int類型
    fmt.Println(unsafe.Sizeof(sliceHeader.Cap))  //8 int類型
}

2.3.4、自定義 struct 佔用用內大小

func TestSizeofStruct(t *testing.T) {
    //struct類型佔用的內存地址
    type arrT struct {
        v [100]int8
    }
    type ST struct {
        b     byte
        i8    int8
        sli   []int8
        s     string
        arrSt arrT
    }

    st := ST{
        b:     1,
        i8:    127,
        sli:   []int8{0, 1, 2, 3, 4},
        s:     "hello",
        arrSt: arrT{v: [100]int8{5, 6, 7, 8}},
    }
    fmt.Println(unsafe.Sizeof(st))       //152 爲結構體各個字段的內存之和
    fmt.Println(unsafe.Sizeof(st.b))     //1 byte類型和uint8 一樣佔用1個字節
    fmt.Println(unsafe.Sizeof(st.i8))    //1 int8類型佔用1個字節
    fmt.Println(unsafe.Sizeof(st.sli))   //24 slice類型佔用24個字節
    fmt.Println(unsafe.Sizeof(st.s))     //16 string類型16個字節
    fmt.Println(unsafe.Sizeof(st.arrSt)) //100 [100]int8 數組類型爲100*int8 爲100個字節
}

一個結構體類型佔用的內存爲組成的各個字段的佔用的內存之和。

2.4 Offsetof 指針的位移

Offsetof 返回變量指定屬性的偏移量,所以如果變量是一個 struct 類型,不能直接將這個 struct 類型的變量當作參數,只能將這個 struct 類型變量的屬性當作參數。

func TestOffsetof(t *testing.T) {
    var abc struct {
        a bool
        b int32
        c []int
    }
    fmt.Println("SIZE")
    fmt.Println(unsafe.Sizeof(abc.a)) //1
    fmt.Println(unsafe.Sizeof(abc.b)) //4
    fmt.Println(unsafe.Sizeof(abc.c)) //24
    fmt.Println(unsafe.Sizeof(abc))   //32
    fmt.Println("OFFSET")
    fmt.Println(unsafe.Offsetof(abc.a)) //0
    fmt.Println(unsafe.Offsetof(abc.b)) //4
    fmt.Println(unsafe.Offsetof(abc.c)) //8
}

三、go 的指針運算

go 指針運算的語法:

pNew = unsafe.Pointer(uintptr(p) + offset)

p 和 pNew 都是 unsafe.Pointer 類型
這裏的 p 轉成了 uintptr 然後和 offset 相加,是因爲,以下兩個用到的函數計算 offset,返回值都是 uintptr

func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr

最常見的用法是訪問結構體中的字段或則數組的元素:

3.1 結構體的指針運算如下:

f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// 等價於 f := unsafe.Pointer(&s.f)

指針的運算思想是:第一個的地址 + 偏移量

func TestPointer2(t *testing.T) {
    var abc struct {
        a bool
        b int32
        c []int
    }
    //1 結構體abc的地址爲
    fmt.Println(unsafe.Pointer(&abc))
    //2 結構體abc.a 的地址爲
    fmt.Println(unsafe.Pointer(&abc.a))
    //3 指針運算得出 abc.a的地址爲
    fmt.Println(unsafe.Pointer(uintptr(unsafe.Pointer(&abc)) + unsafe.Offsetof(abc.a)))
}

輸出爲:

=== RUN   TestPointer2
0xc00008e060
0xc00008e060
0xc00008e060
0xc00008e064
0xc00008e064
--- PASS: TestPointer2 (0.00s)
  1. 我們可以得出結構體的地址爲,第一個元素的地址

  2. 結構體的指針運算 沒問題

3.2 數組的指針運算如下:

e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))
// 等價於 e := unsafe.Pointer(&x[i])

指針的運算思想是:第一個的地址 + 偏移量
這裏偏移量爲 i*unsafe.Sizeof(x[0])) i 爲數組元素的索引 index,sizeOf 爲數組元素的單個大小

func TestPointer3(t *testing.T) {
    var i [4]uint32 = [4]uint32{}
    //1 數組i的地址爲
    fmt.Println(unsafe.Pointer(&i))
    //2 數組i[0],第一個元素的地址爲
    fmt.Println(unsafe.Pointer(&i[0]))
    //3 數組i[1]的地址爲
    fmt.Println(unsafe.Pointer(&i[1]))
    //4 通過指針運算 數組i[1] 的地址爲
    fmt.Println(unsafe.Pointer(uintptr(unsafe.Pointer(&i)) + 1*unsafe.Sizeof(i[0])))
    //5 通過指針運算 最後一個 數組i[3] 的地址爲
    fmt.Println(unsafe.Pointer(uintptr(unsafe.Pointer(&i)) + 3*unsafe.Sizeof(i[0])))
}

3.3 取出內存外的數據會怎麼樣?

我們以數組爲例子:取出 index 爲 10,公共纔有 4,超出了數組的範圍

func TestPointer4(t *testing.T) {
    var i [4]uint32 = [4]uint32{}
    //5 通過指針運算 最後一個 數組i[3] 的地址爲
    fmt.Println(unsafe.Pointer(uintptr(unsafe.Pointer(&i)) + 3*unsafe.Sizeof(i[0])))
    //6 超出的數組元素的範圍會發生什麼?--得到一個數組外其他內容的地址
    fmt.Println(unsafe.Pointer(uintptr(unsafe.Pointer(&i)) + 10*unsafe.Sizeof(i[0])))
}

輸出:

=== RUN   TestPointer4
0xc00011e1cc
0xc00011e1e8
--- PASS: TestPointer4 (0.00s)

當我們試圖打印超出的地址的內容時候,會報錯!需要注意

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4l4QssK24AzdzYGH02V0Ng