欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go 循環(huán)結(jié)構(gòu)for循環(huán)使用教程全面講解

 更新時間:2023年10月12日 10:43:07   作者:賈維斯Echo  
這篇文章主要為大家介紹了Go 循環(huán)結(jié)構(gòu)for循環(huán)使用全面講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

一、for 循環(huán)介紹

日常編碼過程中,我們常常需要重復(fù)執(zhí)行同一段代碼,這時我們就需要循環(huán)結(jié)構(gòu)來幫助我們控制程序的執(zhí)行順序。一個循環(huán)結(jié)構(gòu)會執(zhí)行循環(huán)體中的代碼直到結(jié)尾,然后回到開頭繼續(xù)執(zhí)行。 主流編程語言都提供了對循環(huán)結(jié)構(gòu)的支持,絕大多數(shù)主流語言,比如:Python 提供了不止一種的循環(huán)語句,但 Go 卻只有一種,也就是 for 語句。

二、for 循環(huán)結(jié)構(gòu)

2.1 基本語法結(jié)構(gòu)

Go語言的for循環(huán)的一般結(jié)構(gòu)如下:

for 初始語句;條件表達式;結(jié)束語句{
    循環(huán)體語句
}

  • 初始語句:在循環(huán)開始前執(zhí)行一次的初始化操作,通常用于聲明計數(shù)器或迭代變量的初始值。
  • 條件表達式:循環(huán)會在每次迭代之前檢查條件表達式,只有當條件為真時,循循環(huán)才會繼續(xù)執(zhí)行。如果條件為假,循環(huán)結(jié)束。
  • 結(jié)束語句:在每次迭代之后執(zhí)行的操作,通常用于更新計數(shù)器或迭代變量的值。

以下是一個示例,演示了不同類型的for循環(huán)基本用法:

var sum int
for i := 0; i < 10; i++ {
    sum += i
}
println(sum)

這種 for 語句的使用形式是 Go 語言中 for 循環(huán)語句的進形式。我們用一幅流程圖來直觀解釋一下上面這句 for 循環(huán)語句的組成部分,以及各個部分的執(zhí)行順序:

從圖中我們看到,經(jīng)典 for 循環(huán)語句有四個組成部分(分別對應(yīng)圖中的①~④)。我們按順序拆解一下這張圖。

圖中①對應(yīng)的組成部分執(zhí)行于循環(huán)體(③ )之前,并且在整個 for 循環(huán)語句中僅會被執(zhí)行一次,它也被稱為循環(huán)前置語句。我們通常會在這個部分聲明一些循環(huán)體(③ )或循環(huán)控制條件(② )會用到的自用變量,也稱循環(huán)變量或迭代變量,比如這里聲明的整型變量 i。與 if 語句中的自用變量一樣,for 循環(huán)變量也采用短變量聲明的形式,循環(huán)變量的作用域僅限于 for 語句隱式代碼塊范圍內(nèi)。

圖中②對應(yīng)的組成部分,是用來決定循環(huán)是否要繼續(xù)進行下去的條件判斷表達式。和 if 語句的一樣,這個用于條件判斷的表達式必須為布爾表達式,如果有多個判斷條件,我們一樣可以由邏輯操作符進行連接。當表達式的求值結(jié)果為 true 時,代碼將進入循環(huán)體(③)繼續(xù)執(zhí)行,相反則循環(huán)直接結(jié)束,循環(huán)體(③)與組成部分④都不會被執(zhí)行。

前面也多次提到了,圖中③對應(yīng)的組成部分是 for 循環(huán)語句的循環(huán)體。如果相關(guān)的判斷條件表達式求值結(jié)構(gòu)為 true 時,循環(huán)體就會被執(zhí)行一次,這樣的一次執(zhí)行也被稱為一次迭代(Iteration)。在上面例子中,循環(huán)體執(zhí)行的動作是將這次迭代中變量 i 的值累加到變量 sum 中。

圖中④對應(yīng)的組成部分會在每次循環(huán)體迭代之后執(zhí)行,也被稱為循環(huán)后置語句。這個部分通常用于更新 for 循環(huán)語句組成部分①中聲明的循環(huán)變量,比如在這個例子中,我們在這個組成部分對循環(huán)變量 i 進行加 1 操作。

2.2 省略初始值

for 循環(huán)的初始語句可以被忽略,但是必須要寫初始語句后面的分號

i := 0
    for ; i < 10; i++ {
        fmt.Println(i)
    }

2.3 省略初始語句和結(jié)束語句

for循環(huán)的初始語句和結(jié)束語句都可以省略,例如:

func main() {
    var i int
    for i < 10 {
        fmt.Println(i)
        i++
    }
}

這種寫法類似于其他編程語言中的while,在while后添加一個條件表達式,滿足條件表達式時持續(xù)循環(huán),否則結(jié)束循環(huán)。

2.4 無限循環(huán)

無限循環(huán)是一種循環(huán)結(jié)構(gòu),它會一直執(zhí)行,而不受循環(huán)條件的限制,同時省略了初始語句,條件表達式,結(jié)束語句?;菊Z法格式如下:

for {
    循環(huán)體語句
}

它的形式等價于:

for true {
   // 循環(huán)體代碼
}

或者等價于:

for ; ; {
   // 循環(huán)體代碼
}

在日常使用時,建議你用它的最簡形式,也就是for {...},更加簡單。

舉個栗子:

for {
        fmt.Println("這是一個死循環(huán)!")
    }

無限循環(huán)通常在編程中用于執(zhí)行需要持續(xù)運行的任務(wù),如服務(wù)器監(jiān)聽、事件處理等。

2.5 for 循環(huán)支持聲明多循環(huán)變量

Go 語言的 for 循環(huán)支持聲明多循環(huán)變量,并且可以應(yīng)用在循環(huán)體以及判斷條件中,比如下面就是一個使用多循環(huán)變量的、稍復(fù)雜的例子:

var sum int
    for i, j, k := 0, 1, 2; (i < 20) && (j < 10) && (k < 30); i, j, k = i+1, j+1, k+5 {
        sum += (i + j + k)
        println(sum)
    }

在這個例子中,我們聲明了三個循環(huán)自用變量 i、j 和 k,它們共同參與了循環(huán)條件判斷與循環(huán)體的執(zhí)行。這段代碼的執(zhí)行流程解釋如下:

  • 開始時,i 被初始化為 0,j 被初始化為 1,k 被初始化為 2,sum 被初始化為 0。
  • 進入循環(huán)。在每次迭代中,首先檢查三個條件:i < 20、j < 10 和 k < 30。只有在這三個條件都為真時,循環(huán)才會繼續(xù)執(zhí)行。
  • 在每次迭代中,計算 i + j + k 的和,并將結(jié)果添加到 sum 中。
  • 使用 println 函數(shù)打印 sum 的當前值。
  • 繼續(xù)迭代,i、j 和 k 分別增加 1、1 和 5。
  • 重復(fù)步驟 2、3、4 直到其中一個條件不再滿足。在這種情況下,當 i 大于或等于 20、j 大于或等于 10 或 k 大于或等于 30 時,循環(huán)結(jié)束。

2.6 小練習(xí):打印九九乘法表

for y := 1; y <= 9; y++ {
        // 遍歷, 決定這一行有多少列
        for x := 1; x <= y; x++ {
            fmt.Printf("%d*%d=%d ", x, y, x*y)
        }
        // 手動生成回車
        fmt.Println()
    }

輸出結(jié)果如下:

1*1=1 
1*2=2 2*2=4 
1*3=3 2*3=6 3*3=9 
1*4=4 2*4=8 3*4=12 4*4=16 
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25 
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36 
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49 
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64 
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

執(zhí)行過程如下:

  • for y := 1; y <= 9; y++:這是外部的for循環(huán),它初始化一個名為 y 的循環(huán)變量,從1開始,每次迭代遞增1,一直到 y 的值小于或等于9。
  • 內(nèi)部的for循環(huán) for x := 1; x <= y; x++:這是內(nèi)部的for循環(huán),用于控制每行的列數(shù)。循環(huán)變量 x 從1開始,每次迭代遞增1,一直到 x 的值小于或等于 y。這確保了每一行都只打印與行數(shù)相等或更小的列數(shù)。
  • fmt.Printf("%d*%d=%d ", x, y, x*y):在內(nèi)部循環(huán)中,這一行代碼用于打印每個乘法表達式。它使用 fmt.Printf 函數(shù),打印了一個格式化的字符串,其中 %d 是占位符,分別用 xy 和 x*y 的值替換。這將打印類似 "11=1 "、"12=2 "、"2*2=4 " 的格式。
  • fmt.Println():在內(nèi)部循環(huán)結(jié)束后,使用 fmt.Println 打印一個換行符,以將每行的輸出分開。

三、for range(鍵值循環(huán))

3.1 基本介紹

在編程中,經(jīng)常需要遍歷和操作集合(如數(shù)組、切片、映射等)中的元素。Go語言中可以使用for range遍歷數(shù)組、切片、字符串、map 及通道(channel)。 通過for range遍歷的返回值有以下規(guī)律:

  • 數(shù)組、切片、字符串返回索引和值。
  • map返回鍵和值。
  • 通道(channel)只返回通道內(nèi)的值。

3.2 基本語法格式

for range 循環(huán)的基本語法格式如下:

for key, value := range collection {
    // 循環(huán)體代碼,使用 key 和 value
}
  • key 是元素的索引或鍵。
  • value 是元素的值。
  • collection 是要遍歷的元素,如字符串、數(shù)組、切片、映射等。

舉個例子,首先我們使用for 循環(huán)基本形式:

var sl = []int{1, 2, 3, 4, 5}
for i := 0; i < len(sl); i++ {
    fmt.Printf("sl[%d] = %d\n", i, sl[i])
}

上面的例子中,我們使用循環(huán)前置語句中聲明的循環(huán)變量 作為切片下標,逐一將切片中的元素讀取了出來。不過,這樣就有點麻煩了。但是使用for range 循環(huán)后如下:

var sl = []int{1, 2, 3, 4, 5}
for i, v := range sl {
    fmt.Printf("sl[%d] = %d\n", i, v)
}

我們看到,for range 循環(huán)形式除了循環(huán)體保留了下來,其余組成部分都“不見”了。其實那幾部分已經(jīng)被融合到 for range 的語義中了。

具體來說,這里的 i 和 v 對應(yīng)的是for 語句形式中循環(huán)前置語句的循環(huán)變量,它們的初值分別為切片 sl 的第一個元素的下標值和元素值。并且,隱含在 for range 語義中的循環(huán)控制條件判斷為:是否已經(jīng)遍歷完 sl 的所有元素,等價于i < len(sl)這個布爾表達式。另外,每次迭代后,for range 會取出切片 sl 的下一個元素的下標和值,分別賦值給循環(huán)變量 i 和 v,這與 for 經(jīng)典形式下的循環(huán)后置語句執(zhí)行的邏輯是相同的。

3.3 for range 語句幾個常見的“變種”

3.3.1 省略value

有時候,您可能只對元素中的index感興趣,而不需要值value。在這種情況下,您可以省略值部分,只使用鍵。示例如下:

fruits := []string{"apple", "banana", "cherry"}
for index := range fruits {
    fmt.Printf("Index: %d\n", index)
}

3.3.2 省略 key

如果我們不關(guān)心元素下標,只關(guān)心元素值,那么我們可以用空標識符替代代表下標值的變量 i。這里一定要注意,這個空標識符不能省略,否則就與上面形式一樣了,Go 編譯器將無法區(qū)分:

for _, v := range sl {
  // ... 
}

3.3.3 同時省略 key 和 value

如果我們既不關(guān)心元素下標值,也不關(guān)心元素值,那是否能寫成下面這樣呢:

for _, _ = range sl {
  // ... 
}

這種形式在語法上沒有錯誤,就是看起來不太優(yōu)雅。Go 在Go 1.4 版本中就提供了一種優(yōu)雅的等價形式,后續(xù)直接使用這種形式就好了:

for range sl {
  // ... 
}

四、for 循環(huán)常用操作

4.1 遍歷數(shù)組、切片——獲得索引和元素

在遍歷代碼中,key 和 value 分別代表切片的下標及下標對應(yīng)的值。下面的代碼展示如何遍歷切片,數(shù)組也是類似的遍歷方法:

package main
import "fmt"
func main() {
    for key, value := range []int{1, 2, 3, 4} {
        fmt.Printf("key:%d value:%d\n", key, value)
    }
}
/*
代碼輸出如下:
key:0 value:1
key:1 value:2
key:2 value:3
key:3 value:4
 */

4.2 遍歷string 類型--獲得字符串

下面這段代碼展示了如何遍歷字符串:

var s = "中國人"
for i, v := range s {
    fmt.Printf("%d %s 0x%x\n", i, string(v), v)
}

輸出結(jié)果如下:

0 中 0x4e2d
3 國 0x56fd
6 人 0x4eba

我們看到:for range 對于 string 類型來說,每次循環(huán)得到的 v 值是一個 Unicode 字符碼點,也就是 rune 類型值,而不是一個字節(jié),返回的第一個值 i 為該 Unicode 字符碼點的內(nèi)存編碼(UTF-8)的第一個字節(jié)在字符串內(nèi)存序列中的位置。

4.3 遍歷map——獲得map的鍵和值

map 就是一個鍵值對(key-value)集合,最常見的對 map 的操作,就是通過 key 獲取其對應(yīng)的 value 值。但有些時候,我們也要對 map 這個集合進行遍歷,這就需要 for 語句的支持了。

但在 Go 語言中,我們要對 map 進行循環(huán)操作,for range 是唯一的方法,for 經(jīng)典循環(huán)形式是不支持對 map 類型變量的循環(huán)控制的。下面是通過 for range,對一個 map 類型變量進行循環(huán)操作的示例:

var m = map[string]int {
  "Rob" : 67,
    "Russ" : 39,
    "John" : 29,
}
for k, v := range m {
    println(k, v)
}

運行這個示例,我們會看到這樣的輸出結(jié)果:

John 29
Rob 67
Russ 39

通過輸出結(jié)果我們看到:for range 對于 map 類型來說,每次循環(huán),循環(huán)變量 k 和 v 分別會被賦值為 map 鍵值對集合中一個元素的 key 值和 value 值。而且,map 類型中沒有下標的概念,通過 key 和 value 來循環(huán)操作 map 類型變量也就十分自然了。

4.4 遍歷通道(channel)——接收通道數(shù)據(jù)

除了可以針對 string、數(shù)組 / 切片,以及 map 類型變量進行循環(huán)操作控制之外,for range 還可以與 channel 類型配合工作。

c := make(chan int)
go func() {
    c <- 1
    c <- 2
    c <- 3
    close(c)
}()
for v := range c {
    fmt.Println(v)
}

channel 是 Go 語言提供的并發(fā)設(shè)計的原語,它用于多個 Goroutine 之間的通信。當 channel 類型變量作為 for range 語句的迭代對象時,for range 會嘗試從 channel 中讀取數(shù)據(jù),使用形式是這樣的:

var c = make(chan int)
for v := range c {
   // ... 
}

在這個例子中,for range 每次從 channel 中讀取一個元素后,會把它賦值給循環(huán)變量 v,并進入循環(huán)體。當 channel 中沒有數(shù)據(jù)可讀的時候, for range 循環(huán)會阻塞在對 channel 的讀操作上。直到 channel 關(guān)閉時,for range 循環(huán)才會結(jié)束,這也是 for range 循環(huán)與 channel 配合時隱含的循環(huán)判斷條件。

五、跳出循環(huán)與終止循環(huán)

5.1 continue 語句(繼續(xù)下次循環(huán))

5.1.1 continue 基本語法

首先,我們來看第一種場景。如果循環(huán)體中的代碼執(zhí)行到一半,要中斷當前迭代,忽略此迭代循環(huán)體中的后續(xù)代碼,并回到 for 循環(huán)條件判斷,嘗試開啟下一次迭代,這個時候我們可以怎么辦呢?我們可以使用 continue 語句來應(yīng)對?;菊Z法如下:

for initialization; condition; update {
    // 循環(huán)體
    if someCondition {
        continue
    }
    // 其他循環(huán)體的代碼
}
  • initialization 是初始化語句,通常用于初始化循環(huán)變量。
  • condition 是循環(huán)條件,當條件為真時繼續(xù)循環(huán),否則退出。
  • update 是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。

帶標簽的 continue 語句用于跳過當前迭代中 if 語句中的 someCondition 滿足的部分,直接進行下一次迭代。如果沒有標簽,continue 將默認跳過當前循環(huán)的下一次迭代。

以下是一個示例,演示 continue 語句的基本語法:

var sum int
var sl = []int{1, 2, 3, 4, 5, 6}
for i := 0; i < len(sl); i++ {
    if sl[i]%2 == 0 {
        // 忽略切片中值為偶數(shù)的元素
        continue
    }
    sum += sl[i]
}
println(sum) // 9

這段代碼會循環(huán)遍歷切片中的元素,把值為奇數(shù)的元素相加,然后存儲在變量 sum 中。我們可以看到,在這個代碼的循環(huán)體中,如果我們判斷切片元素值為偶數(shù),就使用 continue 語句中斷當前循環(huán)體的執(zhí)行,那么循環(huán)體下面的 sum += sl[i] 在這輪迭代中就會被忽略。代碼執(zhí)行流會直接來到循環(huán)后置語句i++,之后對循環(huán)條件表達式(i < len(sl))進行求值,如果為 true,將再次進入循環(huán)體,開啟新一次迭代。

5.1.2 帶標簽的continue語句

Go 語言中的 continue 在 C 語言 continue 語義的基礎(chǔ)上又增加了對 label 的支持。label 語句的作用,是標記跳轉(zhuǎn)的目標。帶標簽的continue語句的基本語法格式如下:

loopLabel:
for initialization; condition; update {
    // 循環(huán)體
    if someCondition {
        continue loopLabel
    }
}
  • loopLabel 是一個用戶定義的標簽(標識符),用于標記循環(huán)。
  • initialization 是初始化語句,通常用于初始化循環(huán)變量。
  • condition 是循環(huán)條件,當條件為真時繼續(xù)循環(huán),否則退出。
  • update 是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。

帶標簽的continue語句用于在嵌套循環(huán)中指定要跳過的循環(huán),其工作方式是:如果某個條件滿足,執(zhí)行continue loopLabel,其中loopLabel是要跳過的循環(huán)的標簽,它將控制流轉(zhuǎn)移到帶有相應(yīng)標簽的循環(huán)的下一次迭代。如果沒有指定標簽,continue將默認跳過當前循環(huán)的下一次迭代。

我們可以把上面的代碼改造為使用 label 的等價形式:

func main() {
    var sum int
    var sl = []int{1, 2, 3, 4, 5, 6}
loop:
    for i := 0; i < len(sl); i++ {
        if sl[i]%2 == 0 {
            // 忽略切片中值為偶數(shù)的元素
            continue loop
        }
        sum += sl[i]
    }
    println(sum) // 9
}

你可以看到,在這段代碼中,我們定義了一個 label:loop,它標記的跳轉(zhuǎn)目標恰恰就是我們的 for 循環(huán)。也就是說,我們在循環(huán)體中可以使用 continue+ loop label 的方式來實現(xiàn)循環(huán)體中斷,這與前面的例子在語義上是等價的。不過這里僅僅是一個演示,通常我們在這樣非嵌套循環(huán)的場景中會直接使用不帶 label 的 continue 語句。

而帶 label 的 continue 語句,通常出現(xiàn)于嵌套循環(huán)語句中,被用于跳轉(zhuǎn)到外層循環(huán)并繼續(xù)執(zhí)行外層循環(huán)語句的下一個迭代,比如下面這段代碼:

func main() {
    var sl = [][]int{
        {1, 34, 26, 35, 78},
        {3, 45, 13, 24, 99},
        {101, 13, 38, 7, 127},
        {54, 27, 40, 83, 81},
    }
outerloop:
    for i := 0; i < len(sl); i++ {
        for j := 0; j < len(sl[i]); j++ {
            if sl[i][j] == 13 {
                fmt.Printf("found 13 at [%d, %d]\n", i, j)
                continue outerloop
            }
        }
    }
}

在這段代碼中,變量 sl 是一個元素類型為[]int 的切片(二維切片),其每個元素切片中至多包含一個整型數(shù) 13。main 函數(shù)的邏輯就是在 sl 的每個元素切片中找到 13 這個數(shù)字,并輸出它的具體位置信息。

那這要怎么查找呢?一種好的實現(xiàn)方式就是,我們只需要在每個切片中找到 13,就不用繼續(xù)在這個切片的剩余元素中查找了。

我們用 for 經(jīng)典形式來實現(xiàn)這個邏輯。面對這個問題,我們要使用嵌套循環(huán),具體來說就是外層循環(huán)遍歷 sl 中的元素切片,內(nèi)層循環(huán)遍歷每個元素切片中的整型值。一旦內(nèi)層循環(huán)發(fā)現(xiàn) 13 這個數(shù)值,我們便要中斷內(nèi)層 for 循環(huán),回到外層 for 循環(huán)繼續(xù)執(zhí)行。

如果我們用不帶 label 的 continue 能不能完成這一功能呢?答案是不能。因為它只能中斷內(nèi)層循環(huán)的循環(huán)體,并繼續(xù)開啟內(nèi)層循環(huán)的下一次迭代。而帶 label 的 continue 語句是這個場景下的“最佳人選”,它會直接結(jié)束內(nèi)層循環(huán)的執(zhí)行,并回到外層循環(huán)繼續(xù)執(zhí)行。

這一行為就好比在外層循環(huán)放置并執(zhí)行了一個不帶 label 的 continue 語句。它會中斷外層循環(huán)中當前迭代的執(zhí)行,執(zhí)行外層循環(huán)的后置語句(i++),然后再對外層循環(huán)的循環(huán)控制條件語句進行求值,如果為 true,就將繼續(xù)執(zhí)行外層循環(huán)的新一次迭代。

5.2 goto(跳轉(zhuǎn)到指定標簽)

goto語句通過標簽進行代碼間的無條件跳轉(zhuǎn)。goto語句可以在快速跳出循環(huán)、避免重復(fù)退出上有一定的幫助。Go語言中使用goto語句能簡化一些代碼的實現(xiàn)過程。 例如雙層嵌套的for循環(huán)要退出時:

func main() {
    var breakFlag bool
    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            if j == 2 {
                // 設(shè)置退出標簽
                breakFlag = true
                break
            }
            fmt.Printf("%v-%v\n", i, j)
        }
        // 外層for循環(huán)判斷
        if breakFlag {
            break
        }
    }
}

使用goto語句能簡化代碼:

func main() {
    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            if j == 2 {
                // 設(shè)置退出標簽
                goto breakTag
            }
            fmt.Printf("%v-%v\n", i, j)
        }
    }
    return
    // 標簽
breakTag:
    fmt.Println("結(jié)束for循環(huán)")
}

goto 是一種公認的、難于駕馭的語法元素,應(yīng)用 goto 的代碼可讀性差、代碼難于維護還易錯。雖然 Go 語言保留了 goto,在平常開發(fā)中,不推薦使用。

5.3 break(跳出循環(huán))

日常編碼中,我們還會遇到一些場景,在這些場景中,我們不僅要中斷當前循環(huán)體迭代的進行,還要同時徹底跳出循環(huán),終結(jié)整個循環(huán)語句的執(zhí)行。面對這樣的場景,continue 語句就不再適用了,Go 語言為我們提供了 break 語句來解決這個問題。

5.3.1 break基本語法

break語句的基本語法如下:

for initialization; condition; update {
    // 循環(huán)體
    if someCondition {
        break
    }
    // 其他循環(huán)體的代碼
}
  • initialization 是初始化語句,通常用于初始化循環(huán)變量。
  • condition 是循環(huán)條件,當條件為真時繼續(xù)循環(huán),否則退出。
  • update 是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。

當在循環(huán)中執(zhí)行 break 語句時,它會立即終止當前的循環(huán),無論條件是否滿足,然后將控制流傳遞到循環(huán)之后的代碼。

我們來看下面這個示例中 break 語句的應(yīng)用:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1
    // 找出整型切片sl中的第一個偶數(shù)
    for i := 0; i < len(sl); i++ {
        if sl[i]%2 == 0 {
            firstEven = sl[i]
            break
        }
    }
    println(firstEven) // 6
}

這段代碼邏輯很容易理解,我們通過一個循環(huán)結(jié)構(gòu)來找出切片 sl 中的第一個偶數(shù),一旦找到就不需要繼續(xù)執(zhí)行后續(xù)迭代了。這個時候我們就通過 break 語句跳出了這個循環(huán)。

5.3.2 帶標簽的break語法

 continue 語句一樣,Go 也 break 語句增加了對 label 的支持。而且,和前面 continue 語句一樣,如果遇到嵌套循環(huán),break 要想跳出外層循環(huán),用不帶 label 的 break 是不夠,因為不帶 label 的 break 僅能跳出其所在的最內(nèi)層循環(huán)。要想實現(xiàn)外層循環(huán)的跳出,我們還需給 break 加上 label。所以,帶標簽的 break 語句允許您從嵌套循環(huán)中跳出特定循環(huán),而不是默認跳出當前循環(huán)。帶標簽的 break 語法如下:

loopLabel:
for initialization; condition; update {
    // 循環(huán)體
    if someCondition {
        break loopLabel
    }
    // 其他循環(huán)體的代碼
}
  • loopLabel 是用戶定義的標簽(標識符),用于標記循環(huán)。
  • initialization 是初始化語句,通常用于初始化循環(huán)變量。
  • condition 是循環(huán)條件,當條件為真時繼續(xù)循環(huán),否則退出。
  • update 是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。

當帶標簽的 break 語句執(zhí)行時,它會終止帶有相應(yīng)標簽的循環(huán),而不是默認的當前循環(huán)。

我們來看一個具體的例子:

var gold = 38
func main() {
    var sl = [][]int{
        {1, 34, 26, 35, 78},
        {3, 45, 13, 24, 99},
        {101, 13, 38, 7, 127},
        {54, 27, 40, 83, 81},
    }
outerloop:
    for i := 0; i < len(sl); i++ {
        for j := 0; j < len(sl[i]); j++ {
            if sl[i][j] == gold {
                fmt.Printf("found gold at [%d, %d]\n", i, j)
                break outerloop
            }
        }
    }
}

這個例子和我們前面的帶 label 的 continue 語句的例子很像,main 函數(shù)的邏輯就是,在 sl 這個二維切片中找到 38 這個數(shù)字,并輸出它的位置信息。整個二維切片中至多有一個值為 38 的元素,所以只要我們通過嵌套循環(huán)發(fā)現(xiàn)了 38,我們就不需要繼續(xù)執(zhí)行這個循環(huán)了。這時,我們通過帶有 label 的 break 語句,就可以直接終結(jié)外層循環(huán),從而從復(fù)雜多層次的嵌套循環(huán)中直接跳出,避免不必要的算力資源的浪費。

六、for 循環(huán)常見“坑”與避坑指南

for 語句的常見“坑”點通常和 for range 這個“語法糖”有關(guān)。雖然 for range 的引入提升了 Go 語言的表達能力,也簡化了循環(huán)結(jié)構(gòu)的編寫,但 for range 也不是“免費的午餐”,在開發(fā)中,經(jīng)常會遇到一些問題,下面我們就來看看這些常見的問題。

6.1 循環(huán)變量的重用

我們前面說過,for range 形式的循環(huán)語句,使用短變量聲明的方式來聲明循環(huán)變量,循環(huán)體將使用這些循環(huán)變量實現(xiàn)特定的邏輯,但你在剛開始學(xué)習(xí)使用的時候,可能會發(fā)現(xiàn)循環(huán)變量的值與你之前的“預(yù)期”不符,比如下面這個例子:

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

這個示例是對一個整型切片進行遍歷,并且在每次循環(huán)體的迭代中都會創(chuàng)建一個新的 Goroutine(Go 中的輕量級協(xié)程),輸出這次迭代的元素的下標值與元素值。

現(xiàn)在我們繼續(xù)看這個例子,我們預(yù)期的輸出結(jié)果可能是這樣的:

0 1
1 2
2 3
3 4
4 5

那實際輸出真的是這樣嗎?我們實際運行輸出一下:

4 5
4 5
4 5
4 5
4 5

我們看到,Goroutine 中輸出的循環(huán)變量,也就是 i 和 v 的值都是 for range 循環(huán)結(jié)束后的最終值,而不是各個 Goroutine 啟動時變量 i 和 v 的值,與我們最初的“預(yù)期”不符,這是為什么呢?

這是因為我們最初的“預(yù)期”本身就是錯的。這里,很可能會被 for range 語句中的短聲明變量形式“迷惑”,簡單地認為每次迭代都會重新聲明兩個新的變量 i 和 v。但事實上,這些循環(huán)變量在 for range 語句中僅會被聲明一次,且在每次迭代中都會被重用。

基于隱式代碼塊的規(guī)則,我們可以將上面的 for range 語句做一個等價轉(zhuǎn)換,這樣可以幫助你理解 for range 的工作原理。等價轉(zhuǎn)換后的結(jié)果是這樣的:

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

通過等價轉(zhuǎn)換后的代碼,我們可以清晰地看到循環(huán)變量 i 和 v 在每次迭代時的重用。而 Goroutine 執(zhí)行的閉包函數(shù)引用了它的外層包裹函數(shù)中的變量 i、v,這樣,變量 i、v 在主 Goroutine 和新啟動的 Goroutine 之間實現(xiàn)了共享,而 i, v 值在整個循環(huán)過程中是重用的,僅有一份。在 for range 循環(huán)結(jié)束后,i = 4, v = 5,因此各個 Goroutine 在等待 3 秒后進行輸出的時候,輸出的是 i, v 的最終值。

那么如何修改代碼,可以讓實際輸出和我們最初的預(yù)期輸出一致呢?我們可以為閉包函數(shù)增加參數(shù),并且在創(chuàng)建 Goroutine 時將參數(shù)與 i、v 的當時值進行綁定,看下面的修正代碼:

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

這回的輸出結(jié)果與我們的預(yù)期就是一致的了。不過這里你要注意:你執(zhí)行這個程序的輸出結(jié)果的行序,可能與我的不同,這是由 Goroutine 的調(diào)度所決定的。

6.2 參與循環(huán)的是 range 表達式的副本

在 for range 語句中,range 后面接受的表達式的類型可以是數(shù)組、指向數(shù)組的指針、切片、字符串,還有 map 和 channel(需具有讀權(quán)限)。我們以數(shù)組為例來看一個簡單的例子:

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

這個例子說的是對一個數(shù)組 a 的元素進行遍歷操作,當處理下標為 0 的元素時,我們修改了數(shù)組 a 的第二個和第三個元素的值,并且在每個迭代中,我們都將從 a 中取得的元素值賦值給新數(shù)組 r。我們期望這個程序會輸出如下結(jié)果:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

但實際運行該程序的輸出結(jié)果卻是:

original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]

我們原以為在第一次迭代過程,也就是 i = 0 時,我們對 a 的修改 (a[1] =12,a[2] = 13) 會在第二次、第三次迭代中被 v 取出,但從結(jié)果來看,v 取出的依舊是 a 被修改前的值:2 和 3。

為什么會是這種情況呢?原因就是參與 for range 循環(huán)的是 range 表達式的副本。也就是說,在上面這個例子中,真正參與循環(huán)的是 a 的副本,而不是真正的 a。

為了方便你理解,我們將上面的例子中的 for range 循環(huán),用一個等價的偽代碼形式重寫一下:

for i, v := range a' { //a'是a的一個值拷貝
    if i == 0 {
        a[1] = 12
        a[2] = 13
    }
    r[i] = v
}

現(xiàn)在真相終于揭開了:這個例子中,每次迭代的都是從數(shù)組 a 的值拷貝 a’中得到的元素。a’是 Go 臨時分配的連續(xù)字節(jié)序列,與 a 完全不是一塊內(nèi)存區(qū)域。因此無論 a 被如何修改,它參與循環(huán)的副本 a’依舊保持原值,因此 v 從 a’中取出的仍舊是 a 的原值,而不是修改后的值。

那么應(yīng)該如何解決這個問題,讓輸出結(jié)果符合我們前面的預(yù)期呢?在 Go 中,大多數(shù)應(yīng)用數(shù)組的場景我們都可以用切片替代,這里我們也用切片來試試看:

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

你可以看到,在 range 表達式中,我們用了 a[:]替代了原先的 a,也就是將數(shù)組 a 轉(zhuǎn)換為一個切片,作為 range 表達式的循環(huán)對象。運行這個修改后的例子,結(jié)果是這樣的:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

我們看到輸出的結(jié)果與最初的預(yù)期終于一致了,顯然用切片能實現(xiàn)我們的要求。

那切片是如何做到的呢?切片在 Go 內(nèi)部表示為一個結(jié)構(gòu)體,由(array, len, cap)組成,其中 array 是指向切片對應(yīng)的底層數(shù)組的指針,len 是切片當前長度,cap 為切片的最大容量。

所以,當進行 range 表達式復(fù)制時,我們實際上復(fù)制的是一個切片,也就是表示切片的結(jié)構(gòu)體。表示切片副本的結(jié)構(gòu)體中的 array,依舊指向原切片對應(yīng)的底層數(shù)組,所以我們對切片副本的修改也都會反映到底層數(shù)組 a 上去。而 v 再從切片副本結(jié)構(gòu)體中 array 指向的底層數(shù)組中,獲取數(shù)組元素,也就得到了被修改后的元素值。

6.3 遍歷 map 中元素的隨機性

根據(jù)上面的講解,當 map 類型變量作為 range 表達式時,我們得到的 map 變量的副本與原變量指向同一個 map。如果我們在循環(huán)的過程中,對 map 進行了修改,那么這樣修改的結(jié)果是否會影響后續(xù)迭代呢?這個結(jié)果和我們遍歷 map 一樣,具有隨機性。

比如我們來看下面這個例子,在 map 循環(huán)過程中,當 counter 值為 0 時,我們刪除了變量 m 中的一個元素:

var m = map[string]int{
    "tony": 21,
    "tom":  22,
    "jim":  23,
}
counter := 0
for k, v := range m {
    if counter == 0 {
        delete(m, "tony")
    }
    counter++
    fmt.Println(k, v)
}
fmt.Println("counter is ", counter)

如果我們反復(fù)運行這個例子多次,會得到兩個不同的結(jié)果。當 k="tony"作為第一個迭代的元素時,我們將得到如下結(jié)果:

tony 21
tom 22
jim 23
counter is  3

否則,我們得到的結(jié)果是這樣的:

tom 22
jim 23
counter is  2

如果我們在針對 map 類型的循環(huán)體中,新創(chuàng)建了一個 map 元素項,那這項元素可能出現(xiàn)在后續(xù)循環(huán)中,也可能不出現(xiàn):

var m = map[string]int{
    "tony": 21,
    "tom":  22,
    "jim":  23,
}
counter := 0
for k, v := range m {
    if counter == 0 {
        m["lucy"] = 24
    }
    counter++
    fmt.Println(k, v)
}
fmt.Println("counter is ", counter)

這個例子的執(zhí)行結(jié)果也會有兩個:

tony 21
tom 22
jim 23
lucy 24
counter is  4

或:

tony 21
tom 22
jim 23
counter is  3

考慮到上述這種隨機性,我們?nèi)粘>幋a遇到遍歷 map 的同時,還需要對 map 進行修改的場景的時候,要格外小心。

以上就是Go 循環(huán)結(jié)構(gòu)for循環(huán)使用全面講解的詳細內(nèi)容,更多關(guān)于Go for循環(huán)結(jié)構(gòu)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Golang依賴注入工具digo的使用詳解

    Golang依賴注入工具digo的使用詳解

    這篇文章主要為大家詳細介紹了Golang中依賴注入工具digo的使用,文中的示例代碼講解詳細,具有一定的學(xué)習(xí)價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-06-06
  • Go簡單實現(xiàn)協(xié)程池的實現(xiàn)示例

    Go簡單實現(xiàn)協(xié)程池的實現(xiàn)示例

    本文主要介紹了Go簡單實現(xiàn)協(xié)程池的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • 詳解Go語言中rand(隨機數(shù))包的使用

    詳解Go語言中rand(隨機數(shù))包的使用

    在Golang中,有兩個包提供了rand,分別為math/rand和crypto/rand對應(yīng)兩種應(yīng)用場景。math/rand包實現(xiàn)了偽隨機數(shù)生成器。也就是生成 整形和浮點型;crypto/rand包實現(xiàn)了用于加解密的更安全的隨機數(shù)生成器。本文就來和大家詳細講講math/rand的使用
    2022-08-08
  • 如何使用工具自動監(jiān)測SSL證書有效期并發(fā)送提醒郵件

    如何使用工具自動監(jiān)測SSL證書有效期并發(fā)送提醒郵件

    本文介紹了如何開發(fā)一個工具,用于每日檢測SSL證書剩余有效天數(shù)并通過郵件發(fā)送提醒,工具基于命令行,通過SMTP協(xié)議發(fā)送郵件,需配置SMTP連接信息,本文還提供了配置文件樣例及代碼實現(xiàn),幫助用戶輕松部署和使用該工具
    2024-10-10
  • graphql---go http請求使用詳解

    graphql---go http請求使用詳解

    這篇文章主要介紹了graphql---go http請求使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Golang switch語句的具體使用

    Golang switch語句的具體使用

    switch 語句提供了一種簡潔的方式來執(zhí)行多路分支選擇,本文主要介紹了Golang switch語句的具體使用,具有一定的參考價值,感興趣的可以了解一下
    2024-08-08
  • Go語言使用protojson庫實現(xiàn)Protocol Buffers與JSON轉(zhuǎn)換

    Go語言使用protojson庫實現(xiàn)Protocol Buffers與JSON轉(zhuǎn)換

    本文主要介紹Google開源的工具庫Protojson庫如何Protocol Buffers與JSON進行轉(zhuǎn)換,以及和標準庫encoding/json的性能對比,需要的朋友可以參考下
    2023-09-09
  • Golang 探索對Goroutine的控制方法(詳解)

    Golang 探索對Goroutine的控制方法(詳解)

    下面小編就為大家分享一篇Golang 探索對Goroutine的控制方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-12-12
  • Go整合ElasticSearch的示例代碼

    Go整合ElasticSearch的示例代碼

    這篇文章主要介紹了Go整合ElasticSearch的相關(guān)知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題及處理方法

    VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題及處理方法

    這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-06-06

最新評論