您好,登錄后才能下訂單哦!
之前已經(jīng)用到過(guò)很多次指針了,不過(guò)大多數(shù)時(shí)候是指指針類(lèi)型及其對(duì)應(yīng)的指針值。這里要講更為深入的內(nèi)容。
從傳統(tǒng)意義上說(shuō),指針是一個(gè)指向某個(gè)確切的內(nèi)存地址的值。這個(gè)內(nèi)存地址可以是任何數(shù)據(jù)或代碼的起始地址,比如,某個(gè)變量、某個(gè)字段或某個(gè)函數(shù)。
uintptr
在Go語(yǔ)言中還有其他幾樣?xùn)|西可以代表“指針”。其中最貼近傳統(tǒng)意義的當(dāng)屬u(mài)intptr類(lèi)型了。該類(lèi)型實(shí)際上是一個(gè)數(shù)值類(lèi)型,也是Go語(yǔ)言?xún)?nèi)建的數(shù)據(jù)類(lèi)型之一。根據(jù)當(dāng)前計(jì)算機(jī)的計(jì)算架構(gòu)的不同,它可以存儲(chǔ)32位或64位的無(wú)符號(hào)整數(shù),可以代表任何指針的位(bit)模式,也就是原始的內(nèi)存地址。
unsafe.Pointer
在Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中的unsafe包。unsafe包中有一個(gè)類(lèi)型叫做Pointer,也代表了“指針”。unsafe.Pointer可以表示任何指向可尋址的值的指針,同時(shí)它也是前面提到的指針值和uintptr值之間的橋梁。也就是說(shuō),通過(guò)它,我們可以在這兩種值之上進(jìn)行雙向的轉(zhuǎn)換。這里有一個(gè)很關(guān)鍵的詞——可尋址的(addressable)。在我們繼續(xù)說(shuō)unsafe.Pointer之前,需要先要搞清楚這個(gè)詞的確切含義。
一下的值都是不可尋址的:
上面一堆術(shù)語(yǔ),看看在代碼里具體指的是哪些類(lèi)型:
package main
type Named interface {
// Name 用于獲取名字。
Name() string
}
type Dog struct {
name string
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func main() {
// 示例1。
const num = 123
//_ = &num // 常量不可尋址。
//_ = &(123) // 基本類(lèi)型值的字面量不可尋址。
var str = "abc"
_ = str
//_ = &(str[0]) // 對(duì)字符串變量的索引結(jié)果值不可尋址。
//_ = &(str[0:2]) // 對(duì)字符串變量的切片結(jié)果值不可尋址。
str2 := str[0]
_ = &str2 // 但這樣的尋址就是合法的。
//_ = &(123 + 456) // 算術(shù)操作的結(jié)果值不可尋址。
num2 := 456
_ = num2
//_ = &(num + num2) // 算術(shù)操作的結(jié)果值不可尋址。
//_ = &([3]int{1, 2, 3}[0]) // 對(duì)數(shù)組字面量的索引結(jié)果值不可尋址。
//_ = &([3]int{1, 2, 3}[0:2]) // 對(duì)數(shù)組字面量的切片結(jié)果值不可尋址。
_ = &([]int{1, 2, 3}[0]) // 對(duì)切片字面量的索引結(jié)果值卻是可尋址的。
//_ = &([]int{1, 2, 3}[0:2]) // 對(duì)切片字面量的切片結(jié)果值不可尋址。
//_ = &(map[int]string{1: "a"}[0]) // 對(duì)字典字面量的索引結(jié)果值不可尋址。
var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
_ = map1
//_ = &(map1[2]) // 對(duì)字典變量的索引結(jié)果值不可尋址。
//_ = &(func(x, y int) int {
// return x + y
//}) // 字面量代表的函數(shù)不可尋址。
//_ = &(fmt.Sprintf) // 標(biāo)識(shí)符代表的函數(shù)不可尋址。
//_ = &(fmt.Sprintln("abc")) // 對(duì)函數(shù)的調(diào)用結(jié)果值不可尋址。
dog := Dog{"little pig"}
_ = dog
//_ = &(dog.Name) // 標(biāo)識(shí)符代表的函數(shù)不可尋址。
//_ = &(dog.Name()) // 對(duì)方法的調(diào)用結(jié)果值不可尋址。
//_ = &(Dog{"little pig"}.name) // 結(jié)構(gòu)體字面量的字段不可尋址。
//_ = &(interface{}(dog)) // 類(lèi)型轉(zhuǎn)換表達(dá)式的結(jié)果值不可尋址。
dogI := interface{}(dog)
_ = dogI
//_ = &(dogI.(Named)) // 類(lèi)型斷言表達(dá)式的結(jié)果值不可尋址。
named := dogI.(Named)
_ = named
//_ = &(named.(Dog)) // 類(lèi)型斷言表達(dá)式的結(jié)果值不可尋址。
var chan1 = make(chan int, 1)
chan1 <- 1
//_ = &(<-chan1) // 接收表達(dá)式的結(jié)果值不可尋址。
}
總結(jié)一個(gè)不可尋址的值的特點(diǎn):
最后,如果把臨時(shí)結(jié)果賦值給一個(gè)變量,那么它就是可尋址的了。
不可尋址的值的限制
無(wú)法使用取址操作符&獲取他們的指針。如果嘗試取址會(huì)是編譯器報(bào)錯(cuò),所以不用太擔(dān)心。這里再看個(gè)小問(wèn)題:
package main
import "fmt"
type Dog struct {
name string
}
func (d *Dog) SetName (name string) {
d.name = name
}
func New(name string) Dog {
return Dog{name}
}
func main() {
obj := New("Snoopy")
obj.SetName("Goofy")
fmt.Println(obj.name)
// New("Snoopy").SetName("Wishbone") //
}
這里寫(xiě)了一個(gè)New函數(shù),用于獲取Dog的結(jié)構(gòu)體。返回的是結(jié)構(gòu)體的值類(lèi)型。還有一個(gè)指針?lè)椒?,這里直接對(duì)值類(lèi)型調(diào)用指針?lè)椒ㄊ菦](méi)有問(wèn)題的。因?yàn)闀?huì)被自動(dòng)轉(zhuǎn)譯成(&dog).SetName("Goofy")
。但是New函數(shù)的調(diào)用結(jié)果值是不可尋址的,所以最后一行嘗試直接以鏈?zhǔn)降姆椒ㄕ{(diào)用就會(huì)有編譯問(wèn)題。這個(gè)不可取址的情況應(yīng)該是屬于臨時(shí)結(jié)果,所以把結(jié)果賦值給一個(gè)變量,再調(diào)用指針?lè)椒ㄊ菦](méi)有問(wèn)題的。
自增++和自減--
另外,在Go語(yǔ)言中++和--不屬于操作符,而是自增語(yǔ)句或自減語(yǔ)句的組成部分。只要在++或--的左邊添加一個(gè)表達(dá)式,就組成了一個(gè)自增語(yǔ)句或自減語(yǔ)句,但是表達(dá)式的結(jié)果值必須是可尋址的。比如值字面的表達(dá)式就是無(wú)法自增的1++
。
這里也有例外,字典字面量和字典變量索引表達(dá)式的結(jié)果值都是不可尋址的,但是可以自增、自減。
類(lèi)似的規(guī)則還有兩個(gè):
下面講的方法,可以繞過(guò)Go語(yǔ)言的編譯器和其他工具的重重檢查,并達(dá)到潛入內(nèi)存修改數(shù)據(jù)的目的。這不是一種正常的手段,使用它會(huì)很危險(xiǎn),還很可能造成安全隱患。我們總是應(yīng)該優(yōu)先使用常規(guī)代碼包中提供的API去編寫(xiě)程序,當(dāng)然也可以把像reflect以及go/ast這樣的代碼包作為備選項(xiàng)。
指針值、unsafe.Pointer、uintptr有如下的轉(zhuǎn)換規(guī)則:
所以說(shuō)unsafe.Pointer是指針值和uintptr值之間的橋梁。到這一步,我們現(xiàn)在已經(jīng)可以獲取到變量的uintptr類(lèi)型的值了:
s := student{}
sP := &s
sPtr := uintptr(unsafe.Pointer(sP))
unsafe.Offsetof 的使用
unsafe.Offsetof函數(shù)返回變量(struct類(lèi)型)指定屬性的偏移量,以字節(jié)為單位。如下使用就可以獲取到結(jié)構(gòu)體的屬性相對(duì)于結(jié)構(gòu)體的偏移量了:
func main() {
type student struct {
name string
age int
}
s1 := student{}
p1 := unsafe.Offsetof((&s1).name) // 結(jié)構(gòu)體的第一個(gè)變量,偏移量是0
p2 := unsafe.Offsetof((&s1).age) // 這里就會(huì)有偏移量了
fmt.Println(p1, p2)
}
搭配使用獲取屬性的地址
簡(jiǎn)單的把結(jié)構(gòu)體的地址和屬性的偏移量相加,就能獲得屬性的地址了。獲取到了屬性的地址后,如果再對(duì)這個(gè)地址做兩次地址轉(zhuǎn)換,就變回屬性的指針值了:
package main
import (
"unsafe"
"fmt"
)
func main() {
type student struct {
name string
age int
}
s1 := student{"Adam", 18}
s1P := &s1
s1Ptr := uintptr(unsafe.Pointer(s1P)) // 結(jié)構(gòu)體的地址
fmt.Println(s1Ptr)
namePtr := s1Ptr + unsafe.Offsetof(s1P.name) // name屬性的地址
agePtr := s1Ptr + unsafe.Offsetof(s1P.age) // age屬性的地址
fmt.Println(namePtr, agePtr)
nameP := (*string)(unsafe.Pointer(namePtr)) // 獲取到屬性的指針
ageP := (*int)(unsafe.Pointer(agePtr))
fmt.Println(*nameP, *ageP) // 取值獲取到屬性指針的值
}
上面的方法,饒了一大圈就是為了獲取到結(jié)構(gòu)體里屬性的地址。有了地址就可以對(duì)操作,也就可以直接修改埋藏的很深的內(nèi)部數(shù)據(jù)了。比如可以直接結(jié)果別的包里的結(jié)構(gòu)體內(nèi)的不可導(dǎo)出的屬性值。
修改結(jié)構(gòu)體不可導(dǎo)出的屬性值
知識(shí)點(diǎn)都在上面了,這里直接試著修改別的包的結(jié)構(gòu)體內(nèi)的不可導(dǎo)出的屬性的值:
// article15/example06/model/s.go
package model
// 結(jié)構(gòu)體屬性全小寫(xiě)
type Student struct {
name string
age int
}
// article15/example06/main.go
package main
import (
"Go36/article15/example06/model"
"fmt"
"unsafe"
)
func main() {
s1 := model.Student{}
s1P := &s1
s1Ptr := uintptr(unsafe.Pointer(s1P))
namePtr := s1Ptr + 0
agePtr := s1Ptr + 16
nameP := (*string)(unsafe.Pointer(namePtr))
ageP := (*int)(unsafe.Pointer(agePtr))
*nameP = "Adam"
*ageP = 22
fmt.Println(s1)
}
這里unsafe.Pointer類(lèi)型和uintptr類(lèi)型所代表指針更貼近于底層和內(nèi)存,理論上可以利用它們?nèi)ピL問(wèn)或修改一些內(nèi)部數(shù)據(jù)。但是這么用會(huì)帶來(lái)安全隱患,在很多時(shí)候,使用它們操縱數(shù)據(jù)是弊大于利的??傊谰托辛?,別這么用。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。