簡單聊聊Go?for?range中容易踩的坑
前言
為了讓大家更好的理解本期知識點,先介紹以下幾個知識點:線性結(jié)構(gòu)、非線性結(jié)構(gòu)、循環(huán)、迭代、遍歷、遞歸。
線性結(jié)構(gòu):數(shù)組、隊列
非線性結(jié)構(gòu):樹、圖
循環(huán)(loop):最基礎(chǔ)的概念,所有重復的行為都是循環(huán)
遞歸(recursion):在函數(shù)內(nèi)調(diào)用自身,將復雜情況逐步轉(zhuǎn)化成基本情況
(數(shù)學)迭代(iterate):在多次循環(huán)中逐步接近結(jié)果
(編程)迭代(iterate):按順序訪問線性結(jié)構(gòu)中的每一項
遍歷(traversal):按規(guī)則訪問非線性結(jié)構(gòu)中的每一項
下面會挑選幾個經(jīng)典的案例,一塊來探討下,看看如何避免掉坑,多積累積累采坑經(jīng)驗。
1. for+傳值
先來到開胃菜,熱熱身~
type?student?struct?{ ??name?string ??age??int } func?main()?{ ??m?:=?make(map[string]student) ??stus?:=?[]student{ ????{name:?"張三",?age:?18}, ????{name:?"李四",?age:?23}, ????{name:?"王五",?age:?26}, ??} ??for?_,?stu?:=?range?stus?{ ????m[stu.name]?=?stu ??} ??for?k,?v?:=?range?m?{ ????fmt.Println(k,?"=>",?v.name) ??} }
不出意料,輸出結(jié)果為:
李四 => 李四
王五 => 王五
張三 => 張三
這題比較簡單,就是簡單的傳值操作,大家應該都能答上來。下面加大難度,改為傳址操作
2. for+傳址
將案例一改為傳址操作
type?student?struct?{ ??name?string ??age??int } func?main()?{ ??m?:=?make(map[string]*student) ??stus?:=?[]student{ ????{name:?"張三",?age:?18}, ????{name:?"李四",?age:?23}, ????{name:?"王五",?age:?26}, ??} ??for?_,?stu?:=?range?stus?{ ????m[stu.name]?=?&stu ??} ??for?k,?v?:=?range?m?{ ????fmt.Println(k,?"=>",?v.name) ??} }
好好想想應該輸出什么結(jié)果呢?還是跟案例一是一樣的結(jié)果嗎?難道會有坑?
不出意料,還是出了意外,輸出結(jié)果為:
張三 => 王五
李四 => 王五
王五 => 王五
為什么呢?
- 首先,關(guān)鍵點在于Go的for循環(huán),對
循環(huán)變量stu
每次是循環(huán)并不是迭代(簡單的說,就是對循環(huán)變量stu
只會做一次聲明和內(nèi)存地址的分配,后面循環(huán)就是不斷更新值); - 所以,取址操作
&stu
,其實都是取的同一個變量的地址,只是值被循環(huán)更新為最后一個元素的值; - 最終,輸出的
v.name
,都是最后一個元素的name為王五
。
解決方案:
在for循環(huán)中,做同名變量覆蓋stu:=stu
(即重新聲明一個局部變量,做值拷貝,避免相互影響)
type?student?struct?{ ??name?string ??age??int } func?main()?{ ??m?:=?make(map[string]*student) ??stus?:=?[]student{ ????{name:?"張三",?age:?18}, ????{name:?"李四",?age:?23}, ????{name:?"王五",?age:?26}, ??} ??for?_,?stu?:=?range?stus?{ ????stu?:=?stu??//同名變量覆蓋 ????m[stu.name]?=?&stu ??} ??for?k,?v?:=?range?m?{ ????fmt.Println(k,?"=>",?v.name) ??} }
輸出結(jié)果:
張三 => 張三
李四 => 李四
王五 => 王五
3.for+閉包
在for循環(huán)里,做閉包操作,也是很容易掉坑的。看看下面輸出什么?
var?prints?[]func() for?_,?v?:=?range?[]int{1,?2,?3}?{ ??prints?=?append(prints,?func()?{?fmt.Println(v)?}) } for?_,?print?:=?range?prints?{ ??print() }
一眼看過去,感覺是輸出1 2 3,但實際會輸出 3 3 3
為什么呢?
- 首先,在分析了案例二后,我們知道了Go的for循環(huán)對
循環(huán)變量v
,其實每次是循環(huán)并不是迭代; - 然后,
閉包=函數(shù)+引用環(huán)境
,在同一個引用環(huán)境下,循環(huán)變量v的值會被不斷的覆蓋; - 所以最終,在打印時,輸出的v,都是最后一個值3。
解決方案:
和案例二解決方案一樣,是在for循環(huán)中,做同名變量覆蓋v:=v
var?prints?[]func() for?_,?v?:=?range?[]int{1,?2,?3}?{ ??v?:=?v?//同名變量覆蓋?? ??prints?=?append(prints,?func()?{?fmt.Println(v)?}) } for?_,?print?:=?range?prints?{ ??print() }
輸出結(jié)果:
1
2
3
4. for+goroutine
在for循環(huán)里,起goroutine協(xié)程,也是很迷惑很容易掉坑的。看看下面輸出什么?
var?wg?sync.WaitGroup strs?:=?[]string{"1",?"2",?"3",?"4",?"5"} for?_,?str?:=?range?strs?{ ??wg.Add(1) ??go?func()?{ ????defer?wg.Done() ????fmt.Println(str) ??}() } wg.Wait()
一眼看過去,感覺是會無序輸出1 2 3 4 5,但實際會輸出 5 5 5 5 5
為什么呢?
- 首先,要記得Go的for循環(huán)對
循環(huán)變量str
,其實每次是循環(huán)并不是迭代; - 然后,main協(xié)程會和新起的協(xié)程做相互博弈,看誰執(zhí)行更快,按這個案例執(zhí)行情況來看,main協(xié)程執(zhí)行速度明顯比新起的協(xié)程會更快,所以str被更新為最后一個元素值5(備注:并非絕對);
- 最終,在新起的協(xié)程中,使用str時值都為5,作為結(jié)果去輸出;
- 拓展:如果在新起協(xié)程前,sleep個5s,輸出結(jié)果又會截然不同,感興趣的同學可以自行實驗下,然后逐步深入地了解下GMP調(diào)度機制。
解決方案:
和前面兩個案例解決方案一樣,是在for循環(huán)中,做同名變量覆蓋str:=str
var?wg?sync.WaitGroup strs?:=?[]string{"1",?"2",?"3",?"4",?"5"} for?_,?str?:=?range?strs?{ ??str?:=?str?//同名變量覆蓋 ??wg.Add(1) ??go?func()?{ ????defer?wg.Done() ????fmt.Println(str) ??}() } wg.Wait()
輸出結(jié)果:
5
4
2
1
3
注意是1~5無序輸出
總結(jié)
for循環(huán)中做傳址、閉包、goroutine相關(guān)操作,千萬要注意,一不小心就會很容易掉坑。
使用好同名變量覆蓋v:=v
,這個解決大法,能很便捷的解決這一類問題。
到此這篇關(guān)于簡單聊聊Go for range中容易踩的坑的文章就介紹到這了,更多相關(guān)Go for range內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang因Channel未關(guān)閉導致內(nèi)存泄漏的解決方案詳解
這篇文章主要為大家詳細介紹了當Golang因Channel未關(guān)閉導致內(nèi)存泄漏時蓋如何解決,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2023-07-07golang中cache組件的使用及groupcache源碼解析
本篇主要解析groupcache源碼中的關(guān)鍵部分, lru的定義以及如何做到同一個key只加載一次。緩存填充以及加載抑制的實現(xiàn)方法,本文重點給大家介紹golang中cache組件的使用及groupcache源碼解析,感興趣的朋友一起看看吧2021-06-06Go返回int64類型字段超出javascript Number范圍的解決方法
這篇文章主要介紹了Go返回int64類型字段超出javascript Number范圍的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07GO?CountMinSketch計數(shù)器(布隆過濾器思想的近似計數(shù)器)
這篇文章主要介紹了GO?CountMinSketch計數(shù)器(布隆過濾器思想的近似計數(shù)器),文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-09-09Go基于GORM 獲取當前請求所執(zhí)行的 SQL 信息(思路詳解)
這篇文章主要介紹了Go基于GORM 獲取當前請求所執(zhí)行的 SQL 信息(思路詳解),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01