在程序中,變量是一種佔位符,用於指代內存中某一段的值。每個變量都對應一個內存地址,在該地址上存儲着該變量的內容。html
咱們常說的「指針」也是一種特殊的變量,特殊之處在於,指針變量存儲的是內存中的某處地址,也就是說能夠根據指針變量的內容到對應的變量單元,由此咱們將這種存儲內存地址的變量稱做「指針」。golang
在go中,符號&用於取地址,例如:c#
package main import "fmt" func main() { var a int = 10 fmt.Printf("a的存儲地址爲: %p\n", &a) //a的存儲地址爲: 0xc0000121c0 }
輸出爲:數組
a的存儲地址爲: 0xc0000121c0
符號*有兩種做用:函數
(1)聲明變量時放在類型前,代表變量爲指針變量,如學習
var i *int //聲明i爲指向整型的指針變量
(2)放在指針變量前,用於取出該指針所指向的值,如測試
func main() { var a int = 8 var ptr *int = &a fmt.Printf("ptr指向的地址爲: %v\n", ptr) fmt.Printf("ptr指向的地址中存儲的內容爲: %v\n", *ptr) //此處*用於從地址中取值 }
輸出爲:lua
ptr指向的地址爲: 0xc0000121c0 ptr指向的地址所存儲的內容爲: 8
文章開頭提到,指針自身也是一種特殊的變量,意味着指針也會有本身的存儲地址,故若一個指針變量指向另外一個指針的地址,咱們便稱其爲指向指針的指針變量,採用以下方式聲明:指針
var pptr **int
聲明指針的指針須要兩個*號,相似的,咱們還能夠用三個*號來聲明指向指向指針的指針的指針變量....固然,這在實際使用中絕少用到。事實上,指針的指針的實際使用頻率也不會很高,這裏說起此概念,主要是爲了理解指針也是一種變量。code
給出以下一段指針的指針使用示例
func main() { var a int = 1 var ptr *int = &a var ptrToPtr **int = &ptr //注意:此處不能寫&&a,由於&a的結果沒有存入指定的變量,沒法對其取地址 fmt.Printf("a = %d\n", a) fmt.Printf("指針ptr指向地址 %p\n", ptr) fmt.Printf("指針的指針ptrToPtr指向地址 %p\n", ptrToPtr) fmt.Printf("指針 *ptr = %d\n", *ptr) fmt.Printf("指針的指針變量 **ptrToPtr = %d\n", **ptrToPtr) }
輸出結果爲:
a = 1 指針ptr指向地址 0xc0000a0158 指針的指針ptrToPtr指向地址 0xc0000ca018 指針 *ptr = 1 指針的指針變量 **ptrToPtr = 1
數組指針是一個指針,指向某一個數組。
採用以下語句聲明數組指針:
var arrPtr *[size]Type
示例:
func main() { var arrPtr *[3]int //聲明數組指針arrPtr var arr = [3]int{1, 2, 3} arrPtr = &arr // 將數組 arr的地址賦值給arrPtr fmt.Printf("arr的內存地址arrPtr=%p\n", arrPtr) }
運行可得輸出:
arr的內存地址arrPtr=0xc0000104e0
前文提到過,*可用於從地址取值,則可用*從arrPtr中訪問arr的元素,須要注意的是,若寫成以下形式:
*arrPtr[0]
則程序會報錯:
invalid indirect of arrPtr[0] (type int)
緣由是,[]運算符優先級高於*運算符,則上述運算會計算arrPtr[0]中取出整數1,再對整數1進行*尋址,天然會報錯。解決方法是用括號()調整運算的優先級:
(*arrPtr)[0]
可是在實際使用中,Golang容許咱們省略*號,直接寫做:
arrPtr[0]
經過以下代碼進行一下測試:
fmt.Printf("arr的首元素(*arrPtr)[0] = %d\n", (*arrPtr)[0]) fmt.Printf("arr的首元素arrPtr[0] = %d\n", arrPtr[0])
輸出可得:
arr的首元素(*arrPtr)[0] = 1 arr的首元素arrPtr[0] = 1
但需注意,容許省略*號的狀況十分侷限,經過數組指針訪問數組內元素是其中一種狀況。
還有另外一種狀況容許省略*號,即訪問結構體指針的字段或者方法時,例如,有以下結構體指針:
s:=&struct{}
在訪問struct下的字段或者方法時,本應採用表達式:
(*s).variable (*s).method()
但這裏golang提供了省略*號的語法糖,上述表達式可簡略爲:
s.variable s.method()
指針數組是一個數組,其中每一個元素都是指針,或者說都是地址值。
採用以下語句聲明指針數組:
var ptrArr [size]*Type
示例:
func main() { a, b := 1, 2 var ptrArr []*int = []*int{&a, &b} //構建指針數組 for i := 0; i < 2; i++ { fmt.Printf("ptrArr[%d] = %p\n", i, ptrArr[i]) } for i := 0; i < 2; i++ { fmt.Printf("ptrArr[%d]指向的值*ptrArr[i] = %d\n", i, *ptrArr[i]) //用*取出第i個指針指向的值,此處的符號*不可省略 } }
輸出可得:
ptrArr[0] = 0xc0000121c0 ptrArr[1] = 0xc0000121c8 ptrArr[0]指向的值*ptrArr[i] = 1 ptrArr[1]指向的值*ptrArr[i] = 2
值傳遞與引用傳遞的概念由來已久,每當咱們接觸一門新的語言,必定要關注的即是其函數調用過程當中傳遞的參數究竟是值仍是引用?話題展開以前,首先明確值傳遞與引用傳遞的概念:
值傳遞(pass by value):
在調用函數時將實際參數複製一份傳遞到函數中,這樣在函數中若是對參數進行修改,將不會影響到實際參數。
引用傳遞(pass by reference):
在調用函數時將實際參數的地址直接傳遞到函數中,那麼在函數中對參數所進行的修改,將影響到實際參數。
具體到golang語言上,官方文檔已經給出答案:
In a function call, the function value and arguments are evaluated in the user order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.
可見,golang的函數調用採用的是值傳遞。下面,用幾個代碼實例驗證一下。
func main() { var a int = 10 fmt.Printf("執行函數以前, a=%v\n", a) fmt.Printf("原始地址爲: %p\n", &a) tryToChange(a) fmt.Printf("執行函數以後, a=%v\n", a) } func tryToChange(b int) { fmt.Printf("函數接收到的參數地址爲: %p\n", &b) b = 20 }
獲得輸出:
執行函數以前, a=10 原始地址爲: 0xc0000a0158 函數接收到的參數地址爲: 0xc0000a0188 執行函數以後, a=10
觀察上面的輸出,能夠發現:(1)傳遞給函數的值並未修改爲功(2)實參a和形參b的地址並不同。說明函數在接收到實參a後,爲形參b在局部變量的棧中開闢了新的空間,並拷貝a的值,所以函數內部對形參b的任何操做都不會對實參a產生影響,故而a的值沒有被修改。
func main() { var a int = 10 var ptr_a *int = &a fmt.Printf("執行函數以前, a=%v\n", a) fmt.Printf("原始指針的存儲地址爲: %p\n", &ptr_a) fmt.Printf("原始指針指向的地址爲: %p\n", ptr_a) tryToChange(ptr) fmt.Printf("執行函數以後, a=%v\n", a) } func tryToChange(ptr_b *int) { fmt.Printf("函數接收到的指針存儲地址爲: %p\n", &ptr_b) fmt.Printf("函數接收到的指針指向的地址爲: %p\n", ptr_b) *ptr_b = 20 }
運行輸出爲:
執行函數以前, a=10 原始指針的存儲地址爲: 0xc000006028 原始指針指向的地址爲: 0xc0000121c0 函數接收到的指針存儲地址爲: 0xc000006038 函數接收到的指針指向的地址爲: 0xc0000121c0 執行函數以後, a=20
觀察發現,(1)整數a的值修改爲功(2)實參ptr_a和形參ptr_b的存儲地址不一致,可是做爲指針變量,二者都指向整數a的地址。這再一次印證了golang採用值傳遞的特性,與第一個實例相同,實參ptr_a在傳遞給函數後,程序會爲形參ptr_b從新開闢一塊地址,並將ptr_a的值(也就是整數a的地址)拷貝給ptr_b,從而程序對形參ptr_b的取值賦值操做最終都是對整數a的操做,所以在本例中a的值能夠被成功修改。
應該說,在有了實例一和二以後,已經能夠完整說明golang值傳遞特性,可是在學習golang的過程當中,每每會看到這樣的錯誤描述:「go語言中,map、slice和chan採用引用傳遞,其餘類型採用值傳遞」,但這與go的文檔並不一致,不難猜想,形成這樣誤會的緣由是,當咱們向函數內傳入一個map或者slice,函數內的修改能夠反映到實參上。實際狀況如何,咱們用以下代碼測試一下:
func main() { a := []int{0, 0, 0} //建立一個切片 fmt.Printf("原始切片地址爲: %p\n", &a) fmt.Printf("原始切片首個元素的地址爲: %p\n", &a[0]) fmt.Printf("修改前切片首個元素爲: %v\n", a[0]) tryToChange(a) fmt.Printf("修改後切片首個元素爲: %v\n", a[0]) } func tryToChange(b []int) { fmt.Printf("接收到的切片地址爲: %p\n", &b) fmt.Printf("接收到的切片首個元素的地址爲: %p\n", &b[0]) b[0] = 888 }
獲得輸出:
原始切片地址爲: 0xc000004660 原始切片首個元素的地址爲: 0xc0000104e0 修改前切片首個元素爲: 0 接收到的切片地址爲: 0xc0000046a0 接收到的切片首個元素的地址爲: 0xc0000104e0 修改後切片首個元素爲: 888
從輸出結果能夠看到,(1)原始切片a的地址和傳遞到函數內的切片b地址並不一致,這反映了Golang值傳遞的特性,(2)雖然切片自身的實參形參地址不一樣,可是函數內外訪問的切片首位元素的地址是同樣的,這是切片元素修改可以成功的緣由。這兩個看似衝突的現象同時出現,是由於切片在golang中能夠被理解爲一種特殊的引用類型,Slice類型的定義以下:
type Slice struct { point Point // 內存地址 len int cap int }
slice自身並不存儲切片數據,也不直接指向底層數據,而是經過其point字段指向底層的數據(其實是數組)。所以,在切片參數傳遞到函數中時,代碼爲形參b開闢新的空間,並拷貝了實參a的內容向b賦值,這致使了實參形參的地址不一致;另外一方面,實參a與形參b的內容一致,因此其point字段都指向的是同一處內存空間,由此致使了在函數內修改切片元素的值可以成功。
在golang中,可以用make()函數建立的變量均可以理解作引用類型,即slice、map和chan類型。但須要注意引用類型並不意味着引用傳遞,在golang中只存在值傳遞。
https://learnku.com/articles/44096
https://learnku.com/articles/44096
https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html