Golang 之 我被 for-range 循環進去了

看個例子

在咱們平時的代碼場景中,經常須要改變切片中某個元素的值,先來看一下常見的代碼實現方式:git

package main
 import "fmt"  func test1() {  slice1 := []int{1, 2, 3, 4}  for _, val := range slice1 {  val++  }   fmt.Println(slice1) }  func test2() {  slice2 := []int{1, 2, 3, 4}  for k, _ := range slice2 {  slice2[k]++  }   fmt.Println(slice2) }  func test3() {  slice3 := []int{1, 2, 3, 4}  for i := 0; i < len(slice3); i++ {  slice3[i]++  }   fmt.Println(slice3) }  func main() {  test1()  test2()  test3() } 複製代碼

很是簡單,test1() 中的修改並未對原數據產生影響,而 test2() 和 test3() 中的修改真正改變了原數據。咱們看一下打印的結果:github

[1 2 3 4]
[2 3 4 5] [2 3 4 5] 複製代碼

最終輸出也是跟咱們預想的一致。golang

主要緣由是由於:web

  • val是slice1內元素的副本,對val的改變不會致使slice1內元素的改變
  • 而在test2() 和 test3() 中是直接對切片進行索引修改,改變了底層的數組

爲何會出現這種狀況呢?咱們去了解一下for - range 原理c#

for - range 原理

for range的實現數組

// 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 // } 複製代碼

其中針對 slice 的編譯方式以下:markdown

// 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 // } 複製代碼

具體代碼細節可查看 https://github.com/golang/gofrontend/blob/e387439bfd24d5e142874b8e68e7039f74c744d7/go/statements.cc#L5384 源代碼閉包

從上面的源碼咱們能夠看到,針對slice,for range 作了一下事情:app

  • 對要遍歷的 Slice 作一個拷貝
  • 獲取長度大小
  • 使用常規for循環進行遍歷,返回值的拷貝,並把值存放到全局變量 index 和 value中

也就是說,對於 for k, val := range(slice) 環過程當中,val 在循環內始終都是同一個全局變量frontend

結合上面的結論,咱們接下來再看一道題:

package main
 import "fmt"  func test4() {  s := []int{0, 1, 2, 3}  m := make(map[int]*int)   for index, value := range s {  m[index] = &value  }   printMap(m) }  func printtMap(m map[int]*int) {  for key, value := range m {  fmt.Printf("map[%v]=%v\n", key, *value)  } }  func main() {  test4() } 複製代碼

打印輸出:

map[2]=3
map[3]=3 map[0]=3 map[1]=3 複製代碼

test4() 中直接存的是地址,由於在整個for index, value := range s 循環中,value都是同一個全局變量,地址是同樣的,每次遍歷該地址上的值都會被新的值覆蓋掉,因此在遍歷結束後,該地址存的值是切片上的最後一個元素3

若是我把 test4()方法 換成下面這種形式呢

func test4() {  s := []int{0, 1, 2, 3}  m := make(map[int]*int)   for index, value := range s {  valueCopy := value  m[index] = &valueCopy  }   printtMap(m) } 複製代碼

上面主要改變是:每次進入循環體,都聲明一個新變量valueCopy,並把value賦值給它,最後把新變量valueCopy的地址存到 m 中

打印輸出:

map[0]=0
map[1]=1 map[2]=2 map[3]=3 複製代碼

緣由是由於每次循環都聲明新變量,對應的地址也是不同的。

咱們再看一個閉包,其原理同樣

package main
import (  "fmt"  "time" ) func main() {  str := []string{"I","am","Echo 大叔"}  for _, v := range str{  // 每一個goroutine的v的地址相同,都是爲外部v的地址  go func() {  // 這裏的v是引用外部變量v的地址  fmt.Println(v)  }()  }   time.Sleep(3 * time.Second) } 複製代碼

實際上上面的代碼會輸出:

Echo 大叔
Echo 大叔 Echo 大叔 複製代碼

緣由見註釋

上面閉包要想實現輸出不一樣的值,可利用函數的值傳遞性質:

package main
import (  "fmt"  "time" ) func main() {  str := []string{"I","am","Echo 大叔"}  for _, v := range str{  // 把外部的v值拷貝給函數內部的v  go func(v string) {  fmt.Println(v)  }(v)  }   time.Sleep(3 * time.Second) } 複製代碼

打印輸出(打印順序不必定同樣):

I
am Echo 大叔 複製代碼

對於slice

由 for range 的原理咱們能夠知道 for i, v := range x,進入循環前會對x的長度進行快照,決定一共循環len(x)那麼屢次。後面x的變更不會改變循環次數。經過i,以及最新的x,把x[i]賦予給v。

package main
 import (  "fmt" )  func main() {  x := []int{1, 3, 5, 7, 9, 11, 13, 15}  fmt.Println("start with ", x)   for i, v := range x {  fmt.Println("The current value is", v)  x = append(x[:i], x[i+1:]...)  fmt.Println("And after it is removed, we get", x)  } } 複製代碼

上面代碼,咱們在遍歷切片的時候,每遍歷一次就把該元素從切片中刪掉

打印輸出:

The current value is 1
And after it is removed, we get [3 5 7 9 11 13 15] The current value is 5 And after it is removed, we get [3 7 9 11 13 15] The current value is 9 And after it is removed, we get [3 7 11 13 15] The current value is 13 And after it is removed, we get [3 7 11 15] The current value is 15 panic: runtime error: slice bounds out of range [5:4]  goroutine 1 [running]: main.main()  /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/Interview/for_range.go:13 +0x398 exit status 2 複製代碼

從輸出咱們能夠看出,for range 的循環次數取決於循環前會對遍歷目標的長度進行快照,並不會隨着遍歷目標長度的修改而改變。因此最終會出現切片溢出的panic

做業

最後,留一道題給你們

package main
 import (  "fmt" )  type Guest struct {  id int  name string  surname string  friends []int }  func (self Guest) removeFriend(id int) {  for i, other := range self.friends {  if other == id {  self.friends = append(self.friends[:i], self.friends[i+1:]...)  break  }  } }  func main() {  test := Guest{0, "Echo", "大叔", []int{1,2, 3, 4, 5}}  fmt.Println(test)  test.removeFriend(4)  fmt.Println(test) } 複製代碼

最終會打印輸出:

{0 Echo 大叔 [1 2 3 4 5]}
{0 Echo 大叔 [1 2 3 5 5]} 複製代碼

你們知道其中緣由嗎?歡迎評論交流~

關注公衆號 「大叔說碼」,獲取更多幹貨,下期見~

相關文章
相關標籤/搜索