聊聊Golang中的range關鍵字

聊聊Golang中的range關鍵字

[TOC]git

首先讓咱們來看兩段代碼github

  1. 下面的程序是否能夠正常結束?
func main() {
	v := []int{1, 2, 3}
	for i := range v {
		v = append(v, i)
	}
}
  1. 下面的程序分別輸出什麼?
func IndexArray() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7, 8}

	for i := range a {
		a[3] = 100
		if i == 3 {
			fmt.Println("IndexArray", i, a[i])
		}
	}
}

func IndexValueArray() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7, 8}

	for i, v := range a {
		a[3] = 100
		if i == 3 {
			fmt.Println("IndexValueArray", i, v)
		}
	}
}

func IndexValueArrayPtr() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7, 8}

	for i, v := range &a {
		a[3] = 100
		if i == 3 {
			fmt.Println("IndexValueArrayPtr", i, v)
		}
	}
}

func main() {
	IndexArray()
	IndexValueArray()
	IndexValueArrayPtr()
}

第一步 讓咱們閱讀一下官方文檔

range 變量

咱們應該都知道,對於 range 左邊的循環變量能夠用如下方式來賦值:golang

等號直接賦值 (=) 短變量申明賦值 (:=) 固然也能夠什麼都不寫來徹底忽略迭代遍歷到的值。express

若是使用短變量申明(:=),Go 會在每次循環的迭代中重用申明的變量(只在循環內的做用域裏有效) 表達式左邊必須是可尋址的或者map索引表達式,若是表達式是channel,最多容許一個變量,其餘狀況下容許兩個變量。c#

range表達式

range 右邊表達式的結果,能夠是如下這些數據類型:數組

  • array
  • pointer to an array
  • slice
  • string
  • map
  • channel permitting receive operations 好比:chan int or chan<- int

range 表達式會在開始循環前被 evaluated 一次。但有一個例外狀況:app

若是對一個數組或者指向數組的指針作 range 而且最多隻有一個變量(只用到了數組索引):此時只有表達式長度 被 evaluated。frontend

這裏的 evaluated 究竟是什麼意思?很不幸文檔裏沒有找到相關的說明。固然我猜其實就是徹底的執行表達式直到其不能再被拆解。不管如何,最重要的是 range 表達式 在整個迭代開始前會被徹底的執行一次。那麼你會怎麼讓一個表達式只執行一次?把執行結果放在一個變量裏! range 表達式的處理會不會也是這麼作的?函數

有趣的是規範文檔裏提到了一些對 maps (沒有提到 slices) 作添加或刪除操做的狀況。oop

若是 map 中的元素在尚未被遍歷到時就被移除了,後續的迭代中這個元素就不會再出現。而若是 map 中的元素是在迭代過程當中被添加的,那麼在後續的迭代這個元素可能出現也可能被跳過。

第二步 研究一下range copy

若是咱們假設在循環開始以前會先把 range 表達式複製給一個變量,那咱們須要關注什麼?答案是表達式結果的數據類型,讓咱們更近一步的看看 range 支持的數據類型。

在咱們開始前,先記住:在 Go 裏,不管咱們對什麼賦值,都會被複制。若是賦值了一個指針,那咱們就複製了一個指針副本。若是賦值了一個結構體,那咱們就複製了一個結構體副本。往函數裏傳參也是一樣的狀況。好了,開始吧:

Range expression                          1st value          2nd value

array or slice  a  [n]E, *[n]E, or []E    index    i  int    a[i]       E
string          s  string type            index    i  int    see below  rune
map             m  map[K]V                key      k  K      m[k]       V
channel         c  chan E, <-chan E       element  e  E

然而這些對於真正解決咱們的問題彷佛並無太大做用! 好,咱們先看一段代碼:

func main() {
	// 複製整個數組
	var a [10]int
	acopy := a
	a[0] = 10
	fmt.Println("a", a)
	fmt.Println("acopy", acopy)
	// 只複製了 slice 的結構體,並無複製成員指針指向的數組
	s := make([]int, 10)
	s[0] = 10
	scopy := s
	fmt.Println("s", s)
	fmt.Println("scopy", scopy)
	// 只複製了 map 的指針
	m := make(map[string]int)
	mcopy := m
	m["0"] = 10
	fmt.Println("m", m)
	fmt.Println("mcopy", mcopy)
}

你們猜下這個程序的輸出結果是什麼,不賣關子了,直接上答案。

a [10 0 0 0 0 0 0 0 0 0]
acopy [0 0 0 0 0 0 0 0 0 0]
s [10 0 0 0 0 0 0 0 0 0]
scopy [10 0 0 0 0 0 0 0 0 0]
m map[0:10]
mcopy map[0:10]

因此,若是要在 range 循環開始前把一個數組表達式賦值給一個變量(保證表達式只 evaluate 一次),就會複製整個數組。

第三步 真相在源碼

看下gcc源碼發現,咱們關心的和 range 有關的部分出如今 statements.cc,下面是一段註釋:

// 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 支持的類型作專門的 「語法糖還原」。好比,

數組:

// The loop we generate:
  //   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
  //   }

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
  //   }
  //
  // Using for_temp means that we don't need to check bounds when
  // fetching range_temp[index_temp].

他們的共同點是:

  • 全部類型的 range 本質上都是 C 風格的循環
  • 遍歷到的值會被賦值給一個臨時變量

總結:

  • 循環變量在每一次迭代中都被賦值並會複用。
  • 能夠在迭代過程當中移除一個 map 裏的元素或者向 map 裏添加元素。添加的元素並不必定會在後續迭代中被遍歷到。

如今讓咱們回到開篇的例子。 1.答案是程序能夠正常結束運行。它其實能夠粗略的翻譯成相似下面的這段:

for_temp := v
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
        v = append(v, index)
}

2.先看輸出結果

IndexArray 3 100
IndexValueArray 3 4
IndexValueArrayPtr 3 100

咱們知道切片其實是一個結構體的語法糖,這個結構體有着一個指向數組的指針成員。在循環開始前對這個結構體生成副本而後賦值給 for_temp,後面的循環其實是在對 for_temp 進行迭代。任何對於原始變量 v 自己(而非對其背後指向的數組)的更改都和生成的副本 for_temp 沒有關係。但其背後指向的數組仍是以指針的形式共享給 v 和 for_temp,因此 v[i] = 1 這樣的語句仍然能夠工做。 和上面的例子相似,在循環開始前數組被賦值給了一個臨時變量,在對數組作 range 循環時臨時變量裏存放的是整個數組的副本,對原數組的操做不會反映在副本上。而在對數組指針作 range 循環時臨時變量存放的是指針的副本,操做的也是同一塊內存空間。

附:更深刻理解

下面讓咱們再來一個例子,看看你是否真正理解了。

type Foo struct {
    bar string
}
func main() {
    list := []Foo{
        {"A"},
        {"B"},
        {"C"},
    }
    list2 := make([]*Foo, len(list))
    for i, value := range list {
        list2[i] = &value
    }
    fmt.Println(list[0], list[1], list[2])
    fmt.Println(list2[0], list2[1], list2[2])
}

在這個例子中,咱們幹了下面的一些事情:

  • 定義了一個叫作Foo的結構,裏面有一個叫bar的field。隨後,咱們建立了一個基於Foo結構體的slice,名字叫list
  • 咱們還建立了一個基於Foo結構體指針類型的slice,叫作list2
  • 在一個for循環中,咱們試圖遍歷list中的每個元素,獲取其指針地址,並賦值到list2中index與之對應的位置。
  • 最後,分別輸出list與list2中的每一個元素

從代碼來看,理所固然,咱們指望獲得的結果應該是這樣:

{A} {B} {C}
&{A} &{B} &{C}

可是結果卻出乎意料,程序的輸出是這樣的:

{A} {B} {C}
&{C} &{C} &{C}

在Go的for…range循環中,Go始終使用值拷貝的方式代替被遍歷的元素自己,簡單來講,就是for…range中那個value,是一個值拷貝,而不是元素自己。這樣一來,當咱們指望用&獲取元素的地址時,實際上只是取到了value這個臨時變量的地址,而非list中真正被遍歷到的某個元素的地址。而在整個for…range循環中,value這個臨時變量會被重複使用,因此,在上面的例子中,list2被填充了三個相同的地址,其實都是value的地址。而在最後一次循環中,value被賦值爲{c}。所以,list2輸出的時候顯示出了三個&{c}。

一樣的,下面的寫法,跟for…range的例子一模一樣:

var value Foo
for var i := 0; i < len(list); i++ {
    value = list[i]
    list2[i] = &value
}

那麼,怎樣纔是正確的寫法呢?咱們應該用index來訪問for…range中真實的元素,並獲取其指針地址:

for i, _ := range list {
    list2[i] = &list[i]
}

這樣,輸出list2中的元素,就能獲得咱們想要的結果(&{A} &{B} &{C})了。

相關文章
相關標籤/搜索