Go 循環(huán)結(jié)構(gòu)for循環(huán)使用教程全面講解
一、for 循環(huán)介紹
日常編碼過程中,我們常常需要重復(fù)執(zhí)行同一段代碼,這時(shí)我們就需要循環(huán)結(jié)構(gòu)來幫助我們控制程序的執(zhí)行順序。一個(gè)循環(huán)結(jié)構(gòu)會(huì)執(zhí)行循環(huán)體中的代碼直到結(jié)尾,然后回到開頭繼續(xù)執(zhí)行。 主流編程語言都提供了對(duì)循環(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 初始語句;條件表達(dá)式;結(jié)束語句{
循環(huán)體語句
}
- 初始語句:在循環(huán)開始前執(zhí)行一次的初始化操作,通常用于聲明計(jì)數(shù)器或迭代變量的初始值。
- 條件表達(dá)式:循環(huán)會(huì)在每次迭代之前檢查條件表達(dá)式,只有當(dāng)條件為真時(shí),循循環(huán)才會(huì)繼續(xù)執(zhí)行。如果條件為假,循環(huán)結(jié)束。
- 結(jié)束語句:在每次迭代之后執(zhí)行的操作,通常用于更新計(jì)數(shù)器或迭代變量的值。
以下是一個(gè)示例,演示了不同類型的for
循環(huán)基本用法:
var sum int for i := 0; i < 10; i++ { sum += i } println(sum)
這種 for 語句的使用形式是 Go 語言中 for 循環(huán)語句的進(jìn)形式。我們用一幅流程圖來直觀解釋一下上面這句 for 循環(huán)語句的組成部分,以及各個(gè)部分的執(zhí)行順序:
從圖中我們看到,經(jīng)典 for 循環(huán)語句有四個(gè)組成部分(分別對(duì)應(yīng)圖中的①~④)。我們按順序拆解一下這張圖。
圖中①對(duì)應(yīng)的組成部分執(zhí)行于循環(huán)體(③ )之前,并且在整個(gè) for 循環(huán)語句中僅會(huì)被執(zhí)行一次,它也被稱為循環(huán)前置語句。我們通常會(huì)在這個(gè)部分聲明一些循環(huán)體(③ )或循環(huán)控制條件(② )會(huì)用到的自用變量,也稱循環(huán)變量或迭代變量,比如這里聲明的整型變量 i。與 if 語句中的自用變量一樣,for 循環(huán)變量也采用短變量聲明的形式,循環(huán)變量的作用域僅限于 for 語句隱式代碼塊范圍內(nèi)。
圖中②對(duì)應(yīng)的組成部分,是用來決定循環(huán)是否要繼續(xù)進(jìn)行下去的條件判斷表達(dá)式。和 if 語句的一樣,這個(gè)用于條件判斷的表達(dá)式必須為布爾表達(dá)式,如果有多個(gè)判斷條件,我們一樣可以由邏輯操作符進(jìn)行連接。當(dāng)表達(dá)式的求值結(jié)果為 true 時(shí),代碼將進(jìn)入循環(huán)體(③)繼續(xù)執(zhí)行,相反則循環(huán)直接結(jié)束,循環(huán)體(③)與組成部分④都不會(huì)被執(zhí)行。
前面也多次提到了,圖中③對(duì)應(yīng)的組成部分是 for 循環(huán)語句的循環(huán)體。如果相關(guān)的判斷條件表達(dá)式求值結(jié)構(gòu)為 true 時(shí),循環(huán)體就會(huì)被執(zhí)行一次,這樣的一次執(zhí)行也被稱為一次迭代(Iteration)。在上面例子中,循環(huán)體執(zhí)行的動(dòng)作是將這次迭代中變量 i 的值累加到變量 sum 中。
圖中④對(duì)應(yīng)的組成部分會(huì)在每次循環(huán)體迭代之后執(zhí)行,也被稱為循環(huán)后置語句。這個(gè)部分通常用于更新 for 循環(huán)語句組成部分①中聲明的循環(huán)變量,比如在這個(gè)例子中,我們?cè)谶@個(gè)組成部分對(duì)循環(huán)變量 i 進(jìn)行加 1 操作。
2.2 省略初始值
for 循環(huán)的初始語句可以被忽略,但是必須要寫初始語句后面的分號(hào)
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
后添加一個(gè)條件表達(dá)式,滿足條件表達(dá)式時(shí)持續(xù)循環(huán),否則結(jié)束循環(huán)。
2.4 無限循環(huán)
無限循環(huán)是一種循環(huán)結(jié)構(gòu),它會(huì)一直執(zhí)行,而不受循環(huán)條件的限制,同時(shí)省略了初始語句,條件表達(dá)式,結(jié)束語句?;菊Z法格式如下:
for { 循環(huán)體語句 }
它的形式等價(jià)于:
for true { // 循環(huán)體代碼 }
或者等價(jià)于:
for ; ; { // 循環(huán)體代碼 }
在日常使用時(shí),建議你用它的最簡形式,也就是for {...}
,更加簡單。
舉個(gè)栗子:
for { fmt.Println("這是一個(gè)死循環(huán)!") }
無限循環(huán)通常在編程中用于執(zhí)行需要持續(xù)運(yùn)行的任務(wù),如服務(wù)器監(jiān)聽、事件處理等。
2.5 for 循環(huán)支持聲明多循環(huán)變量
Go 語言的 for 循環(huán)支持聲明多循環(huán)變量,并且可以應(yīng)用在循環(huán)體以及判斷條件中,比如下面就是一個(gè)使用多循環(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) }
在這個(gè)例子中,我們聲明了三個(gè)循環(huán)自用變量 i、j 和 k,它們共同參與了循環(huán)條件判斷與循環(huán)體的執(zhí)行。這段代碼的執(zhí)行流程解釋如下:
- 開始時(shí),
i
被初始化為 0,j
被初始化為 1,k
被初始化為 2,sum
被初始化為 0。 - 進(jìn)入循環(huán)。在每次迭代中,首先檢查三個(gè)條件:
i < 20
、j < 10
和k < 30
。只有在這三個(gè)條件都為真時(shí),循環(huán)才會(huì)繼續(xù)執(zhí)行。 - 在每次迭代中,計(jì)算
i + j + k
的和,并將結(jié)果添加到sum
中。 - 使用
println
函數(shù)打印sum
的當(dāng)前值。 - 繼續(xù)迭代,
i
、j
和k
分別增加 1、1 和 5。 - 重復(fù)步驟 2、3、4 直到其中一個(gè)條件不再滿足。在這種情況下,當(dāng)
i
大于或等于 20、j
大于或等于 10 或k
大于或等于 30 時(shí),循環(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) } // 手動(dòng)生成回車 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),它初始化一個(gè)名為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)中,這一行代碼用于打印每個(gè)乘法表達(dá)式。它使用fmt.Printf
函數(shù),打印了一個(gè)格式化的字符串,其中%d
是占位符,分別用x
、y
和x*y
的值替換。這將打印類似 "11=1 "、"12=2 "、"2*2=4 " 的格式。fmt.Println()
:在內(nèi)部循環(huán)結(jié)束后,使用fmt.Println
打印一個(gè)換行符,以將每行的輸出分開。
三、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ù)組、切片、映射等。
舉個(gè)例子,首先我們使用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
作為切片下標(biāo),逐一將切片中的元素讀取了出來。不過,這樣就有點(diǎ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)體保留了下來,其余組成部分都“不見”了。其實(shí)那幾部分已經(jīng)被融合到 for range 的語義中了。
具體來說,這里的 i
和 v
對(duì)應(yīng)的是for
語句形式中循環(huán)前置語句的循環(huán)變量,它們的初值分別為切片 sl 的第一個(gè)元素的下標(biāo)值和元素值。并且,隱含在 for range 語義中的循環(huán)控制條件判斷為:是否已經(jīng)遍歷完 sl 的所有元素,等價(jià)于i < len(sl)
這個(gè)布爾表達(dá)式。另外,每次迭代后,for range
會(huì)取出切片 sl
的下一個(gè)元素的下標(biāo)和值,分別賦值給循環(huán)變量 i
和 v
,這與 for
經(jīng)典形式下的循環(huán)后置語句執(zhí)行的邏輯是相同的。
3.3 for range 語句幾個(gè)常見的“變種”
3.3.1 省略value
有時(shí)候,您可能只對(duì)元素中的index
感興趣,而不需要值value
。在這種情況下,您可以省略值部分,只使用鍵。示例如下:
fruits := []string{"apple", "banana", "cherry"} for index := range fruits { fmt.Printf("Index: %d\n", index) }
3.3.2 省略 key
如果我們不關(guān)心元素下標(biāo),只關(guān)心元素值,那么我們可以用空標(biāo)識(shí)符替代代表下標(biāo)值的變量 i。這里一定要注意,這個(gè)空標(biāo)識(shí)符不能省略,否則就與上面形式一樣了,Go 編譯器將無法區(qū)分:
for _, v := range sl { // ... }
3.3.3 同時(shí)省略 key 和 value
如果我們既不關(guān)心元素下標(biāo)值,也不關(guān)心元素值,那是否能寫成下面這樣呢:
for _, _ = range sl { // ... }
這種形式在語法上沒有錯(cuò)誤,就是看起來不太優(yōu)雅。Go 在Go 1.4 版本中就提供了一種優(yōu)雅的等價(jià)形式,后續(xù)直接使用這種形式就好了:
for range sl { // ... }
四、for 循環(huán)常用操作
4.1 遍歷數(shù)組、切片——獲得索引和元素
在遍歷代碼中,key 和 value 分別代表切片的下標(biāo)及下標(biāo)對(duì)應(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
對(duì)于 string
類型來說,每次循環(huán)得到的 v
值是一個(gè) Unicode
字符碼點(diǎn),也就是 rune
類型值,而不是一個(gè)字節(jié),返回的第一個(gè)值 i 為該 Unicode
字符碼點(diǎn)的內(nèi)存編碼(UTF-8)
的第一個(gè)字節(jié)在字符串內(nèi)存序列中的位置。
4.3 遍歷map——獲得map的鍵和值
map 就是一個(gè)鍵值對(duì)(key-value)集合,最常見的對(duì) map 的操作,就是通過 key 獲取其對(duì)應(yīng)的 value 值。但有些時(shí)候,我們也要對(duì) map 這個(gè)集合進(jìn)行遍歷,這就需要 for 語句的支持了。
但在 Go 語言中,我們要對(duì) map 進(jìn)行循環(huán)操作,for range 是唯一的方法,for 經(jīng)典循環(huán)形式是不支持對(duì) map 類型變量的循環(huán)控制的。下面是通過 for range,對(duì)一個(gè) map 類型變量進(jìn)行循環(huán)操作的示例:
var m = map[string]int { "Rob" : 67, "Russ" : 39, "John" : 29, } for k, v := range m { println(k, v) }
運(yùn)行這個(gè)示例,我們會(huì)看到這樣的輸出結(jié)果:
John 29
Rob 67
Russ 39
通過輸出結(jié)果我們看到:for range
對(duì)于 map
類型來說,每次循環(huán),循環(huán)變量 k 和 v 分別會(huì)被賦值為 map 鍵值對(duì)集合中一個(gè)元素的 key
值和 value
值。而且,map
類型中沒有下標(biāo)的概念,通過 key
和 value
來循環(huán)操作 map 類型變量也就十分自然了。
4.4 遍歷通道(channel)——接收通道數(shù)據(jù)
除了可以針對(duì) string、數(shù)組 / 切片,以及 map 類型變量進(jìn)行循環(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è)計(jì)的原語,它用于多個(gè) Goroutine
之間的通信。當(dāng) channel
類型變量作為 for range 語句的迭代對(duì)象時(shí),for range
會(huì)嘗試從 channel
中讀取數(shù)據(jù),使用形式是這樣的:
var c = make(chan int) for v := range c { // ... }
在這個(gè)例子中,for range 每次從 channel
中讀取一個(gè)元素后,會(huì)把它賦值給循環(huán)變量 v,并進(jìn)入循環(huán)體。當(dāng) channel 中沒有數(shù)據(jù)可讀的時(shí)候, for range
循環(huán)會(huì)阻塞在對(duì) channel
的讀操作上。直到 channel
關(guān)閉時(shí),for range
循環(huán)才會(huì)結(jié)束,這也是 for range
循環(huán)與 channel
配合時(shí)隱含的循環(huán)判斷條件。
五、跳出循環(huán)與終止循環(huán)
5.1 continue 語句(繼續(xù)下次循環(huán))
5.1.1 continue 基本語法
首先,我們來看第一種場(chǎng)景。如果循環(huán)體中的代碼執(zhí)行到一半,要中斷當(dāng)前迭代,忽略此迭代循環(huán)體中的后續(xù)代碼,并回到 for 循環(huán)條件判斷,嘗試開啟下一次迭代,這個(gè)時(shí)候我們可以怎么辦呢?我們可以使用 continue
語句來應(yīng)對(duì)?;菊Z法如下:
for initialization; condition; update { // 循環(huán)體 if someCondition { continue } // 其他循環(huán)體的代碼 }
initialization
是初始化語句,通常用于初始化循環(huán)變量。condition
是循環(huán)條件,當(dāng)條件為真時(shí)繼續(xù)循環(huán),否則退出。update
是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。
帶標(biāo)簽的 continue
語句用于跳過當(dāng)前迭代中 if
語句中的 someCondition
滿足的部分,直接進(jìn)行下一次迭代。如果沒有標(biāo)簽,continue
將默認(rèn)跳過當(dāng)前循環(huán)的下一次迭代。
以下是一個(gè)示例,演示 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ì)循環(huán)遍歷切片中的元素,把值為奇數(shù)的元素相加,然后存儲(chǔ)在變量 sum 中。我們可以看到,在這個(gè)代碼的循環(huán)體中,如果我們判斷切片元素值為偶數(shù),就使用 continue 語句中斷當(dāng)前循環(huán)體的執(zhí)行,那么循環(huán)體下面的 sum += sl[i] 在這輪迭代中就會(huì)被忽略。代碼執(zhí)行流會(huì)直接來到循環(huán)后置語句i++,之后對(duì)循環(huán)條件表達(dá)式(i < len(sl))
進(jìn)行求值,如果為 true
,將再次進(jìn)入循環(huán)體,開啟新一次迭代。
5.1.2 帶標(biāo)簽的continue語句
Go 語言中的 continue
在 C 語言 continue
語義的基礎(chǔ)上又增加了對(duì) label
的支持。label
語句的作用,是標(biāo)記跳轉(zhuǎn)的目標(biāo)。帶標(biāo)簽的continue
語句的基本語法格式如下:
loopLabel: for initialization; condition; update { // 循環(huán)體 if someCondition { continue loopLabel } }
loopLabel
是一個(gè)用戶定義的標(biāo)簽(標(biāo)識(shí)符),用于標(biāo)記循環(huán)。initialization
是初始化語句,通常用于初始化循環(huán)變量。condition
是循環(huán)條件,當(dāng)條件為真時(shí)繼續(xù)循環(huán),否則退出。update
是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。
帶標(biāo)簽的continue
語句用于在嵌套循環(huán)中指定要跳過的循環(huán),其工作方式是:如果某個(gè)條件滿足,執(zhí)行continue loopLabel
,其中loopLabel
是要跳過的循環(huán)的標(biāo)簽,它將控制流轉(zhuǎn)移到帶有相應(yīng)標(biāo)簽的循環(huán)的下一次迭代。如果沒有指定標(biāo)簽,continue
將默認(rèn)跳過當(dāng)前循環(huán)的下一次迭代。
我們可以把上面的代碼改造為使用 label 的等價(jià)形式:
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 }
你可以看到,在這段代碼中,我們定義了一個(gè) label:loop
,它標(biāo)記的跳轉(zhuǎn)目標(biāo)恰恰就是我們的 for
循環(huán)。也就是說,我們?cè)谘h(huán)體中可以使用 continue+ loop label
的方式來實(shí)現(xiàn)循環(huán)體中斷,這與前面的例子在語義上是等價(jià)的。不過這里僅僅是一個(gè)演示,通常我們?cè)谶@樣非嵌套循環(huán)的場(chǎng)景中會(huì)直接使用不帶 label 的 continue
語句。
而帶 label 的 continue 語句,通常出現(xiàn)于嵌套循環(huán)語句中,被用于跳轉(zhuǎn)到外層循環(huán)并繼續(xù)執(zhí)行外層循環(huán)語句的下一個(gè)迭代,比如下面這段代碼:
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
是一個(gè)元素類型為[]int
的切片(二維切片),其每個(gè)元素切片中至多包含一個(gè)整型數(shù) 13。main 函數(shù)的邏輯就是在 sl 的每個(gè)元素切片中找到 13 這個(gè)數(shù)字,并輸出它的具體位置信息。
那這要怎么查找呢?一種好的實(shí)現(xiàn)方式就是,我們只需要在每個(gè)切片中找到 13,就不用繼續(xù)在這個(gè)切片的剩余元素中查找了。
我們用 for 經(jīng)典形式來實(shí)現(xiàn)這個(gè)邏輯。面對(duì)這個(gè)問題,我們要使用嵌套循環(huán),具體來說就是外層循環(huán)遍歷 sl 中的元素切片,內(nèi)層循環(huán)遍歷每個(gè)元素切片中的整型值。一旦內(nèi)層循環(huán)發(fā)現(xiàn) 13 這個(gè)數(shù)值,我們便要中斷內(nèi)層 for 循環(huán),回到外層 for 循環(huán)繼續(xù)執(zhí)行。
如果我們用不帶 label 的 continue 能不能完成這一功能呢?答案是不能。因?yàn)樗荒苤袛鄡?nèi)層循環(huán)的循環(huán)體,并繼續(xù)開啟內(nèi)層循環(huán)的下一次迭代。而帶 label 的 continue 語句是這個(gè)場(chǎng)景下的“最佳人選”,它會(huì)直接結(jié)束內(nèi)層循環(huán)的執(zhí)行,并回到外層循環(huán)繼續(xù)執(zhí)行。
這一行為就好比在外層循環(huán)放置并執(zhí)行了一個(gè)不帶 label
的 continue
語句。它會(huì)中斷外層循環(huán)中當(dāng)前迭代的執(zhí)行,執(zhí)行外層循環(huán)的后置語句(i++)
,然后再對(duì)外層循環(huán)的循環(huán)控制條件語句進(jìn)行求值,如果為 true
,就將繼續(xù)執(zhí)行外層循環(huán)的新一次迭代。
5.2 goto(跳轉(zhuǎn)到指定標(biāo)簽)
goto
語句通過標(biāo)簽進(jìn)行代碼間的無條件跳轉(zhuǎn)。goto
語句可以在快速跳出循環(huán)、避免重復(fù)退出上有一定的幫助。Go語言中使用goto
語句能簡化一些代碼的實(shí)現(xiàn)過程。 例如雙層嵌套的for循環(huán)要退出時(shí):
func main() { var breakFlag bool for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if j == 2 { // 設(shè)置退出標(biāo)簽 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è)置退出標(biāo)簽 goto breakTag } fmt.Printf("%v-%v\n", i, j) } } return // 標(biāo)簽 breakTag: fmt.Println("結(jié)束for循環(huán)") }
goto 是一種公認(rèn)的、難于駕馭的語法元素,應(yīng)用 goto
的代碼可讀性差、代碼難于維護(hù)還易錯(cuò)。雖然 Go 語言保留了 goto,在平常開發(fā)中,不推薦使用。
5.3 break(跳出循環(huán))
日常編碼中,我們還會(huì)遇到一些場(chǎng)景,在這些場(chǎng)景中,我們不僅要中斷當(dāng)前循環(huán)體迭代的進(jìn)行,還要同時(shí)徹底跳出循環(huán),終結(jié)整個(gè)循環(huán)語句的執(zhí)行。面對(duì)這樣的場(chǎng)景,continue
語句就不再適用了,Go 語言為我們提供了 break
語句來解決這個(gè)問題。
5.3.1 break基本語法
break
語句的基本語法如下:
for initialization; condition; update { // 循環(huán)體 if someCondition { break } // 其他循環(huán)體的代碼 }
initialization
是初始化語句,通常用于初始化循環(huán)變量。condition
是循環(huán)條件,當(dāng)條件為真時(shí)繼續(xù)循環(huán),否則退出。update
是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。
當(dāng)在循環(huán)中執(zhí)行 break
語句時(shí),它會(huì)立即終止當(dāng)前的循環(huán),無論條件是否滿足,然后將控制流傳遞到循環(huán)之后的代碼。
我們來看下面這個(gè)示例中 break 語句的應(yīng)用:
func main() { var sl = []int{5, 19, 6, 3, 8, 12} var firstEven int = -1 // 找出整型切片sl中的第一個(gè)偶數(shù) for i := 0; i < len(sl); i++ { if sl[i]%2 == 0 { firstEven = sl[i] break } } println(firstEven) // 6 }
這段代碼邏輯很容易理解,我們通過一個(gè)循環(huán)結(jié)構(gòu)來找出切片 sl 中的第一個(gè)偶數(shù),一旦找到就不需要繼續(xù)執(zhí)行后續(xù)迭代了。這個(gè)時(shí)候我們就通過 break 語句跳出了這個(gè)循環(huán)。
5.3.2 帶標(biāo)簽的break語法
和 continue
語句一樣,Go 也 break 語句增加了對(duì) label 的支持。而且,和前面 continue
語句一樣,如果遇到嵌套循環(huán),break
要想跳出外層循環(huán),用不帶 label 的 break 是不夠,因?yàn)椴粠?nbsp;label
的 break
僅能跳出其所在的最內(nèi)層循環(huán)。要想實(shí)現(xiàn)外層循環(huán)的跳出,我們還需給 break
加上 label
。所以,帶標(biāo)簽的 break
語句允許您從嵌套循環(huán)中跳出特定循環(huán),而不是默認(rèn)跳出當(dāng)前循環(huán)。帶標(biāo)簽的 break
語法如下:
loopLabel: for initialization; condition; update { // 循環(huán)體 if someCondition { break loopLabel } // 其他循環(huán)體的代碼 }
loopLabel
是用戶定義的標(biāo)簽(標(biāo)識(shí)符),用于標(biāo)記循環(huán)。initialization
是初始化語句,通常用于初始化循環(huán)變量。condition
是循環(huán)條件,當(dāng)條件為真時(shí)繼續(xù)循環(huán),否則退出。update
是在每次迭代后執(zhí)行的操作,通常用于更新循環(huán)變量。
當(dāng)帶標(biāo)簽的 break
語句執(zhí)行時(shí),它會(huì)終止帶有相應(yīng)標(biāo)簽的循環(huán),而不是默認(rèn)的當(dāng)前循環(huán)。
我們來看一個(gè)具體的例子:
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 } } } }
這個(gè)例子和我們前面的帶 label 的 continue 語句的例子很像,main 函數(shù)的邏輯就是,在 sl 這個(gè)二維切片中找到 38 這個(gè)數(shù)字,并輸出它的位置信息。整個(gè)二維切片中至多有一個(gè)值為 38 的元素,所以只要我們通過嵌套循環(huán)發(fā)現(xiàn)了 38,我們就不需要繼續(xù)執(zhí)行這個(gè)循環(huán)了。這時(shí),我們通過帶有 label 的 break 語句,就可以直接終結(jié)外層循環(huán),從而從復(fù)雜多層次的嵌套循環(huán)中直接跳出,避免不必要的算力資源的浪費(fèi)。
六、for 循環(huán)常見“坑”與避坑指南
for 語句的常見“坑”點(diǎn)通常和 for range 這個(gè)“語法糖”有關(guān)。雖然 for range 的引入提升了 Go 語言的表達(dá)能力,也簡化了循環(huán)結(jié)構(gòu)的編寫,但 for range
也不是“免費(fèi)的午餐”,在開發(fā)中,經(jīng)常會(huì)遇到一些問題,下面我們就來看看這些常見的問題。
6.1 循環(huán)變量的重用
我們前面說過,for range
形式的循環(huán)語句,使用短變量聲明的方式來聲明循環(huán)變量,循環(huán)體將使用這些循環(huán)變量實(shí)現(xiàn)特定的邏輯,但你在剛開始學(xué)習(xí)使用的時(shí)候,可能會(huì)發(fā)現(xiàn)循環(huán)變量的值與你之前的“預(yù)期”不符,比如下面這個(gè)例子:
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) }
這個(gè)示例是對(duì)一個(gè)整型切片進(jìn)行遍歷,并且在每次循環(huán)體的迭代中都會(huì)創(chuàng)建一個(gè)新的 Goroutine(Go 中的輕量級(jí)協(xié)程),輸出這次迭代的元素的下標(biāo)值與元素值。
現(xiàn)在我們繼續(xù)看這個(gè)例子,我們預(yù)期的輸出結(jié)果可能是這樣的:
0 1
1 2
2 3
3 4
4 5
那實(shí)際輸出真的是這樣嗎?我們實(shí)際運(yùn)行輸出一下:
4 5
4 5
4 5
4 5
4 5
我們看到,Goroutine 中輸出的循環(huán)變量,也就是 i 和 v 的值都是 for range 循環(huán)結(jié)束后的最終值,而不是各個(gè) Goroutine 啟動(dòng)時(shí)變量 i 和 v 的值,與我們最初的“預(yù)期”不符,這是為什么呢?
這是因?yàn)槲覀冏畛醯?ldquo;預(yù)期”本身就是錯(cuò)的。這里,很可能會(huì)被 for range 語句中的短聲明變量形式“迷惑”,簡單地認(rèn)為每次迭代都會(huì)重新聲明兩個(gè)新的變量 i 和 v。但事實(shí)上,這些循環(huán)變量在 for range 語句中僅會(huì)被聲明一次,且在每次迭代中都會(huì)被重用。
基于隱式代碼塊的規(guī)則,我們可以將上面的 for range 語句做一個(gè)等價(jià)轉(zhuǎn)換,這樣可以幫助你理解 for range 的工作原理。等價(jià)轉(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) }
通過等價(jià)轉(zhuǎn)換后的代碼,我們可以清晰地看到循環(huán)變量 i 和 v 在每次迭代時(shí)的重用。而 Goroutine 執(zhí)行的閉包函數(shù)引用了它的外層包裹函數(shù)中的變量 i、v,這樣,變量 i、v 在主 Goroutine 和新啟動(dòng)的 Goroutine 之間實(shí)現(xiàn)了共享,而 i, v 值在整個(gè)循環(huán)過程中是重用的,僅有一份。在 for range 循環(huán)結(jié)束后,i = 4, v = 5,因此各個(gè) Goroutine 在等待 3 秒后進(jìn)行輸出的時(shí)候,輸出的是 i, v 的最終值。
那么如何修改代碼,可以讓實(shí)際輸出和我們最初的預(yù)期輸出一致呢?我們可以為閉包函數(shù)增加參數(shù),并且在創(chuàng)建 Goroutine 時(shí)將參數(shù)與 i、v 的當(dāng)時(shí)值進(jìn)行綁定,看下面的修正代碼:
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í)行這個(gè)程序的輸出結(jié)果的行序,可能與我的不同,這是由 Goroutine 的調(diào)度所決定的。
6.2 參與循環(huán)的是 range 表達(dá)式的副本
在 for range 語句中,range 后面接受的表達(dá)式的類型可以是數(shù)組、指向數(shù)組的指針、切片、字符串,還有 map 和 channel(需具有讀權(quán)限)。我們以數(shù)組為例來看一個(gè)簡單的例子:
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) }
這個(gè)例子說的是對(duì)一個(gè)數(shù)組 a 的元素進(jìn)行遍歷操作,當(dāng)處理下標(biāo)為 0 的元素時(shí),我們修改了數(shù)組 a 的第二個(gè)和第三個(gè)元素的值,并且在每個(gè)迭代中,我們都將從 a 中取得的元素值賦值給新數(shù)組 r。我們期望這個(gè)程序會(huì)輸出如下結(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]
但實(shí)際運(yùn)行該程序的輸出結(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]
我們?cè)詾樵诘谝淮蔚^程,也就是 i = 0 時(shí),我們對(duì) a 的修改 (a[1] =12,a[2] = 13) 會(huì)在第二次、第三次迭代中被 v 取出,但從結(jié)果來看,v 取出的依舊是 a 被修改前的值:2 和 3。
為什么會(huì)是這種情況呢?原因就是參與 for range 循環(huán)的是 range 表達(dá)式的副本。也就是說,在上面這個(gè)例子中,真正參與循環(huán)的是 a 的副本,而不是真正的 a。
為了方便你理解,我們將上面的例子中的 for range 循環(huán),用一個(gè)等價(jià)的偽代碼形式重寫一下:
for i, v := range a' { //a'是a的一個(gè)值拷貝 if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v }
現(xiàn)在真相終于揭開了:這個(gè)例子中,每次迭代的都是從數(shù)組 a 的值拷貝 a’中得到的元素。a’是 Go 臨時(shí)分配的連續(xù)字節(jié)序列,與 a 完全不是一塊內(nèi)存區(qū)域。因此無論 a 被如何修改,它參與循環(huán)的副本 a’依舊保持原值,因此 v 從 a’中取出的仍舊是 a 的原值,而不是修改后的值。
那么應(yīng)該如何解決這個(gè)問題,讓輸出結(jié)果符合我們前面的預(yù)期呢?在 Go 中,大多數(shù)應(yīng)用數(shù)組的場(chǎng)景我們都可以用切片替代,這里我們也用切片來試試看:
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 表達(dá)式中,我們用了 a[:]替代了原先的 a,也就是將數(shù)組 a 轉(zhuǎn)換為一個(gè)切片,作為 range 表達(dá)式的循環(huán)對(duì)象。運(yùn)行這個(gè)修改后的例子,結(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ù)期終于一致了,顯然用切片能實(shí)現(xiàn)我們的要求。
那切片是如何做到的呢?切片在 Go 內(nèi)部表示為一個(gè)結(jié)構(gòu)體,由(array, len, cap)組成,其中 array 是指向切片對(duì)應(yīng)的底層數(shù)組的指針,len 是切片當(dāng)前長度,cap 為切片的最大容量。
所以,當(dāng)進(jìn)行 range 表達(dá)式復(fù)制時(shí),我們實(shí)際上復(fù)制的是一個(gè)切片,也就是表示切片的結(jié)構(gòu)體。表示切片副本的結(jié)構(gòu)體中的 array,依舊指向原切片對(duì)應(yīng)的底層數(shù)組,所以我們對(duì)切片副本的修改也都會(huì)反映到底層數(shù)組 a 上去。而 v 再從切片副本結(jié)構(gòu)體中 array 指向的底層數(shù)組中,獲取數(shù)組元素,也就得到了被修改后的元素值。
6.3 遍歷 map 中元素的隨機(jī)性
根據(jù)上面的講解,當(dāng) map 類型變量作為 range 表達(dá)式時(shí),我們得到的 map 變量的副本與原變量指向同一個(gè) map。如果我們?cè)谘h(huán)的過程中,對(duì) map
進(jìn)行了修改,那么這樣修改的結(jié)果是否會(huì)影響后續(xù)迭代呢?這個(gè)結(jié)果和我們遍歷 map 一樣,具有隨機(jī)性。
比如我們來看下面這個(gè)例子,在 map 循環(huán)過程中,當(dāng) counter 值為 0 時(shí),我們刪除了變量 m 中的一個(gè)元素:
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ù)運(yùn)行這個(gè)例子多次,會(huì)得到兩個(gè)不同的結(jié)果。當(dāng) k="tony"作為第一個(gè)迭代的元素時(shí),我們將得到如下結(jié)果:
tony 21
tom 22
jim 23
counter is 3
否則,我們得到的結(jié)果是這樣的:
tom 22
jim 23
counter is 2
如果我們?cè)卺槍?duì) map 類型的循環(huán)體中,新創(chuàng)建了一個(gè) map 元素項(xiàng),那這項(xiàng)元素可能出現(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)
這個(gè)例子的執(zhí)行結(jié)果也會(huì)有兩個(gè):
tony 21
tom 22
jim 23
lucy 24
counter is 4
或:
tony 21
tom 22
jim 23
counter is 3
考慮到上述這種隨機(jī)性,我們?nèi)粘>幋a遇到遍歷 map 的同時(shí),還需要對(duì) map 進(jìn)行修改的場(chǎng)景的時(shí)候,要格外小心。
以上就是Go 循環(huán)結(jié)構(gòu)for循環(huán)使用全面講解的詳細(xì)內(nèi)容,更多關(guān)于Go for循環(huán)結(jié)構(gòu)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go簡單實(shí)現(xiàn)協(xié)程池的實(shí)現(xiàn)示例
本文主要介紹了Go簡單實(shí)現(xiàn)協(xié)程池的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06如何使用工具自動(dòng)監(jiān)測(cè)SSL證書有效期并發(fā)送提醒郵件
本文介紹了如何開發(fā)一個(gè)工具,用于每日檢測(cè)SSL證書剩余有效天數(shù)并通過郵件發(fā)送提醒,工具基于命令行,通過SMTP協(xié)議發(fā)送郵件,需配置SMTP連接信息,本文還提供了配置文件樣例及代碼實(shí)現(xiàn),幫助用戶輕松部署和使用該工具2024-10-10Go語言使用protojson庫實(shí)現(xiàn)Protocol Buffers與JSON轉(zhuǎn)換
本文主要介紹Google開源的工具庫Protojson庫如何Protocol Buffers與JSON進(jìn)行轉(zhuǎn)換,以及和標(biāo)準(zhǔn)庫encoding/json的性能對(duì)比,需要的朋友可以參考下2023-09-09Golang 探索對(duì)Goroutine的控制方法(詳解)
下面小編就為大家分享一篇Golang 探索對(duì)Goroutine的控制方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12VSCode Golang dlv調(diào)試數(shù)據(jù)截?cái)鄦栴}及處理方法
這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截?cái)鄦栴},本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06