傳值還是傳參考
呼叫函式時, 傳入的引數的 傳值 還是 傳參考, 幾乎是每種編程語言都會關注的問題. 最近在使用 golang 的時候, 由于 傳值 和 傳參考 的方式沒有弄清楚, 導致了 BUG.
經過深入的嘗試, 終于弄明白了 golang 的 傳值 的 傳參考, 嘗試程序記錄如下, 供大家參考!
golang 本質上都是傳值方式呼叫
嚴格來說, golang 中都是傳值呼叫, 下面通過例子一一說明
普通型別的引數
這里的普通型別, 指的是 int, string 等原始的資料型別, 這些型別作為函式引數時, 都是 傳值 呼叫. 這個基本沒什么疑問.
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
struct 指標, map, slice 型別的引數
對于這種型別的引數, 表面上是 傳參考 呼叫, 我也被這個表面現象迷惑過…
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 沒有生效之外
既然都是傳值呼叫, 為什么 f3 內修改了 *Person, 會導致外面的 t3 改變
改造下 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 了.
既然都是傳值呼叫, 為什么 f4 內修改了 []string, 會導致外面的 t4 改變
golang 中的 slice 也是指標型別, 所以和上面 *Person 的原因一樣
為什么 f4 內對 []string append 之后, 沒有導致外面的 t4 改變
代碼是最好的解釋, 先觀察 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 外的變數
既然都是傳值呼叫, 為什么 f5 內修改了 map, 會導致外面的 t5 改變
map 型別也是指標型別, 所以原因和上面的 *Person 一樣
為什么 f5 內增加了 map 中元素, 會導致外面的 t5 改變, 沒有像 t4 那樣, 只變修改的部分, 不變新增的部分
同樣, 看代碼
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 函式傳參的本質.
- 對于普通型別(int, string 等等), 就是 傳值 呼叫, 函式內對引數的修改, 不影響外面的變數
- 對于 struct 指標, slice 和 map 型別, 函式內對引數的修改之所以能影響外面, 是因為引數和外面的變數指向了同一塊資料的地址
- 對于 struct 指標, slice 和 map 型別, 函式的引數和外面的變數的地址是不一樣的, 所以本質上還是 傳值 呼叫
- slice 的 append 操作會改變 slice 指標的地址, 這個非常重要!!! 我曾經寫了一個基于 slice 的排序演算法在這個上面吃了大虧, 調研很久才發現原因…
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/51183.html
標籤:Go
上一篇:golang自定義error
下一篇:Go搭建一個Web服務器
