指針
之前已經用到過很多次指針了,不過大多數時候是指指針類型及其對應的指針值。這裏要講更爲深入的內容。
其他指針
從傳統意義上說,指針是一個指向某個確切的內存地址的值。這個內存地址可以是任何數據或代碼的起始地址,比如,某個變量、某個字段或某個函數。
uintptr
在Go語言中還有其他幾樣東西可以代表“指針”。其中最貼近傳統意義的當屬uintptr類型了。該類型實際上是一個數值類型,也是Go語言內建的數據類型之一。根據當前計算機的計算架構的不同,它可以存儲32位或64位的無符號整數,可以代表任何指針的位(bit)模式,也就是原始的內存地址。
unsafe.Pointer
在Go語言標準庫中的unsafe包。unsafe包中有一個類型叫做Pointer,也代表了“指針”。unsafe.Pointer可以表示任何指向可尋址的值的指針,同時它也是前面提到的指針值和uintptr值之間的橋樑。也就是說,通過它,我們可以在這兩種值之上進行雙向的轉換。這里有一個很關鍵的詞——可尋址的(addressable)。在我們繼續說unsafe.Pointer之前,需要先要搞清楚這個詞的確切含義。
不可尋址的值
一下的值都是不可尋址的:
- 常量的值
- 基本類型值的字面量
- 算數操作的結果值
- 對各種字面量的索引表達式和切片表達式的結果值。例外,切片字面量的索引結果值是可尋址的
- 對字符串變量的索引表達式和切片表達式的結果值
- 對字典變量的索引表達式的結果值
- 函數字面量和方法字面量,以及對他們的調用表達式的結果值
- 結構體字面量的字段值,也就是對結構體字面量的選擇表達式的結果值
- 類型轉換表達式的結果值
- 類型斷言表達式的結果值
- 接收表達式的結果值
上面一堆術語,看看在代碼裏具體指的是哪些類型:
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) // 基本類型值的字面量不可尋址。
var str = "abc"
_ = str
//_ = &(str[0]) // 對字符串變量的索引結果值不可尋址。
//_ = &(str[0:2]) // 對字符串變量的切片結果值不可尋址。
str2 := str[0]
_ = &str2 // 但這樣的尋址就是合法的。
//_ = &(123 + 456) // 算術操作的結果值不可尋址。
num2 := 456
_ = num2
//_ = &(num + num2) // 算術操作的結果值不可尋址。
//_ = &([3]int{1, 2, 3}[0]) // 對數組字面量的索引結果值不可尋址。
//_ = &([3]int{1, 2, 3}[0:2]) // 對數組字面量的切片結果值不可尋址。
_ = &([]int{1, 2, 3}[0]) // 對切片字面量的索引結果值卻是可尋址的。
//_ = &([]int{1, 2, 3}[0:2]) // 對切片字面量的切片結果值不可尋址。
//_ = &(map[int]string{1: "a"}[0]) // 對字典字面量的索引結果值不可尋址。
var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
_ = map1
//_ = &(map1[2]) // 對字典變量的索引結果值不可尋址。
//_ = &(func(x, y int) int {
// return x + y
//}) // 字面量代表的函數不可尋址。
//_ = &(fmt.Sprintf) // 標識符代表的函數不可尋址。
//_ = &(fmt.Sprintln("abc")) // 對函數的調用結果值不可尋址。
dog := Dog{"little pig"}
_ = dog
//_ = &(dog.Name) // 標識符代表的函數不可尋址。
//_ = &(dog.Name()) // 對方法的調用結果值不可尋址。
//_ = &(Dog{"little pig"}.name) // 結構體字面量的字段不可尋址。
//_ = &(interface{}(dog)) // 類型轉換表達式的結果值不可尋址。
dogI := interface{}(dog)
_ = dogI
//_ = &(dogI.(Named)) // 類型斷言表達式的結果值不可尋址。
named := dogI.(Named)
_ = named
//_ = &(named.(Dog)) // 類型斷言表達式的結果值不可尋址。
var chan1 = make(chan int, 1)
chan1 <- 1
//_ = &(<-chan1) // 接收表達式的結果值不可尋址。
}
總結一個不可尋址的值的特點:
- 不可變的值不可尋址。常量、基本類型的值字面量、字符串變量的值、函數以及方法的字面量都是如此。其實這樣規定也有安全性方面的考慮。
- 絕大多數被視爲臨時結果的值都是不可尋址的。算術操作的結果值屬於臨時結果,針對值字面量的表達式結果值也屬於臨時結果。但有一個例外,對切片字面量的索引結果值雖然也屬於臨時結果,但卻是可尋址的。
- 若拿到某值的指針可能會破壞程序的一致性,那麼就是不安全的,該值就不可尋址。由於字典的內部機制,對字典的索引結果值的取址操作都是不安全的。另外,獲取由字面量或標識符代表的函數或方法的地址顯然也是不安全的。
最後,如果把臨時結果賦值給一個變量,那麼它就是可尋址的了。
不可尋址的值的限制
無法使用取址操作符&獲取他們的指針。如果嘗試取址會是編譯器報錯,所以不用太擔心。這裏再看個小問題:
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") //
}
這裏寫了一個New函數,用於獲取Dog的結構體。返回的是結構體的值類型。還有一個指針方法,這裏直接對值類型調用指針方法是沒有問題的。因爲會被自動轉譯成(&dog).SetName("Goofy")
。但是New函數的調用結果值是不可尋址的,所以最後一行嘗試直接以鏈式的方法調用就會有編譯問題。這個不可取址的情況應該是屬於臨時結果,所以把結果賦值給一個變量,再調用指針方法是沒有問題的。
自增++和自減--
另外,在Go語言中++和--不屬於操作符,而是自增語句或自減語句的組成部分。只要在++或--的左邊添加一個表達式,就組成了一個自增語句或自減語句,但是表達式的結果值必須是可尋址的。比如值字面的表達式就是無法自增的1++
。
這裏也有例外,字典字面量和字典變量索引表達式的結果值都是不可尋址的,但是可以自增、自減。
類似的規則還有兩個:
- 賦值語句,賦值操作符左邊的表達式的結果值必須是可尋址的。但是對字典的索引結果值也是賦值
- 帶有range子句的for語句中,在range關鍵字左邊的表達式的結果值也必須是可尋址的。還是對字典的索引結果值例外。
unsafe.Pointer黑科技
下面講的方法,可以繞過Go語言的編譯器和其他工具的重重檢查,並達到潛入內存修改數據的目的。這不是一種正常的手段,使用它會很危險,還很可能造成安全隱患。我們總是應該優先使用常規代碼包中提供的API去編寫程序,當然也可以把像reflect以及go/ast這樣的代碼包作爲備選項。
指針值、unsafe.Pointer、uintptr有如下的轉換規則:
- 指針值和unsafe.Pointer可以互相轉換
- uintptr和unsafe.Pointer也可以互相轉換
- 指針值和uintptr無法直接互相轉換
所以說unsafe.Pointer是指針值和uintptr值之間的橋樑。到這一步,我們現在已經可以獲取到變量的uintptr類型的值了:
s := student{}
sP := &s
sPtr := uintptr(unsafe.Pointer(sP))
unsafe.Offsetof 的使用
unsafe.Offsetof函數返回變量(struct類型)指定屬性的偏移量,以字節爲單位。如下使用就可以獲取到結構體的屬性相對於結構體的偏移量了:
func main() {
type student struct {
name string
age int
}
s1 := student{}
p1 := unsafe.Offsetof((&s1).name) // 結構體的第一個變量,偏移量是0
p2 := unsafe.Offsetof((&s1).age) // 這裏就會有偏移量了
fmt.Println(p1, p2)
}
搭配使用獲取屬性的地址
簡單的把結構體的地址和屬性的偏移量相加,就能獲得屬性的地址了。獲取到了屬性的地址後,如果再對這個地址做兩次地址轉換,就變回屬性的指針值了:
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)) // 結構體的地址
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) // 取值獲取到屬性指針的值
}
上面的方法,饒了一大圈就是爲了獲取到結構體裏屬性的地址。有了地址就可以對操作,也就可以直接修改埋藏的很深的內部數據了。比如可以直接結果別的包裏的結構體內的不可導出的屬性值。
修改結構體不可導出的屬性值
知識點都在上面了,這裏直接試着修改別的包的結構體內的不可導出的屬性的值:
// article15/example06/model/s.go
package model
// 結構體屬性全小寫
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類型和uintptr類型所代表指針更貼近於底層和內存,理論上可以利用它們去訪問或修改一些內部數據。但是這麼用會帶來安全隱患,在很多時候,使用它們操縱數據是弊大於利的。總之知道就行了,別這麼用。