Go 学习笔记(29)— range 作用于字符串、数组、切片、字典、通道
1. 使用說明
range 應用于不同數據類型時,類似迭代器操作,返回 (索引, 值) 或 (鍵, 值)。 下表是對應的結構:
| type | 1st value | 2nd value |
|---|---|---|
| string | index | s[index] |
| array/slice | index | s[index] |
| map | key | m[key] |
| channel | element |
2. 使用示例
如果想忽略不想要的值時,可以使用 _ 這個特殊變量。
package mainfunc main() {s := "abc"for i := range s { // 忽略 2nd value,支持 string/array/slice/map。println(s[i])}println("***************")for _, c := range s { // 忽略 index。println(c)}println("***************")for range s { // 忽略全部返回值,僅迭代。println("*")}println("***************")m := map[string]int{"a": 1, "b": 2}for k, v := range m { // 返回 (key, value)。println(k, v)}}
當 map 類型變量作為 range 表達式時,我們得到的 map 變量的副本與原變量指向同一個 map,如果我們在循環的過程中,對 map 進行了修改,那么這樣修改的結果是否會影響后續迭代呢?這個結果和我們遍歷 map 一樣,具有隨機性。
3. 常見的坑
3.1 循環變量的重用
func main() {var m = []int{1, 2, 3, 4, 5} for i, v := range m {go func() {time.Sleep(time.Second * 3)fmt.Println(i, v)}()}time.Sleep(time.Second * 10)
}
輸出結果:
4 5
4 5
4 5
4 5
4 5
預期結果
0 1
1 2
2 3
3 4
4 5
這是因為我們最初的“預期”本身就是錯的。這里,初學者很可能會被 for range 語句中的短聲明變量形式“迷惑”,簡單地認為每次迭代都會重新聲明兩個新的變量 i 和 v。但事實上,這些循環變量在 for range 語句中僅會被聲明一次,且在每次迭代中都會被重用。
上面代碼等價于下面
func main() {var m = []int{1, 2, 3, 4, 5} {i, v := 0, 0for i, v = range m {go func() {time.Sleep(time.Second * 3)fmt.Println(i, v)}()}}time.Sleep(time.Second * 10)
}
通過等價轉換后的代碼,我們可以清晰地看到循環變量 i 和 v 在每次迭代時的重用。而 Goroutine 執行的閉包函數引用了它的外層包裹函數中的變量 i、v,這樣,變量 i、v 在主 Goroutine 和新啟動的 Goroutine 之間實現了共享,而 i, v 值在整個循環過程中是重用的,僅有一份。在 for range 循環結束后,i = 4, v = 5,因此各個 Goroutine 在等待 3 秒后進行輸出的時候,輸出的是 i,v 的最終值。
修改代碼
func main() {var m = []int{1, 2, 3, 4, 5}for i, v := range m {go func(i, v int) {time.Sleep(time.Second * 3)fmt.Println(i, v)}(i, v)}time.Sleep(time.Second * 10)
}
3.2 參與循環的是 range 表達式的副本
數組是內置(build-in)類型,是一組同類型數據的集合,它是值類型,通過從 0 開始的下標索引訪問元素值。在初始化后長度是固定的,無法修改其長度。
當作為方法的參數傳入時將復制一份數組而不是引用同一指針。數組的長度也是其類型的一部分,通過內置函數 len(array) 獲取其長度。
注意,和 C 中的數組相比,又是有一些不同的
Go中的數組是值類型,換句話說,如果你將一個數組賦值給另外一個數組,那么,實際上就是將整個數組拷貝一份;- 如果
Go中的數組作為函數的參數,那么實際傳遞的參數是一份數組的拷貝,而不是數組的指針。這個和C要區分開。因此,在Go中如果將數組作為函數的參數傳遞的話,那效率就肯定沒有傳遞指針高了; array的長度也是Type的一部分,這樣就說明[10]int和[20]int是不一樣的;
內置類型切片(“動態數組"),與數組相比切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。
切片中有兩個概念:一是 len 長度,二是 cap 容量,長度是指已經被賦過值的最大下標+1,可通過內置函數 len() 獲得。容量是指切片目前可容納的最多元素個數,可通過內置函數 cap() 獲得。
切片是引用類型,因此在當傳遞切片時將引用同一指針,修改值將會影響其他的對象。
需要強調的是, range 會復制對象, range 返回的是每個元素的副本,而不是直接返回對該元素的引用。
package mainimport "fmt"func main() {a := [3]int{0, 1, 2}for i, v := range a { // index、value 都是從復制品中取出。if i == 0 { // 在修改前,我們先修改原數組。a[1], a[2] = 999, 999fmt.Println(a) // 確認修改有效,輸出 [0, 999, 999]。}a[i] = v + 100 // 使用復制品中取出的 value 修改原數組。}fmt.Println(a) // 輸出 [100, 101, 102]。}
建議改用引用類型,其底層數據不會被復制。
package mainfunc main() {s := []int{1, 2, 3, 4, 5}for i, v := range s { // 復制 struct slice { pointer, len, cap }。if i == 0 {s = s[:3] // 對 slice 的修改,不會影響 range。s[2] = 100 // 對底層數據的修改。}println(i, v)}}
輸出:
0 1
1 2
2 100
3 4
4 5
其它示例
func main() {var a = [5]int{1, 2, 3, 4, 5}var r [5]intfmt.Println("original a =", a)for i, v := range a {if i == 0 {a[1] = 12a[2] = 13}r[i] = v}fmt.Println("after for range loop, r =", r)fmt.Println("after for range loop, a =", a)
}
期望輸出結果:
original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]
實際輸出結果:
original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]
修改循環迭代行代碼為下面即可達到預期效果:
for i, v := range a[:] {# orfor i, v := range &a
總結
以上是生活随笔為你收集整理的Go 学习笔记(29)— range 作用于字符串、数组、切片、字典、通道的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 装电信宽带现在多少钱?
- 下一篇: 电线多少钱一斤啊?