Golang指針與unsafe

前言

咱們知道在golang中是存在指針這個概念的。對於指針不少人有點忌憚(多是由於以前學習過C語言),由於它會致使不少異常的問題。可是不少人學習以後發現,golang中的指針很簡單,沒有C那麼複雜。因此今天就詳細來講說指針。golang

由於博客園發佈markdown格式存在問題,請移步http://www.linkinstar.wiki/2019/06/06/golang/source-code/point-unsafe/數組

指針的使用

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

輸出:0xc42001c070安全

能夠看到p就是一個指針,也能夠說是a的地址。markdown

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

或者也能夠寫成這樣,由於我知道,在不少人看來,看到*號纔是指針(手動滑稽)學習

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

輸出:1ui

而後使用就直接經過*號就能去到對應的值了,就這麼簡單指針

指針的限制

Golang中指針之因此看起來很簡單,是由於指針的功能很少。咱們能看到的功能就是指針的指向一個地址而已,而後對於這個地址也只能進行傳遞,或者經過這個的地址去訪問值。code

  • 不能像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的相關細節和使用。那麼確定有人會問這個有什麼用了?

  • 一、沒啥事你就別亂用了,別人都說unsafe不安全了。
  • 二、源碼中不少大量的使用了指針移動的操做。

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

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

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

相關文章
相關標籤/搜索