這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
曾經聽說過一句話,編程的本質就是指標和遞迴。那會剛開始編碼,只是這兩個的概念有個感性粗淺的認識。最早接觸指標,莫過於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