調用函數時, 傳入的參數的 傳值 仍是 傳引用, 幾乎是每種編程語言都會關注的問題. 最近在使用 golang 的時候, 因爲 傳值 和 傳引用 的方式沒有弄清楚, 致使了 BUG.golang
通過深刻的嘗試, 終於弄明白了 golang 的 傳值 的 傳引用, 嘗試過程記錄以下, 供你們參考!算法
嚴格來講, golang 中都是傳值調用, 下面經過例子一一說明編程
這裏的普通類型, 指的是 int, string 等原始的數據類型, 這些類型做爲函數參數時, 都是 傳值 調用. 這個基本沒什麼疑問.app
func param_ref_test01() { var t1 = 0 var t2 = "000" var f1 = func(p int) { p += 1 } var f2 = func(p string) { p += "-changed" } fmt.Printf(">>>調用前: t1 = %d t2 = %s\n", t1, t2) f1(t1) f2(t2) fmt.Printf("<<<調用後: t1 = %d t2 = %s\n", t1, t2) }
運行的結果:編程語言
>>>調用前: t1 = 0 t2 = 000 <<<調用後: t1 = 0 t2 = 000
對於這種類型的參數, 表面上是 傳引用 調用, 我也被這個表面現象迷惑過…函數
func param_ref_test02() { type Person struct { Name string Age int } var t3 = &Person{ Name: "test", Age: 10, } var t4 = []string{"a", "b", "c"} var t5 = make(map[string]int) t5["hello"] = 1 t5["world"] = 2 var f3 = func(p *Person) { p.Name = "test-change" p.Age = 20 } var f4 = func(p []string) { p[0] = "aa" p = append(p, "d") } var f5 = func(p map[string]int) { p["hello"] = 11 p["hello2"] = 22 } fmt.Printf(">>>調用前: t3 = %v t4 = %v t5 = %v\n", t3, t4, t5) f3(t3) f4(t4) f5(t5) fmt.Printf("<<<調用後: t3 = %v t4 = %v t5 = %v\n", t3, t4, t5) }
運行的結果:指針
>>>調用前: t3 = &{test 10} t4 = [a b c] t5 = map[hello:1 world:2] <<<調用後: t3 = &{test-change 20} t4 = [aa b c] t5 = map[hello:11 hello2:22 world:2]
從運行結果中, 能夠看出基本符合 傳引用 調用的特徵, 除了 t4 的 append 沒有生效以外code
改造下 f3, 將變量的地址打印出來排序
func param_ref_test03() { type Person struct { Name string Age int } var t3 = &Person{ Name: "test", Age: 10, } var f3 = func(p *Person) { p.Name = "test-change" p.Age = 20 fmt.Printf("參數p 指向的內存地址 = %p\n", p) fmt.Printf("參數p 內存地址 = %p\n", &p) } fmt.Printf("t3 指向的內存地址 = %p\n", t3) fmt.Printf("t3 的內存地址 = %p\n", &t3) f3(t3) }
運行的結果:內存
t3 指向的內存地址 = 0xc00000fe20 t3 的內存地址 = 0xc000010570 參數p 指向的內存地址 = 0xc00000fe20 參數p 內存地址 = 0xc000010578
從結果能夠看出, t3 和 p 都是指針類型, 可是它們的內存地址是不同的, 因此這是一個 傳值 調用. 可是, 它們指向的地址(0xc00000fe20)是同樣的, 因此經過 p 修改了指向的數據(*Person), t3 指向的數據也發生了變化.
只要 p 的指向地址變化, 就不會影響 t3 的變化了
var f3 = func(p *Person) { p = &Person{} // 這行會改變p指向的地址 p.Name = "test-change" p.Age = 20 } f3(t3)
能夠試試看, 只要加上上面代碼中有註釋的那行, 調用 f3 就不會改變 t3 了.
golang 中的 slice 也是指針類型, 因此和上面 *Person 的緣由同樣
代碼是最好的解釋, 先觀察 append 以後內存地址的變化, 咱們再分析
func param_ref_test04() { var s = []string{"a", "b", "c"} fmt.Printf("s 的內存地址 = %p\n", &s) fmt.Printf("s 指向的內存地址 = %p\n", s) s[0] = "aa" fmt.Printf("修改s[0] 以後, s 的內存地址 = %p\n", &s) fmt.Printf("修改s[0] 以後, s 指向的內存地址 = %p\n", s) s = append(s, "d") fmt.Printf("append以後, s 的內存地址 = %p\n", &s) fmt.Printf("append以後, s 指向的內存地址 = %p\n", s) }
運行的結果:
s 的內存地址 = 0xc00008fec0 s 指向的內存地址 = 0xc00016d530 修改s[0] 以後, s 的內存地址 = 0xc00008fec0 修改s[0] 以後, s 指向的內存地址 = 0xc00016d530 append以後, s 的內存地址 = 0xc00008fec0 append以後, s 指向的內存地址 = 0xc000096f00
首先, 不管是修改 slice 中的元素, 仍是添加 slice 的元素, 都不會改變 s 自己的地址(0xc00008fec0) 其次, 修改 slice 中的元素, 不會改變 s 指向的地址(0xc00016d530), 全部在 f4 中修改 slice 的元素, 也會改變函數 f4 外面的變量 最後, append 操做會修改 s 指向的地址, append 以後, s 和 函數 f4 外的變量已經不是指向同一地址了, 因此 append 的元素不會影響函數 f4 外的變量
map 類型也是指針類型, 因此緣由和上面的 *Person 同樣
一樣, 看代碼
func param_ref_test05() { var m = make(map[string]int) m["hello"] = 1 m["world"] = 2 fmt.Printf("m 的內存地址 = %p\n", &m) fmt.Printf("m 指向的內存地址 = %p\n", m) m["hello"] = 11 fmt.Printf("修改m 以後, m 的內存地址 = %p\n", &m) fmt.Printf("修改m 以後, m 指向的內存地址 = %p\n", m) m["hello2"] = 22 fmt.Printf("追加元素以後, m 的內存地址 = %p\n", &m) fmt.Printf("追加元素以後, m 指向的內存地址 = %p\n", m) }
運行的結果:
m 的內存地址 = 0xc000010598 m 指向的內存地址 = 0xc000151590 修改m 以後, m 的內存地址 = 0xc000010598 修改m 以後, m 指向的內存地址 = 0xc000151590 追加元素以後, m 的內存地址 = 0xc000010598 追加元素以後, m 指向的內存地址 = 0xc000151590
根據上面的分析經驗, 一目瞭然, 由於不管是修改仍是添加 map 中的元素, m 指向的地址(0xc000151590)都沒變, 因此函數 f5 中 map 參數修改元素, 添加元素以後, 都會影響函數 f5 以外的變量.
注意 這裏並非說 map 類型的參數就是 傳引用 調用, 它仍然是 傳值 調用, 參數 map 的地址和函數 f5 外的變量 t5 的地址是不同的 若是在函數 f5 中修改的 map 類型參數的指向地址, 就會像傳值調用那樣, 不影響函數 f5 外 t5 的值
func param_ref_test06() { var t5 = make(map[string]int) t5["hello"] = 1 t5["world"] = 2 var f5 = func(p map[string]int) { fmt.Printf("修改前 參數p 指向的內存地址 = %p\n", p) fmt.Printf("修改前 參數p 內存地址 = %p\n", &p) p = make(map[string]int) // 這行改變了 p 的指向, 使得 p 和 t5 再也不指向同一個地方 p["hello"] = 11 p["hello2"] = 22 fmt.Printf("修改後 參數p 指向的內存地址 = %p\n", p) fmt.Printf("修改後 參數p 內存地址 = %p\n", &p) } fmt.Printf("t5 指向的內存地址 = %p\n", t5) fmt.Printf("t5內存地址 = %p\n", &t5) fmt.Printf(">>>調用前: t5 = %v\n", t5) f5(t5) fmt.Printf("<<<調用後: t5 = %v\n", t5) }
運行的結果:
t5 指向的內存地址 = 0xc000151590 t5內存地址 = 0xc000010598 >>>調用前: t5 = map[hello:1 world:2] 修改前 參數p 指向的內存地址 = 0xc000151590 修改前 參數p 內存地址 = 0xc0000105a0 修改後 參數p 指向的內存地址 = 0xc000151650 修改後 參數p 內存地址 = 0xc0000105a0 <<<調用後: t5 = map[hello:1 world:2]
雖然是 map 類型參數, 可是調用先後, t5 的值沒有改變.
上面的嘗試不敢說有多全, 但基本能夠弄清 golang 函數傳參的本質.