可能被忽略的Golang細節——range
range關鍵字是Go語言中一個非常有用的迭代array,slice,map, string, channel中元素的內建關鍵字。
range的使用
range的使用非常簡單,對於遍歷array,*array,string它返回兩個值分別是資料的索引和值,遍歷map時返回的兩個值分別是key和value,遍歷channel時,則只有一個返回資料。各種型別的返回值參考下表:
range expression | 1st Value | 2nd Value(optional) | notes |
---|---|---|---|
array[n]E,*[n]E
|
indexint
|
valueE[i]
|
|
slice[]E
|
indexint
|
valueE[i]
|
|
stringabcd
|
indexint
|
runeint
|
對於string,range迭代的是Unicode而不是位元組,所以返回的值是rune |
mapmap[k]v
|
keyk
|
valuev
|
|
channel | element | none |
使用方式非常簡單,下面直接貼一段gobyexample中的示例程式碼作為參考,執行結果請點選連結跳轉到gobyexample。
package main import "fmt" func main() { nums := []int{2, 3, 4} sum := 0 for _, num := range nums { sum += num } fmt.Println("sum:", sum) for i, num := range nums { if num == 3 { fmt.Println("index:", i) } } kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) } for k := range kvs { fmt.Println("key:", k) } for i, c := range "go" { fmt.Println(i, c) } }
range的詳細說明也請跳轉官方文件的for_statement章節自行閱讀。本文主要介紹一些平時可(yi)能(bu)被(xiao)忽(xin)略(jiu)的(diao)細(keng)節(li)。
range表示式構建
先來看看下面這段程式碼的輸出是什麼?這段程式碼會無限迴圈的執行下去嗎?
func modifySlice() { v := []int{1, 2, 3, 4} for i := range v { v = append(v, i) fmt.Printf("Modify Slice: value:%v\n", v) } }
答案肯定不會無限迴圈的,這麼低階的錯誤,Go的開發者肯定是不會範的。那結果會是什麼呢?執行這段程式碼會列印下面的內容:
Modify Slice: value:[1 2 3 4 0] Modify Slice: value:[1 2 3 4 0 1] Modify Slice: value:[1 2 3 4 0 1 2] Modify Slice: value:[1 2 3 4 0 1 2 3]
可以看到每次迴圈在map中插入新的內容後,map的長度確實發生了變化,但是迴圈只執行了三次,正好是執行range前map的長度。說明range在執行之初就構建好了range表示式的內容了,雖然後面map的內容增加了,但是並不會影響初始構建的結果。官方文件對於range表示式的構建描述是這樣的:
The range expression x is evaluated once before beginning the loop, with one exception: if at most one iteration variable is present and len(x) is constant, the range expression is not evaluated.
就是說range表示式在開始執行迴圈之前就已經構建了,但是有一個例外就是:如果最多隻有一個迭代變數,且len(x)表達的是值是一個常量的時候range表示式不會構建。
那什麼時候len(x)是一個常量呢?按照通常的理解,len(string), len(slice), len(array)…的返回應該都是常量啊?事實上不是這樣的,這其實是由資料結構的特性決定的。因為相比較於其他語言,對於這一類容器資料結構,在Go語言中不僅有長度屬性可以通過內建函式len()獲取,還有一個可以通過內建函式cap()獲取的容量屬性,尤其是slice,map這一類可變長的資料結構。所以對於常量的定義,Go官方文件Length and capacity有詳細的說明。
如果到這裡,你以為你已經理解了range的構建,並且一眼就能看出一個for-range迴圈的迭代方式和執行情況。前面可能就已經有一個大坑為你準備好了。這時候官方文件裡面下面這樣一段話可能就被你忽略了,讓我先貼出來:
The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.
用中國話解釋下,首先對於map的迭代來說,map中內容的迭代順序沒有指定,也不保證,簡單的說就是迭代map的時候將以隨機的順序返回裡面的內容。這個好理解,也就是說如果你想按順序迭代一些東西,map就不要指望了,換其他資料結構吧。
然後就進入高潮部分了,如果一個map的key在還沒有被迭代到的時候就被delete掉了,那麼它所對應的迭代值就不會被產生了。然後對於在迭代過程中新增加的key,則它可能會被迭代到也可能不會。如何選擇會根據key增加的時機以及從上一次到下一次的迭代而不同。你可能會想,What?你TM在逗我麼,說好的提前構建的呢…但是很不幸,事實就是這樣,將前面的示例程式碼改成使用map我執行了幾次結果都不一樣。所以這種坑還是繞過為好。至於為什麼會這樣,容我有空再研究研究,下次重開一篇文章介紹。
func modifyMap() { data := map[string]string{"a": "A", "b": "B", "c": "C"} for k, v := range data { data[v] = k fmt.Println("modify Mapping", data) } }
結果1,迭代了6次
modify Mapping map[a:A b:B c:C A:a] modify Mapping map[b:B c:C A:a B:b a:A] modify Mapping map[c:C A:a B:b C:c a:A b:B] modify Mapping map[a:A b:B c:C A:a B:b C:c] modify Mapping map[A:a B:b C:c a:A b:B c:C] modify Mapping map[a:A b:B c:C A:a B:b C:c]
結果2,迭代了4次:
modify Mapping map[a:A b:B c:C A:a] modify Mapping map[b:B c:C A:a B:b a:A] modify Mapping map[a:A b:B c:C A:a B:b C:c] modify Mapping map[B:b C:c a:A b:B c:C A:a]
range string
使用range迭代字串時,需要主要的是range迭代的是Unicode而不是位元組。返回的兩個值,第一個是被迭代的字元的UTF-8編碼的第一個位元組在字串中的索引,第二個值的為對應的字元且型別為rune(實際就是表示unicode值的整形資料)。
總結下來就是使用range迭代string時,需要注意下面兩點:
迭代的是Unicode不是位元組,第一個返回值是UTF-8編碼第一個位元組的索引,所以索引值可能並不是連續的。
第二個返回值的型別為rune,不是string型別的,如果要使用該值需要格式化。
看下面程式碼:
//string func rangeString() { datas := "aAbB" for k, d := range datas { fmt.Printf("k_addr:%p, k_value:%v\nd_addr:%p, d_value:%v\n----\n", &k, k, &d, d) } }
這段程式碼的輸出如下,這裡使用的string是隻佔用一個位元組的字元的字串,所以返回的第一個索引是連續的,可以看到第二個值都是整型數字。
k_addr:0xc420014148, k_value:0 d_addr:0xc420014150, d_value:97 k_addr:0xc420014148, k_value:1 d_addr:0xc420014150, d_value:65 k_addr:0xc420014148, k_value:2 d_addr:0xc420014150, d_value:98 k_addr:0xc420014148, k_value:3 d_addr:0xc420014150, d_value:66
range可以對string做更多的事情
前面說到range是對Unicode進行迭代來迭代字串的,所以range還能夠在迭代過程中發現字串中非Unicode的字元,並使用U+FFFD字元替換改無效字元。
看下面程式碼的執行:
func rangeStringMore() { for pos, char := range "中\x80文" { // \x80 is an illegal UTF-8 encoding fmt.Printf("character %#U starts at byte position %d\n", char, pos) } }
上面這段程式碼使用range迭代字串"中\x80文",字串中\x80是一個無效的的Unicode字元,所以range在迭代時會使用U+FFFD將其替換。另外UTF-8使用變長方式編碼,第一個漢字中佔用了3個位元組,所以遍歷第二個字元的時候,其索引已經是3了,但是其只佔一個位元組,所以第三個字元文的索引4.
character U+4E2D '中' starts at byte position 0 character U+FFFD '�' starts at byte position 3 character U+6587 '文' starts at byte position 4
range表示式是指標
前面說過range可以迭代的資料型別包括array,slice,map,string和channel。在這些資料型別裡面只有array型別能以指標的形式出現在range表示式中。
具體參考下面的程式碼:
func rangePointer() { //compile error: cannot range over datas (type *string) //d := "aAbBcCdD" d := [5]int{1, 2, 3, 4, 5} //range successfully //d := []int{1, 2, 3, 4, 5} //compile error: cannot range over datas (type *[]int) //d := make(map[string]int) //compile error: cannot range over datas (type *map[string]int) datas := &d for k, d := range datas { fmt.Printf("k_addr:%p, k_value:%v\nd_addr:%p, d_value:%v\n----\n", &k, k, &d, d) }
參考連結:
range
For statements
Length and capacity
Rune literals
Go by Example: Range
作者:xingwangc2014
來源:CSDN
原文:https://blog.csdn.net/xingwangc2014/article/details/79777724
版權宣告:本文為博主原創文章,轉載請附上博文連結!