背景:
?? 開發(fā)階段發(fā)現(xiàn)某數(shù)據(jù)結(jié)構(gòu)切片中,字段值都相等揭措,經(jīng)排查后發(fā)現(xiàn)某次對結(jié)構(gòu)體實例切片進(jìn)行for循環(huán)遍歷胆数,以修改字段數(shù)值時,出現(xiàn)問題黎泣。
示例代碼:
# forTest 模擬遍歷循環(huán)結(jié)構(gòu)體過程
func forTest() {
// 定義函數(shù)內(nèi)部結(jié)構(gòu)體
type forStruct struct {
ID int
Name string
}
// 生成數(shù)據(jù)切片恕刘,并進(jìn)行賦值操作
list := make([]forStruct, 0)
list = append(list, forStruct{1, "測試1"}, forStruct{2, "測試2"})
// 存放遍歷后修改的結(jié)果,其中result是結(jié)構(gòu)體指針類型
result := make([]*forStruct, 0)
// 開始遍歷數(shù)據(jù)抒倚,并打印出相關(guān)結(jié)果
for i, v := range list {
// 修改前數(shù)據(jù)
fmt.Println(fmt.Sprintf("第%d行數(shù)據(jù):%v", i, v))
// 將ID值+10
v.ID = v.ID + 10
// 修改后數(shù)據(jù)
fmt.Println(fmt.Sprintf("第%d行修改數(shù)據(jù):%v", i, v))
// 臨時變量m的值
fmt.Println(fmt.Sprintf("v臨時變量的地址:%v", &v))
// 切片原變量的值
fmt.Println(fmt.Sprintf("切片原變量的地址:%v", &list[i]))
fmt.Println("------------------------------------------")
result = append(result, &v)
}
// 遍歷返回的結(jié)果
for i, r := range result {
fmt.Println(fmt.Sprintf("第%d行數(shù)據(jù)為%v", i, r))
}
}
輸出結(jié)果:
第0行數(shù)據(jù):{1 測試1}
第0行修改數(shù)據(jù):{11 測試1}
v臨時變量的地址:&{11 測試1}
切片原變量的地址:&{1 測試1}
------------------------------------------
第1行數(shù)據(jù):{2 測試2}
第1行修改數(shù)據(jù):{12 測試2}
v臨時變量的地址:&{12 測試2}
切片原變量的地址:&{2 測試2}
------------------------------------------
第0行數(shù)據(jù)為&{12 測試2}
第1行數(shù)據(jù)為&{12 測試2}
??可以看到輸出的最后兩行都是相同的褐着,這是什么原因呢?
原因
?? golang中的參數(shù)傳遞都是值傳遞!
??在for循環(huán)遍歷slice或者map時托呕,生成了臨時變量v献起,變量v在遍歷過程中,是重復(fù)利用的镣陕。也就是說谴餐,無論是第幾次遍歷,始終都是將slice或者map中的數(shù)據(jù)復(fù)制到了v中呆抑,并通過操作v來實現(xiàn)內(nèi)部變量岂嗓。
??在上述示例代碼中,處理邏輯是將遍歷結(jié)構(gòu)體數(shù)據(jù)切片并修改字段后鹊碍,存入到結(jié)構(gòu)體指針切片中厌殉,在此過程中食绿,變量v的內(nèi)存地址始終不變,存入的均為臨時變量的地址公罕。等循環(huán)結(jié)束后器紧,由于golang使用的是引用計數(shù)算法,因此v并不會被垃圾回收楼眷,存入的所有結(jié)構(gòu)體數(shù)據(jù)均為最后一次遍歷的數(shù)據(jù)铲汪。
解決方式:
- 原切片改用指針類型:數(shù)據(jù)相同是由于存入的是臨時變量地址,因此只需要將原本的切片存入類型從結(jié)構(gòu)體統(tǒng)一成結(jié)構(gòu)體指針即可罐柳。在操作v的時候掌腰,實際會操作切片底層的數(shù)據(jù),操作完成后再次存儲指針即可张吉,或者直接使用原本的切片齿梁。
- 結(jié)果切片改為結(jié)構(gòu)體: 與存入指針相對,也可以存為結(jié)構(gòu)體肮蛹。在遍歷過程中勺择,會生成一個新的結(jié)構(gòu)體對象,并將這個結(jié)構(gòu)實例存入結(jié)果中伦忠。不過建議大數(shù)據(jù)結(jié)構(gòu)體還是使用指針省核,以減少內(nèi)存復(fù)制成本。
- 直接操作原切片: 上述兩種方式都是原現(xiàn)的切片類型統(tǒng)一缓苛,如果仍需要結(jié)構(gòu)體轉(zhuǎn)換為指針類型芳撒,則可不操作臨時變量v,直接操作原變量未桥,并將原變量賦值到結(jié)果集笔刹。