Golang指針與unsafe


前言

我們知道在golang中是存在指針這個概念的。對於指針很多人有點忌憚(可能是因為之前學習過C語言),因為它會導致很多異常的問題。但是很多人學習之后發現,golang中的指針很簡單,沒有C那么復雜。所以今天就詳細來說說指針。

因為博客園發布markdown格式存在問題,請移步http://www.linkinstar.wiki/2019/06/06/golang/source-code/point-unsafe/

指針的使用

a := 1
p := &a
fmt.Println(p)

輸出:0xc42001c070

可以看到p就是一個指針,也可以說是a的地址。

a := 1
var p *int
p = &a
fmt.Println(p)

或者也可以寫成這樣,因為我知道,在很多人看來,看到*號才是指針(手動滑稽)

a := 1
p := &a
fmt.Println(*p)

輸出:1

然后使用就直接通過*號就能去到對應的值了,就這么簡單

指針的限制

Golang中指針之所以看起來很簡單,是因為指針的功能不多。我們能看到的功能就是指針的指向一個地址而已,然后對於這個地址也只能進行傳遞,或者通過這個的地址去訪問值。

  • 不能像C語言中一樣p++,這樣移動操作指針,因為其實這樣操作確實不安全,很容易訪問到奇怪的區域。
  • 不同類型的指針不能相互賦值、轉換、比較。會出現cannot use &a (type *int) as type *float32 in assignment類似這樣的錯誤

如果只是單純說go中指針的功能,上面就已經說完了,沒必要寫博客,但是其實go中還有一個包叫unsafe,有了它,指針就可以像C一樣想干嘛干嘛了。

unsafe

三個類型

其實指針有三種:
一種是我們常見的*,用*去表示的指針;
一種是unsafe.Pointer,Pointer是unsafe包下的一個類型;
最后一種是uintptr,uintptr就厲害了,這玩意是可以進行運算的也就是可以++--;

他們之間有這樣的轉換關系:
* <=> unsafe.Pointer <=> uintptr

  • 有一點要注意的是,uintptr 並沒有指針的語義,意思就是 uintptr 所指向的對象會被 gc 無情地回收。而 unsafe.Pointer 有指針語義,可以保護它所指向的對象在“有用”的時候不會被垃圾回收。

從這樣的關系你大概就可以猜到,我們使用的指針*p轉換成Pointer然后轉換uintptr進行運算之后再原路返回,理論上就能等同於進行了指針的運算。我們下面就來實踐一下。

unsafe操作slice

func main() {
    s := make([]int, 10)
    s[1] = 2
    
    p := &s[0]
    fmt.Println(*p)
    
    up := uintptr(unsafe.Pointer(p))
    up += unsafe.Sizeof(int(0)) // 這里可不是up++哦

    p2 := (*int)(unsafe.Pointer(up))
    fmt.Println(*p2)
}

輸出:
0
2

從代碼中我們可以看到,我們首先將指針指向切片的第一個位置,然后通過轉換得到uintptr,操作uintptr + 上8位(注意這里不能++因為存放的是int,下一個元素位置相隔舉例int個字節),最后轉換回來得到指針,取值,就能取到切片的第二個位置了。

unsafe操作struct

當然有人肯定要說了,上面那個一頓操作猛如虎,不就是訪問下一個位置嘛,我直接訪問就行了。
那下面就是厲害的來了,我們知道如果一個結構體里面定義的屬性是私有的,那么這個屬性是不能被外界訪問到的。我們來看看下面這個操作:

package basic

type User struct {
    age  int
    name string
}
package main

func main() {
    user := &basic.User{}
    fmt.Println(user)
    
    s := (*int)(unsafe.Pointer(user))
    *s = 10

    up := uintptr(unsafe.Pointer(user)) + unsafe.Sizeof(int(0))

    namep := (*string)(unsafe.Pointer(up))
    *namep = "xxx" 

    fmt.Println(user)
}

User是另外一個basic包中的結構體,其中的age是小寫開頭的,理論上來說,我們在外部沒有辦法修改age的值,但是經過上面這波操作之后,輸出信息是:
&{0 }
&{10 xxx}
也就是說成功操作到了結構體的私有屬性。

順便提一句:創建結構體會被分配一塊連續的內存,結構體的地址也代表了第一個成員的地址。

下面我們來驗證一下你是否已經學會了unsafe的操作,嘗試不看一個小結,自己嘗試一下:如何完成字符串到[]byte的轉換,並且不開辟新的空間?





字符串和byte數組轉換inplace

我們知道如果將字符串轉換成[]byte非常方便

s := "123"
a := []byte(s)

但是這樣需要開辟額外的空間,那么如何實現原地的,不需要拷貝數據的轉換呢?
我們想一下,其實從底層的存儲角度來說,string的存儲規則和[]byte是一樣的,也就是說,其實指針都是從某個位置開始到一段空間,中間一格一格。所以利用unsafe就可以做到。

func main() {
    s := "123"
    a := []byte(s)
    
    print("s = " , &s, "\n")
    print("a = " , &a, "\n")
    
    a2 := (*[]byte)(unsafe.Pointer(&s))
    print("a2 = " , a2, "\n")

    fmt.Println(*a2)
}

輸出結果:
s = 0xc420055f40
a = 0xc420055f60
a2 = 0xc420055f40
[49 50 51]

我們可以看到s和a的地址是不一樣的,但是s和a2的地址是一樣的,並且a2已經是一個[]byte了。






嘿嘿嘿~你以為這樣就結束了???

存在的問題

其實這個轉換是存在問題的,問題就在新的[]byte的Cap沒有正確的初始化。
我們打印一下cap看一下
fmt.Println("cap a =", cap(a))
fmt.Println("cap a2 =", cap(*a2))
結果是:
cap a = 32
cap a2 = 17418400
這么大的容量是要上天呢???

問題的原因

在src/reflect/value.go下看

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

看到其實string沒有cap而[]byte有,所以導致問題出現,也容易理解,string是沒有容量擴容這個說法的,所以新的[]byte沒有賦值cap所以使用了默認值。

問題解決

stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

bh := reflect.SliceHeader{
    Data: stringHeader.Data,
    Len:  stringHeader.Len,
    Cap:  stringHeader.Len,
}

return *(*[]byte)(unsafe.Pointer(&bh))

通過重新設置SliceHeader就可以完成

總結

以上就是所有golang指針和unsafe的相關細節和使用。那么肯定有人會問這個有什么用了?

  • 1、沒啥事你就別亂用了,別人都說unsafe不安全了。
  • 2、源碼中很多大量的使用了指針移動的操作。

如map中通過key獲取value的時候:

v := add(unsafe.Pointer(b), dataOffset+bucketCnt * uintptr(t.keysize)+i * uintptr(t.valuesize))

通過桶的指針的偏移拿到值,具體我就不多介紹了。
總之對於你看golang源碼的時候會有很大幫助的。可能必要的時候你也能用到它,還是那句話,除非你知道它在干什么,否則不要用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM