Dig101: Go之for-range排坑指南

很久沒寫了,打算今年作個Dig101系列,挖一挖技術背後的故事。html

也能夠移步微信版閱讀git

Dig101: dig more, simplified more and know moregithub

golang經常使用的遍歷方式,有兩種: for 和 for-range。 而for-range使用中有些坑常會遇到,今天咱們一塊兒來捋一捋。golang

0x01 遍歷取不到全部元素指針?

以下代碼想從數組遍歷獲取一個指針元素切片集合c#

arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
    res = append(res, &v)
}
//expect: 1 2
fmt.Println(*res[0],*res[1]) 
//but output: 2 2 
複製代碼

答案是【取不到】 一樣代碼對切片[]int{1, 2}map[int]int{1:1, 2:2}遍歷也不符合預期。 問題出在哪裏?數組

經過查看go編譯源碼能夠了解到, for-range實際上是語法糖,內部調用仍是for循環,初始化會拷貝帶遍歷的列表(如array,slice,map),而後每次遍歷的v都是對同一個元素的遍歷賦值。 也就是說若是直接對v取地址,最終只會拿到一個地址,而對應的值就是最後遍歷的那個元素所附給v的值。對應僞代碼以下:微信

// len_temp := len(range)
// range_temp := range
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = range_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
複製代碼

那麼怎麼改? 有兩種數據結構

  • 使用局部變量拷貝v
for _, v := range arr {
    //局部變量v替換了v,也可用別的局部變量名
    v := v 
    res = append(res, &v)
}
複製代碼
  • 直接索引獲取原來的元素
//這種其實退化爲for循環的簡寫
for k := range arr {
    res = append(res, &arr[k])
}
複製代碼

理順了這個問題後邊的坑基本都好發現了,來迅速過一遍app

0x02 遍歷會中止麼?

v := []int{1, 2, 3}
for i := range v {
    v = append(v, i)
}
複製代碼

答案是【會】,由於遍歷前對v作了拷貝,因此期間對原來v的修改不會反映到遍歷中frontend

0x03 對大數組這樣遍歷有啥問題?

//假設值都爲1,這裏只賦值3個
var arr = [102400]int{1, 1, 1} 
for i, n := range arr {
    //just ignore i and n for simplify the example
    _ = i 
    _ = n 
}
複製代碼

答案是【有問題】!遍歷前的拷貝對內存是極大浪費啊 怎麼優化?有兩種

  • 對數組取地址遍歷 for i, n := range &arr
  • 對數組作切片引用 for i, n := range arr[:]

反思題:對大量元素的slice和map遍歷爲啥不會有內存浪費問題? (提示,底層數據結構是否被拷貝)

0x04 對大數組這樣重置效率高麼?

//假設值都爲1,這裏只賦值3個
var arr = [102400]int{1, 1, 1} 
for i, _ := range &arr {
    arr[i] = 0
}
複製代碼

答案是【高】,這個要理解得知道go對這種重置元素值爲默認值的遍歷是有優化的, 詳見go源碼:memclrrange

// Lower n into runtime·memclr if possible, for
// fast zeroing of slices and arrays (issue 5373).
// Look for instances of
//
// for i := range a {
// a[i] = zero
// }
//
// in which the evaluation of a is side-effect-free.
複製代碼

0x05 對map遍歷時刪除元素能遍歷到麼?

var m = map[int]int{1: 1, 2: 2, 3: 3}
//only del key once, and not del the current iteration key
var o sync.Once 
for i := range m {
    o.Do(func() {
        for _, key := range []int{1, 2, 3} {
            if key != i {
                fmt.Printf("when iteration key %d, del key %d\n", i, key)
                delete(m, key)
                break
            }
        }
    })
    fmt.Printf("%d%d ", i, m[i])
}
複製代碼

答案是【不會】 map內部實現是一個鏈式hash表,爲保證每次無序,初始化時會隨機一個遍歷開始的位置, 這樣,若是刪除的元素開始沒被遍歷到(上邊once.Do函數內保證第一次執行時刪除未遍歷的一個元素),那就後邊就不會出現。

0x06 對map遍歷時新增元素能遍歷到麼?

var m = map[int]int{1:1, 2:2, 3:3}
for i, _ := range m {
    m[4] = 4
    fmt.Printf("%d%d ", i, m[i])
}
複製代碼

答案是【可能會】,輸出中可能會有44。緣由同上一個, 能夠用如下代碼驗證

var createElemDuringIterMap = func() {
    var m = map[int]int{1: 1, 2: 2, 3: 3}
    for i := range m {
        m[4] = 4
        fmt.Printf("%d%d ", i, m[i])
    }
}
for i := 0; i < 50; i++ {
    //some line will not show 44, some line will
    createElemDuringIterMap()
    fmt.Println()
}
複製代碼

0x07 這樣遍歷中起goroutine能夠麼?

var m = []int{1, 2, 3}
for i := range m {
    go func() {
        fmt.Print(i)
    }()
}
//block main 1ms to wait goroutine finished
time.Sleep(time.Millisecond) 
複製代碼

答案是【不能夠】。預期輸出0,1,2的某個組合,如012,210.. 結果是222. 一樣是拷貝的問題 怎麼解決

  • 以參數方式傳入
for i := range m {
    go func(i int) {
        fmt.Print(i)
    }(i)
}
複製代碼
  • 使用局部變量拷貝
for i := range m {
    i := i
    go func() {
        fmt.Print(i)
    }()
}
複製代碼

發現沒,一個簡單的for-range,仔細剖析下來也是有很多有趣的地方。 但願剖析後能讓你更進一步的瞭解。 若有問題歡迎關注留言交流。

菜鳥Miao

文章首發地址:blog.newbmiao.com/2020/01/03/…

參考

相關文章
相關標籤/搜索