<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Golang中的for-range趟坑

          共 19869字,需瀏覽 40分鐘

           ·

          2021-09-22 23:44

          近日,機(jī)緣巧合下入了一個 Golang 語言 for-range 的坑,出于敬畏深入學(xué)習(xí)過程中又一步步陷入了更深的坑,先上個代碼,大家看看應(yīng)該輸出什么吧?

          package main

          import (
           "fmt"
           "time"
          )

          func main() {
           slice := []int{1, 2, 3} 
           m := make(map[int]*int)
           var slice2 [3]int
           for index,value := range slice {
             slice = append(slice, value)
             go func(){
              fmt.Println("in goroutine: ",index,value)
             }()
             //time.Sleep(time.Second * 1)
             m[index] = &value
             if index == 0{
                slice[1] = 11
                slice[2] = 22
             }
             slice2[index] = value
           }
           fmt.Println("slice: ",slice)  
           for key,value := range m { 
            fmt.Println("in map: ",key,"->",*value)
           } 
           fmt.Println("slice2: ",slice2)
           time.Sleep(time.Second * 10)
          }

          考慮輸出結(jié)果之前吶,先思考以下幾個問題:

          1. 循環(huán)切片時不停的給被循環(huán)的那個切片追加元素會死循環(huán)嗎?
          2. 循環(huán)中改變被循環(huán)切片內(nèi)容,原切片內(nèi)容會同步發(fā)生變化嗎?
          3. 循環(huán)中通過協(xié)程進(jìn)行循環(huán)變量的操作會怎么樣吶?
          4. 把循環(huán)切片改成循環(huán)map會有什么變化嗎?
          5. 要想讓循環(huán)中的協(xié)程接受到希望的index和value需要怎么做吶?
          6. 要想讓循環(huán)中新賦值的切片slice2和原切片slice值保持一致要怎么做吶?

          公布下運(yùn)行結(jié)果吧:

          完全正確的同學(xué)可以直接跳到文末了~~,32個贊送給你呦!其實每次的結(jié)果也不完全一致,map 部分 key 的順序不一致但 value 的值能對的上也算正確哈~

          或多或少覺得結(jié)果有點詭異的同學(xué),咱們結(jié)合這段代碼和這幾個問題一起往下看看吧~~

          range 是 Golang 語言定義的一種語法糖迭代器,1.5版本 Golang 引入自舉編譯器后 range 相關(guān)源碼如下,根據(jù)類型不同進(jìn)行不同的處理,支持對切片和數(shù)組、map、通道、字符串類型的的迭代。編譯器會對每一種 range 支持的類型做專門的 “語法糖還原”。

          src/cmd/compile/internal/gc/range.go

          // walkrange transforms various forms of ORANGE into
          // simpler forms.  The result must be assigned back to n.
          // Node n may also be modified in place, and may also be
          // the returned node.
          func walkrange(n *Node) *Node {
              …………
              switch t.Etype {
                  default:
                      Fatalf("walkrange")

                  case TARRAY, TSLICE:
                      ……
                  case TMAP:
                      ……
                  case TCHAN:
                      ……
                  case TSRTING:
                      ……
              }
              ……
              n = walkstmt(n)

           lineno = lno
           return n
          }

          這里我們主要介紹數(shù)組切片和 map 的 for-range 迭代。字符串和通道的 range 迭代平時使用的不多,同時篇幅原因我們就不詳細(xì)介紹了,感興趣可以自行查看 Golang 源碼和參考文獻(xiàn)中自舉前 gcc 的源碼。

          一、for-range 數(shù)組和切片

          切片和數(shù)組的遍歷在 Golang 自舉后入口是同一個處理邏輯是相同的(1.5版本之前通過 gcc 編譯時數(shù)組和切片的 range 入口不同,但其實內(nèi)部邏輯大同小異),我們編碼過程中看到的實際表現(xiàn)不同都是數(shù)組和切片自身的底層結(jié)構(gòu)不同造成的。

          看這樣一個例子

          func main() {
               var a = [5]int{1, 2, 3, 4, 5} 
               var r [5]int
               for i, v := range a { 
                  if i == 0 {
                      a[1] = 12
                      a[2] = 13 
                  }
                  r[i] = v 
               }
               fmt.Println("r = ", r) 
               fmt.Println("a = ", a)
             }
             …………
             r = [1,2,3,4,5]
             a = [1,12,13,4,5]

          對于所有的 range 循環(huán) Go 語言都會在編譯期為遍歷對象創(chuàng)造一個副本,所以循環(huán)中通過短聲明的變量修改值不會影響原循環(huán)數(shù)組的值。

          第一次遍歷時修改了 a 的第二個和第三個元素,理論上第二次和第三次遍歷時 r 應(yīng)該能取到 a 修改后的值,但是我們剛說了 range 遍歷開始前會創(chuàng)建副本,也就是說 range 的是 a 的副本而不是 a 本身。所以 r 賦值時用的都是 a 的副本的 value 值,所以不變。

          那為啥 a 變了吶,if 語句中賦值語句是用的 a[1],a[2] 這時候是真的修改 a 的值的,所以 a 變了,這里也是我們推薦的用法。

          那如果想要讓 r 和 a 保持一致,修改同時生效吶? 可以range &a,通過引用的方式進(jìn)行循環(huán),這樣遍歷的每個元素雖然創(chuàng)建了副本但副本依舊是一個指向 a 的指針,因此后續(xù)所有循環(huán)中均是 &a 指向的原數(shù)組親自參與的,因此 v 能從 &a 指向的原數(shù)組中取出 a 修改后的值。


          接下來把遍歷的對象從數(shù)組改成切片再看下吧

          func main() {
               var a = []int{1, 2, 3, 4, 5} 
               var r = make([]int,5)
               for i, v := range a { 
                  if i == 0 {
                      a[1] = 12
                      a[2] = 13 
                  }
                  r[i] = v 
               }
               fmt.Println("r = ", r) 
               fmt.Println("a = ", a)
             }
             …………
             r = [1,12,13,4,5]   //注意變化
             a = [1,12,13,4,5]

          循環(huán)過程中依然創(chuàng)建了原切片的副本,但是因為切片自身的結(jié)構(gòu),創(chuàng)建的副本依然和原切片共享底層數(shù)組,只要沒發(fā)生擴(kuò)容,他們的值發(fā)生變化時就是同步變化的。效果就如同數(shù)組時range &a 一樣了。


          到這里我們一起來看下遍歷數(shù)組和切片時源碼是什么樣的吧?源碼比較長,我們大概挑選出來關(guān)鍵的簡單匯總就是如下

          ha := a   //創(chuàng)建副本
          hv1 := 0
          hn := len(ha)   //循環(huán)前長度已經(jīng)確定
          v1 := hv1       //索引變量和取值變量都只在開始時聲明,后面都是復(fù)用
          v2 := nil       
          for ; hv1 < hn; hv1++ {
              tmp := ha[hv1]  
              v1, v2 = hv1, tmp
              ...
          }

          這里給的是分析使用 for i, elem := range a {} 遍歷數(shù)組和切片,同時關(guān)心索引和數(shù)據(jù)的情況,只關(guān)心索引或者只關(guān)心數(shù)據(jù)值的代碼稍微不同,也就是關(guān)不關(guān)心 v1 和 v2 ,不關(guān)心直接nil掉。

          Golang 1.5版本之前的 gcc 源碼中語法糖擴(kuò)展的 range 源碼我們也貼出來方便大家理解。

          // The loop we generate:
          //   for_temp := range    //創(chuàng)建副本,數(shù)組的話重新復(fù)制新數(shù)組,切片的話復(fù)制新切片后,副本切片與原切片共享底層數(shù)組
          //   len_temp := len(for_temp)  //循環(huán)前長度已經(jīng)確定
          //   for index_temp = 0; index_temp < len_temp; index_temp++ {
          //           value_temp = for_temp[index_temp]
          //           index = index_temp
          //           value = value_temp
          //           original body
          //   }

          仔細(xì)看這兩段代碼,原來玄機(jī)都藏在這里了~~

          1. 循環(huán)次數(shù)在循環(huán)開始前已經(jīng)確定

          循環(huán)開始前先計算了數(shù)組和切片的長度,for 循環(huán)用這個長度來限制循環(huán)次數(shù)的,也就是循環(huán)次數(shù)在循環(huán)開始前就已經(jīng)確定了吶,so 循環(huán)中再怎么追加或者刪除元素都不會影響循環(huán)次數(shù),也就不會死循環(huán)了~~

          func main() {
             v := []int{1, 2, 3} 
             counter := 0
             for i := range v {
                counter++
                v = append(v, i) 
             }
             fmt.Println(counter)   //counter代表循環(huán)次數(shù),3次哦,沒有死循環(huán),也不是6次,雖然v其實已經(jīng)是長度為6的切片
             fmt.Println(v)   //[1,2,3,0,1,2]
          }

          2. 循環(huán)的時候會創(chuàng)建每個元素的副本

           type T struct {
               n int
           }
           func main() {
               ts := [2]T{}
               for i, t := range ts {
                   switch i {
                   case 0:
                       t.n = 3
                       ts[1].n = 9 
                   case 1:
                       fmt.Print(t.n, " "
                   }
               }
               fmt.Print(ts)
           }
          …………
           0 [{0} {9}]

          for-range 循環(huán)數(shù)組時使用的是數(shù)組 ts 的副本,所以 t.n = 3 的賦值操作不會影響原數(shù)組。但 ts[1].n = 9這種方式操作的確是原數(shù)組的元素值,所以是會發(fā)生變化的。這也是我們推崇的方法。

          3. 循環(huán)的時候短聲明只會在開始時執(zhí)行一次,后面都是重用

          循環(huán) index 和 value 在每次循環(huán)體中都會被重用,而不是新聲明。for-range 循環(huán)里的短聲明index,value :=相當(dāng)于第一次是 := ,后面都是 =,所以變量地址是不變的,就相當(dāng)于全局變量了。

          每次遍歷會把被循環(huán)元素當(dāng)前 key 和值賦給這兩個全局變量,但是注意變量還是那個變量,地址不變,所以如果用的是地址的或者當(dāng)前上下文環(huán)境值的話最后打印出來都是同一個值。

           func main() {
               slice := []int{0,1,2,3}
               m := make(map[int]*int)
               for key,val := range slice {
                 m[key] = &val
                 fmt.Println(key,&key)
                 fmt.Println(val,&val)
               }
               for k,v := range m {
                fmt.Println(k,"->",*v)
               }
           }
           …………
           0 0xc0000b4008
           0 0xc0000b4010
           1 0xc0000b4008
           1 0xc0000b4010
           2 0xc0000b4008
           2 0xc0000b4010
           3 0xc0000b4008
           3 0xc0000b4010
           0 -> 3
           1 -> 3
           2 -> 3
           3 -> 3

          key0、key1、key2、key3 其實都是短聲明中的key變量,所以地址是一致的,val0、val1、val2、val3 其實都是短聲明中的val變量,地址也一致

          最終遍歷 map 進(jìn)行輸出時因為 map 賦值時用的是 val 的地址m[key] = &val,循環(huán)結(jié)束時 val 的值是3,所以最終輸出時4個元素的值都是3。 

          這里需要注意 map 的遍歷輸出結(jié)果 key 的順序可能會不一致,比如2,0,1,3這樣,那是因為 map 的遍歷輸出是無序的,后面會再說,但是對應(yīng)的 value 的值都是3。

          那如果想要新生成的map也輸出正確的值怎么做吶?

          func main() {
               slice := []int{0,1,2,3}
               m := make(map[int]*int)
               for key,val := range slice {
                 value := val    //增加臨時變量,每次都是新聲明的,地址也就不一樣,也就能傳過去正確的值
                 m[key] = &value
                 fmt.Println(key,&key)
                 fmt.Println(val,&val)
               }
               for k,v := range m {
                fmt.Println(k,"->",*v)
               }
           }
           …………
           0 0xc00001a080
           1 0xc00001a0a0
           2 0xc00001a0b0
           3 0xc00001a0c0
           0 -> 0
           1 -> 1
           2 -> 2
           3 -> 3

          再來看下 for-range 循環(huán)中開啟了協(xié)程會怎么樣?

          func main() {
               var m = []int{1, 2, 3}
               for i, v := range m {
                   go func() {
                       fmt.Println(i, v) 
                   }()
               }
               time.Sleep(time.Second * 3) 
          }
          ……………
          2 3
          2 3
          2 3

          各個 goroutine 中輸出的 i、v 值都是 for-range 循環(huán)結(jié)束后的 i、v 最終值,而不是各個 goroutine 啟動時的 i, v值。因為 goroutine 執(zhí)行是在后面的某一個時間,使用的是執(zhí)行時上下文環(huán)境的變量值,i,v又相當(dāng)于一個全局變量,協(xié)程執(zhí)行時 for-range 循環(huán)已結(jié)束,i 和 v 都是最后一次循環(huán)的值2和3,所以最后輸出都是2和3。

          試試改成這樣

             func main() {
               var m = []int{1, 2, 3}
               for i, v := range m {
                   go func() {
                       fmt.Println(i, v) 
                   }()
                   if i==0 {
                       time.Sleep(time.Second*1)
                   }
               }
               time.Sleep(time.Second * 3) 
          }
          ……………
          0 1
          2 3
          2 3

          第一次遍歷后 sleep 了1秒,所以第一次循環(huán)中的協(xié)程有時間執(zhí)行了,開始執(zhí)行時當(dāng)前上下文中 i 和 v 的值還是第一次遍歷的0和1,后面的沒 sleep 就是最后循環(huán)結(jié)束時的2和3了。

          這里只是為了講明白環(huán)境上下文,其實我們平時不會這么用的,協(xié)程本來就是為了提升并發(fā)特性的,如果每次都 sleep 那還有什么意義吶。

          兩種方法,一種是臨時變量存儲循環(huán)iv值進(jìn)行使用,另外一種是通過函數(shù)參數(shù)進(jìn)行傳遞 go func(i,v){}(i,v)

          for i, v := range m {
               index := i // 這里的 := 會新聲明變量,而不是重用 
               value := v
               go func() {
                  fmt.Println(index, value) 
               }()
          }
          for i, v := range m { 
              go func(i,v int) {
                fmt.Println(i, v) 
              }(i,v)
          }

          至于 for-range 中通過 append 函數(shù)為切片追加元素繼而在循環(huán)外打印切片時元素值是否發(fā)生變化,取決于切片 append 的原理,容量是否足夠,是否發(fā)生擴(kuò)容生成新的底層數(shù)組,底層數(shù)組值是否發(fā)生改變等,不是本文的重點,這里就不詳細(xì)說了~~

          二、for-range Map

          接下來我們看看針對 Map 的 for-range, 還是先用一段代碼帶入。

          func main() {
               var m = map[string]int{ "A": 21,
                                       "B": 22,
                                       "C": 23, 
               }
               counter := 0
               for k, v := range m {
                   counter++
                   fmt.Println(k, v) 
                   key := fmt.Sprintf("%s%d""D", counter)
                   m[key] = 24   //給map增加了新元素
               }
               fmt.Println("counter is ", counter)
               fmt.Println(m)   
           }
           …………
           B 22
           C 23
           D1 24
           D2 24
           D3 24
           D4 24
           D5 24
           A 21
           counter is  8
           map[B:22 C:23 D1:24 D2:24 D3:24 D4:24 D5:24 D6:24 D7:24 D8:24 A:21]


          看看還原的源碼和語法糖吧,理解的更清楚些。

           ha := a   //副本,but沒計算長度
           hit := hiter(n.Type)
           th := hit.Type
           mapiterinit(typename(t), ha, &hit)
           for ; hit.key != nil; mapiternext(&hit) {
               key := *hit.key
               val := *hit.val
           }
           …………
           func mapiterinit(t *maptype, h *hmap, it *hiter) {
               it.t = t
               it.h = h
               it.B = h.B
               it.buckets = h.buckets

               r := uintptr(fastrand())
               it.startBucket = r & bucketMask(h.B)
               it.offset = uint8(r >> h.B & (bucketCnt - 1))
               it.bucket = it.startBucket
               mapiternext(it)
           }
           …………
           //  老版本中的gcc源碼
           //   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
           //   }

          從 mapiterinit 這個函數(shù)的參數(shù)調(diào)用的是指針 h *hmap,可以看出 ha := a 這個拷貝的是其實是指針,所以后續(xù)對 map 的修改還是會影響到原來的 map,所以與切片的 for-range 不同,map 的 for-range 長度沒有確定,所以遍歷的 counter 次數(shù)不是原始 map 大小3,但是也不會死循環(huán),而是一個不固定的值。     

          Golang 中 Map 是一種無序的鍵值對,索引順序沒有定義,Golang 不保證使用不同的索引后結(jié)果的順序相同( Golang 有意為之),所以其遍歷是無序的,包括循環(huán)外 println 打印整個 map 也是無序的。 

          如果 map 中的元素是在迭代過程中被添加的,添加的元素并不一定會在后續(xù)迭代中被遍歷到,可能出現(xiàn)也可能被跳過。

          func main() {
               var m = map[string]int{ "A": 21,
                                       "B": 22,
                                       "C": 23, 
               }
               counter := 0
               for k, v := range m {
                   if counter == 0 { 
                       delete(m, "A")
                   }
                   counter++
                   fmt.Println(k, v) 
                   
               }
               fmt.Println("counter is ", counter)  
           }
           …………
           2或者3

          for range map 是無序的,如果第一次循環(huán)到 A,則輸出 3,否則輸出 2。如果 map 中的元素在還沒有被遍歷到時就被移除了,后續(xù)的迭代中這個元素就不會再出現(xiàn)。

          三、for-range 編碼建議

          現(xiàn)在相信你對文章開頭的示例代碼的輸出應(yīng)該已經(jīng)明朗了,那么基于不同類型range 的這些特性,我們建議用 for-range 進(jìn)行迭代時最好遵循以下原則。

          1. 盡量用 index 來訪問 for-range 中真實的元素slice[index]
          2. go func()最好通過函數(shù)參數(shù)方式傳遞循環(huán)中的變量
          3. 循環(huán)變量在每一次迭代中都被賦值并會復(fù)用,不是每次都重新聲明,地址一樣。所以需要區(qū)分的時候需要重新每次重新聲明臨時變量。
          4. 可以在迭代過程中移除一個 map 里的元素或者向 map 里添加元素,添加的元素并不一定會在后續(xù)迭代中被遍歷到。所以最好不要在 range 迭代中修改 map,容易造成不確定性。
          5. 遍歷對象是引用類型時要注意副本其實依賴于源對象,合理使用。
          6. 數(shù)組和切片因為自身數(shù)據(jù)結(jié)構(gòu)的不同,range 迭代時表現(xiàn)也不一樣,可以根據(jù)實際場景進(jìn)行合理使用。


          今天我們通過編碼過程中的一些不那么直觀的坑點一起探討了 Golang 中 for-range 的原理、特殊注意事項,重點介紹了 for-range 切片和 Map。希望能幫助大家繞坑,表述不當(dāng)之處還能請大家見諒并及時指正~~

          【參考文獻(xiàn)】

          1. https://github.com/gcc-mirror/gcc/blob/master/gcc/go/gofrontend/statements.cc
          2. https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/


          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 182
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  又黄又爽一区二区三区 | 奇米影视成人社区 | 豆花视频在线欧美亚洲自拍 | 无码性爱片 | 免费看片色 |