Go36-15-指針

指針

之前已經用到過很多次指針了,不過大多數時候是指指針類型及其對應的指針值。這裏要講更爲深入的內容。

其他指針

從傳統意義上說,指針是一個指向某個確切的內存地址的值。這個內存地址可以是任何數據或代碼的起始地址,比如,某個變量、某個字段或某個函數。

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) // 接收表達式的結果值不可尋址。
}

總結一個不可尋址的值的特點:

  1. 不可變的值不可尋址。常量、基本類型的值字面量、字符串變量的值、函數以及方法的字面量都是如此。其實這樣規定也有安全性方面的考慮。
  2. 絕大多數被視爲臨時結果的值都是不可尋址的。算術操作的結果值屬於臨時結果,針對值字面量的表達式結果值也屬於臨時結果。但有一個例外,對切片字面量的索引結果值雖然也屬於臨時結果,但卻是可尋址的。
  3. 若拿到某值的指針可能會破壞程序的一致性,那麼就是不安全的,該值就不可尋址。由於字典的內部機制,對字典的索引結果值的取址操作都是不安全的。另外,獲取由字面量或標識符代表的函數或方法的地址顯然也是不安全的。

最後,如果把臨時結果賦值給一個變量,那麼它就是可尋址的了。

不可尋址的值的限制
無法使用取址操作符&獲取他們的指針。如果嘗試取址會是編譯器報錯,所以不用太擔心。這裏再看個小問題:

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++
這裏也有例外,字典字面量和字典變量索引表達式的結果值都是不可尋址的,但是可以自增、自減。
類似的規則還有兩個:

  1. 賦值語句,賦值操作符左邊的表達式的結果值必須是可尋址的。但是對字典的索引結果值也是賦值
  2. 帶有range子句的for語句中,在range關鍵字左邊的表達式的結果值也必須是可尋址的。還是對字典的索引結果值例外。

unsafe.Pointer黑科技

下面講的方法,可以繞過Go語言的編譯器和其他工具的重重檢查,並達到潛入內存修改數據的目的。這不是一種正常的手段,使用它會很危險,還很可能造成安全隱患。我們總是應該優先使用常規代碼包中提供的API去編寫程序,當然也可以把像reflect以及go/ast這樣的代碼包作爲備選項。
指針值、unsafe.Pointer、uintptr有如下的轉換規則:

  1. 指針值和unsafe.Pointer可以互相轉換
  2. uintptr和unsafe.Pointer也可以互相轉換
  3. 指針值和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類型所代表指針更貼近於底層和內存,理論上可以利用它們去訪問或修改一些內部數據。但是這麼用會帶來安全隱患,在很多時候,使用它們操縱數據是弊大於利的。總之知道就行了,別這麼用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章