Golang指標與nil淺析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

曾經聽說過一句話,編程的本質就是指標和遞迴。那會剛開始編碼,只是這兩個的概念有個感性粗淺的認識。最早接觸指標,莫過於C語言了,能否理解用好指標也成為一個合格C語言的基本標誌。

Golang也提供了指標,但是go不能進行指標運算,因此相對於C也少了很多複雜度。私以為,go之所以提供指標,並不是為了讓你更多和記憶體打交道,而是提供操作資料的基本橋樑。因為go很多調用,往往複製一份對象,例如函數的參數,如果沒有指標,有些情況不得不存在很多副本。

記憶體和變數

程式設計語言中一般都會有變數。變數儲存一些值。通常我們會對變數聲明,賦值,和銷毀等操作。

想象一下,記憶體好比一個長長的桌子,桌子有很多連續的抽屜(記憶體塊)。我們可以按照順序給每一個抽屜從0開始編號(記憶體位址),這個編號就是抽屜的地址。當我們需要使用抽屜存放東西的時候,就通過編號找到對應的抽屜,放好東西。這個東西就是我們存的資料。

 addr      1          2         3         4         5      +----------+---------+---------+---------+---------+      |          |         |         |         |         |      |          |         |         |         |         |      |  a book  |  none   |   none  |   none  |  a pen  |      |          |         |         |         |         |      |          |         |         |         |         |      +----------+---------+---------+---------+---------+

通過編號找東西固然不錯,可是有時候我們想直觀的知道抽屜裡放了什麼內容,就給抽屜外面貼上(聲明)一個標籤(變數名),比如編號5的抽屜式水果,編號7的抽屜式書啦。下次要找書,就直接找到貼有書標籤的抽屜即可。

 addr      1          2         3         4         5      +----------+---------+---------+---------+---------+      |          |         |         |         |         |      |          |         |         |         |         |      |  a book  |  none   |   none  |   none  |  a pen  |      |          |         |         |         |         |      |          |         |         |         |         |      +----------+---------+---------+---------+---------+ tag      book                                     pen

記憶體,記憶體位址,記憶體儲存的資料,變數名,這些概念幾乎是電腦通過程式設計語言執行程式的基本套路。只不過進階語言往往幫我們隱藏了記憶體位址和變數名的映射。像C這樣的可以聲明一個變數,然後賦值,而像Python,聲明和賦值甚至可以寫成一起。

指標

瞭解了記憶體位址和變數的關係,我們再看看指標。可以把指標看成是一種“類型”,這種類型的值是一個記憶體位址。例如有一個編號3抽屜,裡面存放了一個指標,而這個指標的值是一個編號5,通過操作指標,我們可以直接操作編號5的記憶體資料。

 addr      1          2         3         4         5      +----------+---------+---------+---------+---------+      |          |         |         |         |         |      |          |         |         |         |         |      |  a book  |  none   | 5 -->   |   none  |  a pen  |      |          |         |         |         |         |      |          |         |         |         |         |      +----------+---------+---------+---------+---------+ tag      book                pointer              pen

記住,指標的是記憶體位址,但是指標本身也是有記憶體位址的。正如指向別的抽屜,也有一個抽屜來儲存它自己。

golang指標的地址和值

進階語言提供完美聲明變數和值之間的綁定關係。幫我們隱藏了變數記憶體位址。想要擷取記憶體位址,需要在變數前加上一個符號&&即為取址符。例如變數a的記憶體位址為&a

對於一個指標,它的值是一個別處的地址,想要擷取這個地址的值,可以使用*符號。*即為取值符。例如上面的&a是一個地址,那麼這個地址裡儲存的值為*&a

由此可見,&*是是一對相愛相殺的兄弟,他們做著相反的事情。

初學指標的同學,往往混淆指標的值和指標地址的差別,指標的值是一個地址,是別的記憶體位址,指標的地址則是儲存指標記憶體塊的地址。例如你家裡裝著公司的鑰匙,這個鑰匙可以開啟公司的大門,而你家的大門需要你自己的鑰匙。

零值與nil

talk is cheaper,下面來看看golang中的指標相關操作

package mainimport "fmt"func main() {    // 聲明一個變數 aVar 類型為 string    var aVar string    fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 輸出 aVar: 0xc42000e240 ""}

我們聲明了一個字串類似的變數,尚未賦值,go就會自動賦予一個零值。字元的零值就是空子串。同時通過&符號讀取了變數的記憶體位址。

fmt.Printf 函數可以通過格式化字串列印出變數,p表示可以列印指標,v可以列印變數的值,#v可以列印變數的結構。

上面的過程可以用下面的簡圖來表示:

         addr        0xc42000e240                    +---------+                    |         |                    |   ""    |                    |         |                    |         |                    +---------+                       aVar

下面再聲明一個指標變數,使用*標記法宣告一個指標變數。

    // 聲明一個指標變數 aPot 其類型也是 string    var aPot *string    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 輸出 aPot: 0xc42000c030 (*string)(nil)

指標變數的零值不是空子串,而是nil。aPot的值是指標類型,由於尚未該指標尚未指向另外一個地址。因此初始化為nil。

這個過程可以用下面的圖表示:

         addr        0xc42000c030                    +---------+                    |         |                    |         |                    |  nil    |                    |         |                    +---------+                      aPot

正常的變數初始化之後,可以使用=賦值:

func main() {    // 聲明一個變數 aVar 類型為 string    var aVar string    fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 輸出 aVar: 0xc42000e240 ""    aVar = "This is a aVar"    fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 輸出 aVar: 0xc42000e240 "This is a aVar"}

普通變數賦值十分簡單,無非就是抽屜換一個值啦。

         addr        0xc42000e240                    +---------+                    | This is |                    |         |                    |  a aVar |                    |         |                    +---------+                       aVar

可是如果一個值為nil的指標變數,直接賦值會出問題。

func main(){    // 聲明一個指標變數 aPot 其類型也是 string    var aPot *string    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 輸出 aPot: 0xc42000c030 (*string)(nil)    *aPot = "This is a Pointer"  // 報錯: panic: runtime error: invalid memory address or nil pointer dereference}

出錯也很正常,*aPot = "This is a Pointer"的含義可以理解為,將aPot的指標地址的值賦予"This is a Pointer"。可是aPot的值是nil,但還沒有賦值成地址,因此不能把一個子串賦值給一個nil值。此外,即使不是賦值,對nil的指標通過*讀取也會報錯,畢竟讀取不到任何地址。

解決問題方式就是初始化一個記憶體,並把該記憶體位址賦予指標變數。

    // 聲明一個指標變數 aPot 其類型也是 string    var aPot *string    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 輸出 aPot: 0xc42000c030 (*string)(nil)    aPot = &aVar    *aPot = "This is a Pointer"    fmt.Printf("aVar: %p %#v \n", &aVar, aVar) // 輸出 aVar: 0xc42000e240 "This is a Pointer"     fmt.Printf("aPot: %p %#v %#v \n", &aPot, aPot, *aPot) // 輸出 aPot: 0xc42000c030 (*string)(0xc42000e240) "This is a Pointer"

我們把aVar的記憶體位址賦值給aPot,也可以看到aPot的值也就是aVar的地址,同時也可以通過*讀取aPot指標地址所指向的值,即aVar的值。

         addr        0xc42000c030             0xc42000c240                    +---------------+        +----------+                    |               |        |          |                    | 0oxc42000x240 |+-----> | This is  |                    |               |        |          |                    |               |        | a aVar   |                    +---------------+        +----------+                      aPot                      aVar

new 關鍵字

通過已經存在的aVar,我們可以給aPot指標賦值。可以如果沒有已存在都變數,go提供了new來初始化一個地址。

    var aNewPot *int    aNewPot = new(int)    *aNewPot = 217    fmt.Printf("aNewPot: %p %#v %#v \n", &aNewPot, aNewPot, *aNewPot) // 輸出 aNewPot: 0xc42007a028 (*int)(0xc42006e1f0) 217

new 可以開闢一個記憶體,然後返回這個記憶體的地址。因為int指標是簡單類型,因此new(int)的操作,除了可以開闢一個記憶體,還能為這個記憶體初始化零值,即0。

new 不僅可以為簡單類型開闢記憶體,也可以為複合參考型別開闢,不過後者初始化的零值還是nil,如果需要賦值,還會有別的問題,下面我們再討論。

複合類型與nil

int,string等是基礎類型,array則是基於這些基礎類型的複合類型。複合類型的指標初始化也需要注意:

    var arr [5]int    fmt.Printf("arr: %p %#v \n", &arr, arr) // arr: 0xc420014180 [5]int{0, 0, 0, 0, 0}     arr[0], arr[1] = 1, 2    fmt.Printf("arr: %p %#v \n", &arr, arr) // arr: 0xc420014180 [5]int{1, 2, 0, 0, 0}

聲明一個大小為5的數組,go會自動為數組的item初始化為零值,數組可以通過索引讀取和賦值。

如果聲明的是一個數組指標,即一個指標的類型是數組,這個指標如何初始化和賦值呢?

    var arrPot *[5]int    fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 (*[5]int)(nil)

從輸出可以看到,arrPot初始化的值是nil。我們已經瞭解,nil的值是不能直接賦值的,因此(*arrPot)[0] = 11直接賦值會拋錯。

new 關鍵之函數

既然如此,我們可以使用new建立一塊記憶體,並把記憶體位址給arrPot指標變數。然後賦值就正常啦。

    var arrPot *[5]int    fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 (*[5]int)(nil)     arrPot = new([5]int)    fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 &[5]int{0, 0, 0, 0, 0}     (*arrPot)[0] = 11    fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 &[5]int{11, 0, 0, 0, 0}

上面的記憶體配置圖如下:

  addr      0xc42000c040         +------------------+               0xc42000c099 (new建立的記憶體)         |                  |       +------+------+------+------+------+         |                  |       |      |      |      |      |      |         |    0xc42000c099  +-----> | 11   |  0   |   0  |  0   |  0   |         |                  |       |      |      |      |      |      |         |                  |       +------+------+------+------+------+         +------------------+              arrPot

參考型別與nil

Go的array是雖然是複合類型,但不是參考型別。go中的引用類似是slice,map等。下面我們就看看map類型如何初始化已經對nil的處理。

    var aMap map[string]string    fmt.Printf("aMap: %p %#v \n", &aMap, aMap)  // aMap: 0xc42000c048 map[string]string(nil)

聲明一個map類型的變數,map不像array那樣聲明之後可以初始化成零值。go會給參考型別初始化為nil,nil是不能直接賦值的。並且,map和數組指標還不一樣,不能使用new開闢一個記憶體,然後再賦值。aMap本身就是實值型別,聲明就已經初始化記憶體了,只不過其值是nil而已,我們不能修改地址。&aMap = new(map[string]string)這樣的操作會報錯。

make 關鍵字

既然無法使用new,那麼go提供了另外一個函數make。make不僅可以開闢一個記憶體,還能給這個記憶體的類型初始化其零值,同時返回這個記憶體執行個體。

    aMap = make(map[string]string)    aMap["name"] = "Golang"    fmt.Printf("aMap: %p %#v \n", &aMap, aMap) // aMap: 0xc420078038 map[string]string{"name":"Golang"}

new 和 make

New和make都是golang用來初始設定變數的記憶體的關鍵字函數。new返回的是記憶體的地址,make則返回時類型的樣本。比如new一個數組,則返回一個數組的記憶體位址,make一個數組,則返回一個初始化的數組。

經過上面的case,相信再面對map類型的指標,也一樣可以通過new和make配合完成初始化工作。

    var mapPot *map[string]int    fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) //  mapPot: 0xc42000c050 (*map[string]int)(nil)     // 初始化map指標的地址    mapPot = new(map[string]int)    fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) // mapPot: 0xc42000c050 &map[string]int(nil)      //(*mapPot)["age"] = 21 // 報錯    // 初始化map指標指向的map    (*mapPot) = make(map[string]int)    (*mapPot)["age"] = 21    fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) // mapPot: 0xc42000c050 &map[string]int{"age":21}

上面的代碼聲明了一個指標變數mapPot,這個指標變數的類型是一個map。通過new給指標變數開闢了一個記憶體,並賦予其記憶體位址。
Map是參考型別,其零值為nil,因此使用make初始化map,然後變數就能使用*給指標變數mapPot賦值了。

Make除了可以初始化map,還可以初始化slice和channel,以及基於這三種類型的自訂類型。

    type User map[string]string    var user User    fmt.Printf("user: %p %#v \n", &user, user)  // user: 0xc42000c060 main.User(nil)    user = make(User)    user["name"] = "Golang"    fmt.Printf("user: %p %#v \n", &user, user) // user: 0xc42007a050 main.User{"name":"Golang"}    var userPot *User    fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 (*main.User)(nil)    userPot = new(User)    fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User(nil)    (*userPot) = make(User)    fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User{}    (*userPot)["name"] = "Golang"    fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User{"name":"Golang"}

可見,再複雜的類型,只要弄清楚了指標與nil的關係,配合new和make就能輕鬆的給golang的資料類型進行初始化。

方法中的指標

Go可以讓我自訂類型,而類型又可以建立方法。與OOP類似,方法接受一個類型的執行個體對象,稱之為接受者,接受者既可以是類型的執行個體變數,也可以是類型的執行個體指標變數。

func main(){    person := Person{"vanyar", 21}    fmt.Printf("person<%s:%d>\n", person.name, person.age)    person.sayHi()    person.ModifyAge(210)    person.sayHi()}type Person struct {    name string    age int}func (p Person) sayHi() {    fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)}func (p Person) ModifyAge(age int) {    fmt.Printf("ModifyAge")    p.age = age}

輸出如下:

person<vanyar:21>SayHi -- This is vanyar, my age is 21ModifyAgeSayHi -- This is vanyar, my age is 21

儘管 ModifyAge 方法修改了其age欄位,可是方法裡的p是person變數的一個副本,修改的只是副本的值。下一次調用sayHi方法的時候,還是person的副本,因此修改方法並不會生效。

也許有人會想,方法會拷貝執行個體變數,如果執行個體變數是一個指標,不就輕而易舉修改了嗎?

    personPot := &Person{"noldor", 27}    fmt.Printf("personPot<%s:%d>\n", personPot.name, personPot.age)    personPot.sayHi()    personPot.ModifyAge(270)    personPot.sayHi()

輸出如下:

personPot<noldor:27>SayHi -- This is noldor, my age is 27ModifyAgeSayHi -- This is noldor, my age is 27

可見並沒有效果,實際上,go的確實copy裡personPot,只不過會根據接受者是值還是指標類型做一個自動轉換,然後再拷貝轉換後的對象。即personPot.ModifyAge(270)實際上等同於
(*personPot).ModifyAge(270),也就是拷貝的是(*personPot)。與personPot本身是值還是指標沒有關係。

真正能修改對象的方式是設定指標類型的接受者。指標類型的接受者,如果執行個體對象是值,那麼go會轉換成指標,然後再拷貝,如果本身就是指標對象,那麼就直接拷貝指標執行個體。因為指標都指向一處值,自然就能修改對象了。代碼如下:

func (p *Person) ChangeAge(age int)  {    fmt.Printf("ModifyAge")    p.age = age}

Go會根據Person的樣本類型,轉換成指標類型再拷貝,即 person.ChangeAge會變成 (&person).ChangeAge。

總結

Golang是一門簡潔的語言,提供了指標用於操作資料記憶體,並通過引用來修改變數。

只聲明未賦值的變數,golang都會自動為其初始化為零值,基礎資料類型的零值比較簡單,參考型別和指標的零值都為nil,nil類型不能直接賦值,因此需要通過new開闢一個記憶體,或者通過make初始化資料類型,或者兩者配合,然後才能賦值。

指標也是一種類型,不同於一般類似,指標的值是地址,這個地址指向其他的記憶體,通過指標可以讀取其所指向的地址所儲存的值。

函數方法的接受者,也可以是指標變數。無論普通接受者還是指標接受者都會被拷貝傳入方法中,不同在於拷貝的指標,其指向的地方都一樣,只是其自身的地址不一樣。

文字輸出的記憶體位址因編譯環境和運行有所不同。參考代碼gist

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.