天天看點

golang for range原理(轉載)2. 熱身3. 實作原理4. 程式設計Tips5. 總結

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傳回值會發生一次資料拷貝