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
是占位符,分別用x
、y
和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)變量 i
作為切片下標,逐一將切片中的元素讀取了出來。不過,這樣就有點麻煩了。但是使用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)文章
Go簡單實現(xiàn)協(xié)程池的實現(xiàn)示例
本文主要介紹了Go簡單實現(xiàn)協(xié)程池的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06如何使用工具自動監(jiān)測SSL證書有效期并發(fā)送提醒郵件
本文介紹了如何開發(fā)一個工具,用于每日檢測SSL證書剩余有效天數(shù)并通過郵件發(fā)送提醒,工具基于命令行,通過SMTP協(xié)議發(fā)送郵件,需配置SMTP連接信息,本文還提供了配置文件樣例及代碼實現(xiàn),幫助用戶輕松部署和使用該工具2024-10-10Go語言使用protojson庫實現(xiàn)Protocol Buffers與JSON轉(zhuǎn)換
本文主要介紹Google開源的工具庫Protojson庫如何Protocol Buffers與JSON進行轉(zhuǎn)換,以及和標準庫encoding/json的性能對比,需要的朋友可以參考下2023-09-09VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題及處理方法
這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06