前言
我們知道在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源碼的時候會有很大幫助的。可能必要的時候你也能用到它,還是那句話,除非你知道它在干什么,否則不要用。