在介紹之前先說(shuō)明一下彼哼,標(biāo)題中帶有【beego】標(biāo)簽的歇终,是beego框架使用中遇到的坑谢谦。如果沒(méi)有伐割,那就是golang本身的坑构灸。當(dāng)然戴而,此坑并非人家代碼有問(wèn)題收叶,有幾個(gè)地方反而是出于性能等各方面的考量有意而為之勒葱。但這些地方卻是一門(mén)語(yǔ)言或框架的初學(xué)者大概率會(huì)遇到的困惑写穴。
1惰拱、slice/map遍歷時(shí)的修改問(wèn)題
在go中,slice/map的迭代循環(huán)的常用方式為:
slice:
for index,value := range _slice {
}
map:
for key,value := range _map {
}
這其中value就是遍歷時(shí)的當(dāng)前值啊送。
按照我們之前在java中的遍歷習(xí)慣偿短,當(dāng)遍歷到第幾個(gè)時(shí)欣孤,拿到的就是指向第幾個(gè)對(duì)象的引用,因此對(duì)對(duì)象的所有修改行為本質(zhì)上修改的都是原值翔冀。但是在這里并不是导街。看一個(gè)例子:
//tag1
var structs = []testModel{
testModel{
A: "一",
B: "一一",
C: 1,
},
testModel{
A: "二",
B: "二二",
C: 2,
},
testModel{
A: "三",
B: "三三",
C: 3,
},
}
//tag2
for _, val := range structs {
val.A = "四"
val.B = "四四"
val.C = 4
}
//tag3
for _, val := range structs {
fmt.Print(val.A)
fmt.Print(val.B)
fmt.Println(val.C)
}
代碼邏輯很簡(jiǎn)單纤子。
- 我們?cè)趖ag1處定義了一個(gè)數(shù)組搬瑰,里面包含三個(gè)testModel實(shí)例。每個(gè)testModel實(shí)例有三個(gè)字段控硼,并且他們的字段值都是互不相同的泽论。
- 按照事先的想法,是要在tag2處將所有testModel實(shí)例的字段值修改為相同的卡乾。
- tag3檢查一下修改結(jié)果翼悴。
結(jié)果console輸出如下;
=== RUN TestLogic
一一一1
二二二2
三三三3
--- PASS: TestLogic (0.00s)
PASS
打印的仍然是舊值。那這是為什么呢幔妨?
原因是:在range遍歷時(shí)鹦赎,map中的key&value,slice中的index&value误堡,都是新的臨時(shí)變量古话,這個(gè)臨時(shí)變量被每一次迭代所共用,臨時(shí)變量的值也是由被遍歷元素復(fù)制而來(lái)锁施。因此在該變量上修改是無(wú)效的陪踩。
那如何才能有效呢? 很簡(jiǎn)單悉抵,對(duì)上例的tag2部分做如下修改:
for key, _ := range structs {
structs[key].A = "四"
structs[key].B = "四"
structs[key].C = 4
}
同理Slice修改時(shí)也需要用slice[index].column = newValue 的方式進(jìn)行肩狂。
2、map/slice遍歷時(shí)多協(xié)程問(wèn)題(multi-goroutines)
在map遍歷時(shí)姥饰,我們有可能會(huì)在map中使用go routines進(jìn)行一些操作傻谁,例如下面這個(gè)例子:
var values = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var block = make(chan int, 2)
//wrong
for i, val := range values {
go func() {
fmt.Println(1000 + val)
if i == len(values)-1 {
block <- 1
}
}()
}
for i := 0; i < 2; i++ {
<-block
}
}
我們想用map中迭代的當(dāng)前值,在協(xié)程中做一番大事業(yè)列粪,潛意識(shí)的寫(xiě)法可能就是按照wrong中的寫(xiě)法那樣栅螟,直接把value拿過(guò)來(lái)就用。但是卻得到這樣的結(jié)果:
=== RUN TestLogic
1009
1009
1009
1009
1009
1009
1009
1009
1009
--- PASS: TestLogic (0.00s)
PASS
也就是說(shuō)篱竭,在goroutine中的val力图,值竟然都是map遍歷的最后一個(gè)!導(dǎo)致這一現(xiàn)象的原因有兩個(gè):
- for range下的迭代變量val的值是共用的掺逼,這一點(diǎn)在《slice/map遍歷時(shí)修改問(wèn)題 》中有提到
- main函數(shù)所在的goroutine和后續(xù)啟動(dòng)的goroutines存在競(jìng)爭(zhēng)關(guān)系
為了證實(shí)這一點(diǎn)吃媒,修改代碼為如下:
for i, val := range values {
fmt.Println(&val)
go func() {
fmt.Println(1000 + val)
if i == len(values)-1 {
block <- 1
}
}()
}
加了一行代碼,打印val的內(nèi)存地址,結(jié)果如下:
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
1005
1009
1009
1009
1009
1009
val的地址在每次遍歷時(shí)是同一個(gè)赘那!證明第一點(diǎn)刑桑;goroutines在不同的遍歷中存在變化,例如1005募舟,證明第二點(diǎn)祠斧;當(dāng)然也可用go run -trace ***.go 命令來(lái)查看協(xié)程的變化,就不多贅述拱礁。
那如何修改呢琢锋?只需要使用 函數(shù)參數(shù)復(fù)制 做一次數(shù)據(jù)復(fù)制即可,而不是閉包:
for i, val := range values {
go func(val int) {
fmt.Println(2000 + val)
if i == len(values)-1 {
block <- 1
}
}(val)
}
關(guān)于map遍歷時(shí)多協(xié)程并發(fā)問(wèn)題也可參考:https://github.com/golang/go/wiki/CommonMistakes
3呢灶、數(shù)組與值拷貝
首先把結(jié)論放在這吴超,然后再展開(kāi)討論:
go語(yǔ)言數(shù)組的一切傳遞都是值拷貝,包括但不限于以下三個(gè)方面:
- 1鸯乃、數(shù)組之間的直接賦值鲸阻。
- 2、數(shù)組作為函數(shù)參數(shù)缨睡。
- 3鸟悴、數(shù)組內(nèi)嵌到struct中。
數(shù)組之間的直接賦值
看下面一段代碼:
a := []int{1,2,3}
//值復(fù)制
b := a
fmt.Printf("%p, %v\n", &a, a) //0xc0000bf660, [1 2 3]
fmt.Printf("%p, %v\n", &b, b) //0xc0000bf680, [1 2 3]
a = append(a, 4)
a[0] = 4
fmt.Println(len(b))//3
for e := range a {//0123
fmt.Print(e)
}
首先定義了一個(gè)數(shù)組a奖年,a中有3個(gè)元素细诸。然后通過(guò)一次賦值操作,將a賦值給了b拾并。
在java中揍堰,數(shù)組之間的賦值鹏浅,是引用的傳遞嗅义,在a中修改后再通過(guò)b進(jìn)行打印輸出,會(huì)得到修改后的值隐砸。但是剛才說(shuō)過(guò)之碗,go中數(shù)組之間的復(fù)制操作是值拷貝。因此打印b仍然還是修改前的樣子季希,會(huì)發(fā)現(xiàn)a和b在內(nèi)存中是完全不同的兩塊內(nèi)存區(qū)域褪那。
數(shù)組作為函數(shù)參數(shù)傳遞
因?yàn)間olang中函數(shù)參數(shù)的傳遞都是值拷貝,因此這一點(diǎn)放在數(shù)組上也不難理解式塌。
在如上代碼添加一句:test4(a)
a := []int{1,2,3}
//值復(fù)制
b := a
fmt.Printf("%p, %v\n", &a, a) //0xc0000bf660, [1 2 3]
fmt.Printf("%p, %v\n", &b, b) //0xc0000bf680, [1 2 3]
a = append(a, 4)
a[0] = 4
fmt.Println(len(b))//3
for e := range a {//0123
fmt.Print(e)
}
test4(a)
其中test4代碼如下:
func test4(param []int) {
fmt.Printf("%p, %v\n", ¶m, param) //01230xc0000bf700, [4 2 3 4]
}
會(huì)發(fā)現(xiàn)a & b & c各有各的內(nèi)存地址~~
數(shù)組內(nèi)嵌到struct中
a := []string{"1", "22"}
var c = struct {
S []string
}{
S: []string{"1", "22"},
}
//結(jié)構(gòu)是值拷貝博敬,內(nèi)部的數(shù)組也是值拷貝
b := c
//修改c中的數(shù)組元素值不影響b
c.S[0] = "2"
//修改b中的數(shù)組元素不影響c
b.S[0] = "3"
//地址不相同,說(shuō)明每一個(gè)變量在內(nèi)存中是獨(dú)立的內(nèi)存區(qū)域
fmt.Printf("%p,%v\n", &a, a) //0xc0000bd660,[1 22]
fmt.Printf("%p,%v\n", &b.S, b.S) //0xc0000bd720,[3 22]
fmt.Printf("%p,%v\n", &c.S, c.S) //0xc0000bd6a0,[3 22]