range是Golang提供的一種疊代周遊手段,可操作的類型有數組、切片、Map、channel等,實際使用頻率非常高。
探索range的實作機制是很有意思的事情,這可能會改變你使用range的習慣。
2. 熱身
按照慣例,我們看幾個有意思的題目,用于檢測對range的了解程度。
2.1 題目一:切片周遊
下面函數通過周遊切片,列印切片的下标和元素值,請問性能上有沒有可優化的空間?
func RangeSlice(slice []int) {
for index, value := range slice {
_, _ = index, value
}
}
程式解釋:
函數中使用for-range對切片進行周遊,擷取切片的下标和元素素值,這裡忽略函數的實際意義。
參考答案:
周遊過程中每次疊代會對index和value進行指派,如果資料量大或者value類型為string時,對value的指派操作可能是多餘的,可以在for-range中忽略value值,使用slice[index]引用value值。
2.2 題目二:Map周遊
下面函數通過周遊Map,列印Map的key和value,請問性能上有沒有可優化的空間?
func RangeMap(myMap map[int]string) {
for key, _ := range myMap {
_, _ = key, myMap[key]
}
}
程式解釋:
函數中使用for-range對map進行周遊,擷取map的key值,并根據key值擷取擷取value值,這裡忽略函數的實際意義。
參考答案:
函數中for-range語句中隻擷取key值,然後根據key值擷取value值,雖然看似減少了一次指派,但通過key值查找value值的性能消耗可能高于指派消耗。能否優化取決于map所存儲資料結構特征、結合實際情況進行。
2.3 題目三:動态周遊
請問如下程式是否能正常結束?
func main() {
v := []int{1, 2, 3}
for i:= range v {
v = append(v, i)
}
}
程式解釋:
main()函數中定義一個切片v,通過range周遊v,周遊過程中不斷向v中添加新的元素。
參考答案:
能夠正常結束。循環内改變切片的長度,不影響循環次數,循環次數在循環開始前就已經确定了。
3. 實作原理
對于for-range語句的實作,可以從編譯器源碼中找到答案。
編譯器源碼
gofrontend/go/statements.cc/For_range_statement::do_lower()
方法中有如下注釋。
// Arrange to do a loop appropriate for the type. We will produce
// for INIT ; COND ; POST {
// ITER_INIT
// INDEX = INDEX_TEMP
// VALUE = VALUE_TEMP // If there is a value
// original statements
// }
可見range實際上是一個C風格的循環結構。range支援數組、數組指針、切片、map和channel類型,對于不同類型有些細節上的差異。
3.1 range for slice
下面的注釋解釋了周遊slice的過程:
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
周遊slice前會先擷取slice的長度len_temp作為循環次數,循環體中,每次循環會先擷取元素值,如果for-range中接收index和value的話,則會對index和value進行一次指派。
由于循環開始前循環次數就已經确定了,是以循環過程中新添加的元素是沒辦法周遊到的。
另外,數組與數組指針的周遊過程與slice基本一緻,不再贅述。
3.2 range for map
下面的注釋解釋了周遊map的過程:
// The loop we generate:
// var hiter map_iteration_struct
// for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
// index_temp = *hiter.key
// value_temp = *hiter.val
// index = index_temp
// value = value_temp
// original body
// }
周遊map時沒有指定循環次數,循環體與周遊slice類似。由于map底層實作與slice不同,map底層使用hash表實作,插入資料位置是随機的,是以周遊過程中新插入的資料不能保證周遊到。
3.3 range for channel
周遊channel是最特殊的,這是由channel的實作機制決定的:
// The loop we generate:
// for {
// index_temp, ok_temp = <-range
// if !ok_temp {
// break
// }
// index = index_temp
// original body
// }
channel周遊是依次從channel中讀取資料,讀取前是不知道裡面有多少個元素的。如果channel中沒有元素,則會阻塞等待,如果channel已被關閉,則會解除阻塞并退出循環。
注:
- 上述注釋中index_temp實際上描述是有誤的,應該為value_temp,因為index對于channel是沒有意義的。
- 使用for-range周遊channel時隻能擷取一個傳回值。
4. 程式設計Tips
- 周遊過程中可以視情況放棄接收index或value,可以一定程度上提升性能
- 周遊channel時,如果channel中沒有資料,可能會阻塞
- 盡量避免周遊過程中修改原資料
5. 總結
- for-range的實作實際上是C風格的for循環
- 使用index,value接收range傳回值會發生一次資料拷貝