go 指針使用

參考:https://time.geekbang.org/column/article/18042?utm_source=weibo&utm_medium=xuxiaoping&utm_campaign=promotion&utm_content=columns面試

指針示例:編程

type Dog struct {
  name string
}

func (dog *Dog) SetName(name string) {
  dog.name = name
}

對於基本類型Dog來講,*Dog就是它的指針類型。而對於一個Dog類型,值不爲nil的變量dog,取址表達式&dog的結果就是該變量的值(也就是基本值)的指針值。數組

若是一個方法的接收者是*Dog類型的,那麼該方法就是基本類型Dog的指針方法。安全

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

在 Go 語言中還有其餘幾樣東西能夠表明「指針」。其中最貼近傳統意義的當屬uintptr類型了。該類型其實是一個數值類型,也是 Go 語言內建的數據類型之一。函數

再來看 Go 語言標準庫中的unsafe包。unsafe包中有一個類型叫作Pointer,也表明了「指針」。工具

unsafe.Pointer能夠表示任何指向可尋址的值的指針,同時它也是前面提到的指針值和uintptr值之間的橋樑。也就是說,經過它,咱們能夠在這兩種值之上進行雙向的轉換。這裏有一個很關鍵的詞——可尋址的(addressable)。在咱們繼續說unsafe.Pointer以前,須要先要搞清楚這個詞的確切含義。ui

你能列舉出 Go 語言中的哪些值是不可尋址的嗎?指針

常量的值。
基本類型值的字面量。
算術操做的結果值。
對各類字面量的索引表達式和切片表達式的結果值。
不過有一個例外,對切片字面量的索引結果值倒是可尋址的。
對字符串變量的索引表達式和切片表達式的結果值。
對字典變量的索引表達式的結果值。函數字面量和方法字面量,以及對它們的調用表達式的結果值。結構體字面量的字段值,也就是對結構體字面量的選擇表達式的結果值。類型轉換表達式的結果值。類型斷言表達式的結果值。接收表達式的結果值。code

常量的值老是會被存儲到一個確切的內存區域中,而且這種值確定是不可變的。基本類型值的字面量也是同樣,其實它們本就能夠被視爲常量,只不過沒有任何標識符能夠表明它們罷了。

第一個關鍵詞:不可變的。因爲 Go 語言中的字符串值也是不可變的,因此對於一個字符串類型的變量來講,基於它的索引或切片的結果值也都是不可尋址的,由於即便拿到了這種值的內存地址也改變不了什麼

第二個關鍵詞:臨時結果。這個關鍵詞能被用來解釋不少現象。咱們能夠把各類對值字面量施加的表達式的求值結果都看作是臨時結果。

Go 語言中的表達式有不少種,其中經常使用的包括如下幾種。
用於得到某個元素的索引表達式。
用於得到某個切片(片斷)的切片表達式。
用於訪問某個字段的選擇表達式。
用於調用某個函數或方法的調用表達式。
用於轉換值的類型的類型轉換表達式。
用於判斷值的類型的類型斷言表達式。

向通道發送元素值或從通道那裏接收元素值的接收表達式。咱們把以上這些表達式施加在某個值字面量上通常都會獲得一個臨時結果。好比,對數組字面量和字典字面量的索引結果值,又好比,對數組字面量和切片字面量的切片結果值。它們都屬於臨時結果,都是不可尋址的。

一個須要特別注意的例外是,對切片字面量的索引結果值是可尋址的。由於不論怎樣,每一個切片值都會持有一個底層數組,而這個底層數組中的每一個元素值都是有一個確切的內存地址的。

我一直在說針對數組值、切片值或字典值的字面量的表達式會產生臨時結果。若是針對的是數組類型或切片類型的變量,那麼索引或切片的結果值就都不屬於臨時結果了,是可尋址的。

這主要由於變量的值自己就不是「臨時的」。對比而言,值字面量在尚未與任何變量(或者說任何標識符)綁定以前是沒有落腳點的,咱們沒法以任何方式引用到它們。這樣的值就是「臨時的」。

再說一個例外。咱們經過對字典類型的變量施加索引表達式,獲得的結果值不屬於臨時結果,但是,這樣的值倒是不可尋址的。緣由是,字典中的每一個鍵 - 元素對的存儲位置均可能會變化,並且這種變化外界是沒法感知的。典中總會有若干個哈希桶用於均勻地儲存鍵 - 元素對。當知足必定條件時,字典可能會改變哈希桶的數量,並適時地把其中的鍵 - 元素對搬運到對應的新的哈希桶中。

在這種狀況下,獲取字典中任何元素值的指針都是無心義的,也是不安全的。咱們不知道何時那個元素值會被搬運到何處,也不知道原先的那個內存地址上還會被存放什麼別的東西。因此,這樣的值就應該是不可尋址的。

第三個關鍵詞:不安全的。「不安全的」操做極可能會破壞程序的一致性,引起不可預知的錯誤,從而嚴重影響程序的功能和穩定性。

函數在 Go 語言中是一等公民,因此咱們能夠把表明函數或方法的字面量或標識符賦給某個變量、傳給某個函數或者從某個函數傳出。可是,這樣的函數和方法都是不可尋址的。一個緣由是函數就是代碼,是不可變的。

另外一個緣由是,拿到指向一段代碼的指針是不安全的。此外,對函數或方法的調用結果值也是不可尋址的,這是由於它們都屬於臨時結果。

總結一下

不可變的值不可尋址。常量、基本類型的值字面量、字符串變量的值、函數以及方法的字面量都是如此。其實這樣規定也有安全性方面的考慮。

絕大多數被視爲臨時結果的值都是不可尋址的。算術操做的結果值屬於臨時結果,針對值字面量的表達式結果值也屬於臨時結果。但有一個例外,對切片字面量的索引結果值雖然也屬於臨時結果,但倒是可尋址的。

若拿到某值的指針可能會破壞程序的一致性,那麼就是不安全的,該值就不可尋址。因爲字典的內部機制,對字典的索引結果值的取址操做都是不安全的。另外,獲取由字面量或標識符表明的函數或方法的地址顯然也是不安全的。

最後,若是咱們把臨時結果賦給一個變量,那麼它就是可尋址的了。如此一來,取得的指針指向的就是這個變量持有的那個值了。

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

}

問題 :不可尋址的值在使用上有哪些限制?

固然是沒法使用取址操做符&獲取它們的指針了
結構體類型Dog爲例

爲它編寫一個函數New。這個函數會接受一個名爲name的string類型的參數,並會用這個參數初始化一個Dog類型的值,最後返回該值。我如今要問的是:若是我調用該函數,並直接以鏈式的手法調用其結果值的指針方法SetName,那麼能夠達到預期的效果嗎?

若是你還記得我在前面講述的內容,那麼確定會知道調用New函數所獲得的結果值屬於臨時結果,是不可尋址的。

別忘了,我在講結構體類型及其方法的時候還說過,咱們能夠在一個基本類型的值上調用它的指針方法,這是由於 Go 語言會自動地幫咱們轉譯。

更具體地說,對於一個Dog類型的變量dog來講,調用表達式dog.SetName("monster")會被自動地轉譯爲(&dog).SetName("monster"),即:先取dog的指針值,再在該指針值上調用SetName方法。

於New函數的調用結果值是不可尋址的,因此沒法對它進行取址操做。所以,上邊這行鏈式調用會讓編譯器報告兩個錯誤,一個是果,即:不能在New("little pig")的結果值上調用指針方法。一個是因,即:不能取得New("little pig")的地址。

咱們都知道,Go 語言中的++和--並不屬於操做符,而分別是自增語句和自減語句的重要組成部分。

Go 語言規範中的語法定義是,只要在++或--的左邊添加一個表達式,就能夠組成一個自增語句或自減語句,可是,它還明確了一個很重要的限制,那就是這個表達式的結果值必須是可尋址的。這就使得針對值字面量的表達式幾乎都沒法被用在這裏。

不過這有一個例外,雖然對字典字面量和字典變量索引表達式的結果值都是不可尋址的,可是這樣的表達式卻能夠被用在自增語句和自減語句中。

與之相似的規則還有兩個。一個是,在賦值語句中,賦值操做符左邊的表達式的結果值必須可尋址的,可是對字典的索引結果值也是能夠的。

另外一個是,在帶有range子句的for語句中,在range關鍵字左邊的表達式的結果值也都必須是可尋址的,不過對字典的索引結果值一樣能夠被用在這裏。以上這三條規則咱們合併起來記憶就能夠了。

與這些定死的規則相比,我剛剛講到的那個與指針方法有關的問題,你須要好好理解一下,它涉及了兩個知識點的聯合運用。起碼在我面試的時候,它是一個可選擇的考點。

package main

type Dog struct {
    name string
}

func New(name string) Dog {
    return Dog{name}
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func main() {
    // 示例1。
    //New("little pig").SetName("monster") // 不能調用不可尋址的值的指針方法。

    // 示例2。
    map[string]int{"the": 0, "word": 0, "counter": 0}["word"]++
    map1 := map[string]int{"the": 0, "word": 0, "counter": 0}
    map1["word"]++
}

問題:怎樣經過unsafe.Pointer操縱可尋址的值?

前邊的基礎知識很重要。不過如今讓咱們再次關注指針的用法。我說過,unsafe.Pointer是像*Dog類型的值這樣的指針值和uintptr值之間的橋樑,那麼咱們怎樣利用unsafe.Pointer的中轉和uintptr的底層操做來操縱像dog這樣的值呢?

這是一項黑科技。它能夠繞過 Go 語言的編譯器和其餘工具的重重檢查,並達到潛入內存修改數據的目的。這並非一種正常的編程手段,使用它會很危險,頗有可能形成安全隱患。

咱們老是應該優先使用常規代碼包中提供的 API 去編寫程序,固然也能夠把像reflect以及go/ast這樣的代碼包做爲備選項。做爲上層應用的開發者,請謹慎地使用unsafe包中的任何程序實體。

我先聲明瞭一個Dog類型的變量dog,而後用取址操做符&,取出了它的指針值,並把它賦給了變量dogP。

最後,我使用了兩個類型轉換,先把dogP轉換成了一個unsafe.Pointer類型的值,而後緊接着又把後者轉換成了一個uintptr的值,並把它賦給了變量dogPtr。這背後隱藏着一些轉換規則,以下:
一個指針值(好比*Dog類型的值)能夠被轉換爲一個unsafe.Pointer類型的值,反之亦然。
一個uintptr類型的值也能夠被轉換爲一個unsafe.Pointer類型的值,反之亦然。
一個指針值沒法被直接轉換成一個uintptr類型的值,反過來也是如此。

因此,對於指針值和uintptr類型值之間的轉換,必須使用unsafe.Pointer類型的值做爲中轉。那麼,咱們把指針值轉換成uintptr類型的值有什麼意義嗎?

這裏須要與unsafe.Offsetof函數搭配使用才能看出端倪。unsafe.Offsetof函數用於獲取兩個值在內存中的起始存儲地址之間的偏移量,以字節爲單位。

這兩個值一個是某個字段的值,另外一個是該字段值所屬的那個結構體值。咱們在調用這個函數的時候,須要把針對字段的選擇表達式傳給它,好比dogP.name。

有了這個偏移量,又有告終構體值在內存中的起始存儲地址(這裏由dogPtr變量表明),把它們相加咱們就能夠獲得dogP的name字段值的起始存儲地址了。這個地址由變量namePtr表明。

此後,咱們能夠再經過兩次類型轉換把namePtr的值轉換成一個*string類型的值,這樣就獲得了指向dogP的name字段值的指針值。

你可能會問,我直接用取址表達式&(dogP.name)不就能拿到這個指針值了嗎?幹嗎繞這麼大一圈呢?你能夠想象一下,若是咱們根本就不知道這個結構體類型是什麼,也拿不到dogP這個變量,那麼還能去訪問它的name字段嗎?

答案是,只要有namePtr就能夠。它就是一個無符號整數,但同時也是一個指向了程序內部數據的內存地址。它可能會給咱們帶來一些好處,好比能夠直接修改埋藏得很深的內部數據。

可是,一旦咱們有意或無心地把這個內存地址泄露出去,那麼其餘人就可以肆意地改動dogP.name的值,以及周圍的內存地址上存儲的任何數據了。

即便他們不知道這些數據的結構也無所謂啊,改很差還改不壞嗎?不正確地改動必定會給程序帶來不可預知的問題,甚至形成程序崩潰。這可能仍是最好的災難性後果;因此我才說,使用這種非正常的編程手段會很危險。

好了,如今你知道了這種手段,也知道了它的危險性,那就謹慎對待

總結
咱們今天集中說了說與指針有關的問題。基於基本類型的指針值應該是咱們最經常使用到的,也是咱們最須要關注的,好比*Dog類型的值。怎樣獲得一個這樣的指針值呢?這須要用到取址操做和操做符&。

不過這裏還有個前提,那就是取址操做的操做對象必須是可尋址的。關於這方面你須要記住三個關鍵詞:不可變的、臨時結果和不安全的。只要一個值符合了這三個關鍵詞中的任何一個,它就是不可尋址的。

但有一個例外,對切片字面量的索引結果值是可尋址的。那麼不可尋址的值在使用上有哪些限制呢?一個最重要的限制是關於指針方法的,即:沒法調用一個不可尋址值的指針方法。這涉及了兩個知識點的聯合運用。

相比於剛說到的這些,unsafe.Pointer類型和uintptr類型的重要性好像就沒那麼高了。它們的值一樣能夠表明指針,而且比前面說的指針值更貼近於底層和內存。

雖然咱們能夠利用它們去訪問或修改一些內部數據,並且就靈活性而言,這種要比通用的方式高不少,可是這每每也會帶來不容小覷的安全隱患。

package main

import (
    "fmt"
    "unsafe"
)

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。
    dog := Dog{"little pig"}
    dogP := &dog
    dogPtr := uintptr(unsafe.Pointer(dogP))

    namePtr := dogPtr + unsafe.Offsetof(dogP.name)
    nameP := (*string)(unsafe.Pointer(namePtr))
    fmt.Printf("nameP == &(dogP.name)? %v\n",
        nameP == &(dogP.name))
    fmt.Printf("The name of dog is %q.\n", *nameP)

    *nameP = "monster"
    fmt.Printf("The name of dog is %q.\n", dogP.name)
    fmt.Println()

    // 示例2。
    // 下面這種不匹配的轉換雖然不會引起panic,可是其結果每每不符合預期。
    numP := (*int)(unsafe.Pointer(namePtr))
    num := *numP
    fmt.Printf("This is an unexpected number: %d\n", num)

}
go run demo37.go 
nameP == &(dogP.name)? true
The name of dog is "little pig".
The name of dog is "monster".

This is an unexpected number: 17596893
相關文章
相關標籤/搜索