一衰絮、Golang指針與C/C++指針的差別
在Golang支持的數(shù)據(jù)類型中练俐,是包含指針的沸久,但是Golang中的指針季眷,與C/C++的指針卻又不同,筆者覺得主要表現(xiàn)在下面的兩個(gè)方面:
- 弱化了指針的操作卷胯,在Golang中子刮,指針的作用僅是操作其指向的對象,不能進(jìn)行類似于C/C++的指針運(yùn)算,例如指針相減挺峡、指針移動(dòng)等葵孤。從這一點(diǎn)來看,Golang的指針更類似于C++的引用
- 指針類型不能進(jìn)行轉(zhuǎn)換橱赠,如int不能轉(zhuǎn)換為int32
上述的兩個(gè)限定主要是為了簡化指針的使用尤仍,減少指針使用過程中出錯(cuò)的機(jī)率,提高代碼的魯棒性狭姨。但是在開發(fā)過程中宰啦,有時(shí)需要打破這些限制,對內(nèi)存進(jìn)行任意的讀寫送挑,這里就需要unsafe.Pointer了绑莺。
二、unsafe.Pointer
1)unsafe.Pointer的定義
從unsate.Pointer的定義如下惕耕,從定義中我們可以看出纺裁,Pointer的本質(zhì)是一個(gè)int的指針:
type ArbitraryType int
type Pointer *ArbitraryType
2)unsafe.Pointer的功能介紹
下面再來看一下Golang官網(wǎng)對于unsafe.Pointer的介紹:
- 任意類型的指針值都可以轉(zhuǎn)換為unsafe.Pointer(A pointer value of any type can be converted to a Pointer.)
- unsafe.Pointer可以轉(zhuǎn)換為任意類型的指針值(A Pointer can be converted to a pointer value of any type.)
- uintptr可以轉(zhuǎn)換為unsafe.Pointer(A uintptr can be converted to a Pointer.)
- unsafe.Pointer可以轉(zhuǎn)換為uintptr(A Pointer can be converted to a uintptr.)
從上面的功能介紹可以看到,Pointer允許程序突破Golang的類型系統(tǒng)的限制司澎,任意讀寫內(nèi)存欺缘,使用時(shí)需要額外小心,正如它的包名unsafe所提示的一樣挤安。
PS:uintptr本質(zhì)上是一個(gè)用于表示地址值的無符號整數(shù)谚殊,而不是一個(gè)引用,它表示程序中使用的某個(gè)對象的地址值蛤铜。
3)指針類型轉(zhuǎn)換
下面以int64轉(zhuǎn)換為int為例子嫩絮,說明unsafe.Pointer在指針類型轉(zhuǎn)換時(shí)的使用,如下:
func main() {
i := int64(1)
var iPtr *int
// iPtr = &i // 錯(cuò)誤
iPtr = (*int)(unsafe.Pointer(&i))
fmt.Printf("%d\n", *iPtr)
}
注意围肥,這種類型轉(zhuǎn)換剿干,需要保證轉(zhuǎn)換后的類型的大小不大于轉(zhuǎn)換前的類型,且具有相同的內(nèi)存布局穆刻,則可將數(shù)據(jù)解釋為另一個(gè)類型置尔。反之,如將int32的指針氢伟,轉(zhuǎn)換為int64的指針榜轿,在后續(xù)的讀寫中,可能會(huì)發(fā)生錯(cuò)誤朵锣。
4)讀寫結(jié)構(gòu)內(nèi)部成員
上面的類型轉(zhuǎn)換只是一個(gè)簡單的例子谬盐,在實(shí)際開發(fā)中,使用unsafe.Pointer進(jìn)行類型轉(zhuǎn)換一般用于讀取結(jié)構(gòu)的私有成員變量或者修改結(jié)構(gòu)的變量猪勇,下面以修改一個(gè)string變量的值為例子设褐,說明類型轉(zhuǎn)換對于任意內(nèi)存讀寫。
我們先來看看在Golang中string是如何定義的:
type stringStruct struct {
str unsafe.Pointer
len int
}
string的結(jié)構(gòu)由是由一個(gè)指向字節(jié)數(shù)組的unsafe.Pointer和int類型的長度字段組成,我們可以定義一下與其結(jié)構(gòu)相同的類型助析,并通過unsafe.Pointer把string的指針轉(zhuǎn)換并賦值到新類型的變量中犀被,通過操作該變量來讀寫string內(nèi)部的成員。
在Golang中已經(jīng)存在這樣的結(jié)構(gòu)體了外冀,它就是reflect.StringHeader寡键,它的定義如下:
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
Data uintptr
Len int
}
unsafe.Pointer與uintptr在內(nèi)存結(jié)構(gòu)上是相同的,下面通過一個(gè)原地修改字符串的值來演示相關(guān)的操作:
func main() {
str1 := "hello world"
hdr1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) // 注1
fmt.Printf("str:%s, data addr:%d, len:%d\n", str1, hdr1.Data, hdr1.Len)
str2 := "abc"
hdr2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
hdr1.Data = hdr2.Data // 注2
hdr1.Len = hdr2.Len // 注3
fmt.Printf("str:%s, data addr:%d, len:%d\n", str1, hdr1.Data, hdr1.Len)
}
其運(yùn)行結(jié)果如下:
str:hello world, data addr:4996513, len:11
str:abc, data addr:4992867, len:3
代碼解釋:
- 注1:該行代碼是把str1轉(zhuǎn)化為unsafe.Pointer后雪隧,再把unsafe.Pointer轉(zhuǎn)換來StringHeader的指針西轩,然后通過讀寫hdr1的成員即可讀寫str1成員的值
- 注2:通過修改hdr1的Data的值,修改str1的字節(jié)數(shù)組的指向
- 注3:為了保證字符串的結(jié)果是完整的脑沿,通過修改hdr1的Len的值藕畔,修改str1的長度字段
最后,str1的值庄拇,已經(jīng)被修改成了str2的值注服,即"abc"。
5)指針運(yùn)算
下面的代碼措近,模擬了通過指針移動(dòng)溶弟,遍歷slice的功能,其本質(zhì)思想是瞭郑,找到slice的第一個(gè)元素的地址辜御,然后通過加上slice每個(gè)元素所占的大小作為偏移量,實(shí)現(xiàn)指針的移動(dòng)和運(yùn)算屈张。
func main() {
data := []byte("abcd")
for i := 0; i < len(data); i++ {
ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&data[0])) + uintptr(i)*unsafe.Sizeof(data[0]))
fmt.Printf("%c,", *(*byte)(unsafe.Pointer(ptr)))
}
fmt.Printf("\n")
}
其運(yùn)行結(jié)果如下:
a,b,c,d,
代碼解釋:
要理解上述代碼擒权,首選需要了解兩個(gè)原則,分別是:
- 其他類型的指針只能轉(zhuǎn)化為unsafe.Pointer阁谆,也只有unsafe.Pointer才能轉(zhuǎn)化成任意類型的指針
- 只有uintptr才支持加減操作菜拓,而uintptr是一個(gè)非負(fù)整數(shù),表示地址值笛厦,沒有類型信息,以字節(jié)為單位
for循環(huán)的ptr賦值是該例子中的重點(diǎn)代碼俺夕,它表示:
- 把data的第0個(gè)元素的地址裳凸,轉(zhuǎn)化為unsafe.Pointer,再把它轉(zhuǎn)換成uintptr劝贸,用于加減運(yùn)算姨谷,即(uintptr(unsafe.Pointer(&data[0])) )
- 加上第i個(gè)元素的偏移量,得到一個(gè)新的uintptr值映九,計(jì)算方法為i每個(gè)元素所占的字節(jié)數(shù)梦湘,即(+ uintptr(i)unsafe.Sizeof(data[0]))
- 把新的uintptr再轉(zhuǎn)化為unsafe.Pointer,用于在后續(xù)的打印操作中,轉(zhuǎn)化為實(shí)際類型的指針
三捌议、總結(jié)
閱讀本文后哼拔,希望能讓你對unsafe.Pointer有一定的了解,總的來說瓣颅,它的作用是用于打破類型系統(tǒng)實(shí)現(xiàn)更靈活的內(nèi)存讀寫倦逐。但同時(shí)也是不安全的,使用時(shí)需要額外小心宫补。
總結(jié)一下unsafe.Pointer的使用法則就是:
- 任意類型的指針值都可以轉(zhuǎn)換為unsafe.Pointer檬姥,unsafe.Pointer也可以轉(zhuǎn)換為任意類型的指針值
- unsafe.Pointer與uintptr可以實(shí)現(xiàn)相互轉(zhuǎn)換
- 可以通過uintptr可以進(jìn)行加減操作,從而實(shí)現(xiàn)指針的運(yùn)算