Go中的流程控制語句和其它很多流行語言很類似,但是也有不少區(qū)別。 本篇文章將列出所有這些相似點和不同點。
Go語言中有三種基本的流程控制代碼塊:
Go中另外還有幾種和特定種類的類型相關(guān)的流程控制代碼塊:
和很多其它流行語言一樣,Go也支持break、continue和goto等跳轉(zhuǎn)語句。 另外,Go還支持一個特有的fallthrough跳轉(zhuǎn)語句。
Go所支持的六種流程控制代碼塊中,除了if-else條件分支代碼塊,其它五種稱為可跳出代碼塊。 我們可以在一個可跳出代碼塊中使用break語句以跳出此代碼塊。
我們可以在for和for-range兩種循環(huán)代碼塊中使用continue語句提前結(jié)束一個循環(huán)步。 除了這兩種循環(huán)代碼塊,其它四種代碼塊稱為分支代碼塊。
請注意,上面所提及的每種流程控制的一個分支都屬于一條語句。這樣的語句常常會包含很多子語句。
上面所提及的流程控制語句都屬于狹義上的流程控制語句。 下一篇文章中將要介紹的協(xié)程、延遲函數(shù)調(diào)用、以及恐慌和恢復(fù),以及今后要介紹的并發(fā)同步技術(shù)屬于廣義上的流程控制語句。
本文余下的部分將只解釋三種基本的流程控制語句和各種代碼跳轉(zhuǎn)語句。其它上面提及的語句將在后面其它文章中逐漸介紹。
一個if-else條件分支控制代碼塊的完整形式如下:
if InitSimpleStatement; Condition {
// do something
} else {
// do something
}
?if
?和?else
?是兩個關(guān)鍵字。 和很多其它編程語言一樣,?else
?分支是可選的。
在一個if-else條件分支控制代碼塊中,
InitSimpleStatement
?部分是可選的,如果它沒被省略掉,則它必須為一條簡單語句。 如果它被省略掉,它可以被視為一條空語句(簡單語句的一種)。 在實際編程中,?InitSimpleStatement
?常常為一條變量短聲明語句。Condition
?必須為一個結(jié)果為布爾值的表達式(它被稱為條件表達式)。 ?Condition
?部分可以用一對小括號括起來,但大多數(shù)情況下不需要。注意,我們不能用一對小括號將?InitSimpleStatement
?和?Condition
?兩部分括在一起。
在執(zhí)行一個?if-else
?條件分支控制代碼塊中,如果?InitSimpleStatement
?這條語句沒有被省略,則此條語句將被率先執(zhí)行。 如果?InitSimpleStatement
?被省略掉,則其后跟隨的分號?;
?也可一塊兒被省略。
每個?if-else
?流程控制包含一個隱式代碼塊,一個?if
?分支顯式代碼塊和一個可選的?else
?分支代碼塊。 這兩個分支代碼塊內(nèi)嵌在這個隱式代碼塊中。 在程序運行中,如果?Condition
?條件表達式的估值結(jié)果為?true
?,則?if
?分支式代碼塊將被執(zhí)行;否則,?else
?分支代碼塊將被執(zhí)行。
一個例子:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
if n := rand.Int(); n%2 == 0 {
fmt.Println(n, "是一個偶數(shù)。")
} else {
fmt.Println(n, "是一個奇數(shù)。")
}
n := rand.Int() % 2 // 此n不是上面聲明的n
if n % 2 == 0 {
fmt.Println("一個偶數(shù)。")
}
if ; n % 2 != 0 {
fmt.Println("一個奇數(shù)。")
}
}
如果?InitSimpleStatement
?語句是一個變量短聲明語句,則在此語句中聲明的變量被聲明在外層的隱式代碼塊中。
可選的?else
?分支代碼塊一般情況下必須為顯式的,但是如果此分支為另外一個?if-else
?塊,則此分支代碼塊可以是隱式的。
另一個例子:
package main
import (
"fmt"
"time"
)
func main() {
if h := time.Now().Hour(); h < 12 {
fmt.Println("現(xiàn)在為上午。")
} else if h > 19 {
fmt.Println("現(xiàn)在為晚上。")
} else {
fmt.Println("現(xiàn)在為下午。")
// 左h是一個新聲明的變量,右h已經(jīng)在上面聲明了。
h := h
// 剛聲明的h遮掩了上面聲明的h。
_ = h
}
// 上面聲明的兩個h在此處都不可見。
}
for
循環(huán)代碼塊的完整形式如下:
for InitSimpleStatement; Condition; PostSimpleStatement {
// do something
}
其中?for
?是一個關(guān)鍵字。
在一個for循環(huán)代碼塊中,
InitSimpleStatement
?(初始化語句)和?PostSimpleStatement
?(步尾語句)兩個部分必須均為簡單語句,并且?PostSimpleStatement
?不能為一個變量短聲明語句。Condition
?必須為一個結(jié)果為布爾值的表達式(它被稱為條件表達式)。所有這三個剛提到的部分都是可選的。和很多其它流行語言不同,在Go中上述三部分不能用小括號括在一起。
每個?for
?流程控制包括至少兩個子代碼塊。 其中一個是隱式的,另一個是顯式的(花括號起始和終止的部分,又稱循環(huán)體)。 此顯式代碼塊內(nèi)嵌在隱式代碼塊之中。
在一個?for
?循環(huán)流程控制中,初始化語句(?InitSimpleStatement
?)將被率先執(zhí)行,并且只會被執(zhí)行一次。
在每個循環(huán)步的開始,?Condition
?條件表達式將被估值。如果估值結(jié)果為?false
?,則循環(huán)立即結(jié)束;否則循環(huán)體(即顯式代碼塊)將被執(zhí)行。
在每個循環(huán)步的結(jié)尾,步尾語句(?PostSimpleStatement
?)將被執(zhí)行。
下面是一個使用for循環(huán)流程控制的例子。此程序?qū)⒅鹦写蛴〕?span style="background-color: rgb(249, 242, 244); color: rgb(199, 37, 78); font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: inherit; white-space: nowrap;">0到9十個數(shù)字。
for i := 0; i < 10; i++ {
fmt.Println(i)
}
在一個for循環(huán)流程控制中,如果InitSimpleStatement和PostSimpleStatement兩部分同時被省略(可將它們視為空語句),則和它們相鄰的兩個分號也可被省略。 這樣的形式被稱為只有條件表達式的for循環(huán)。只有條件表達式的for循環(huán)和很多其它語言中的while循環(huán)類似。
var i = 0
for ; i < 10; {
fmt.Println(i)
i++
}
for i < 20 {
fmt.Println(i)
i++
}
在一個for循環(huán)流程控制中,如果條件表達式部分被省略,則編譯器視其為true。
for i := 0; ; i++ { // 等價于:for i := 0; true; i++ {
if i >= 10 {
break
}
fmt.Println(i)
}
// 下面這幾個循環(huán)是等價的。
for ; true; {
}
for true {
}
for ; ; {
}
for {
}
在一個for循環(huán)流程控制中,如果初始化語句InitSimpleStatement是一個變量短聲明語句,則在此語句中聲明的變量被聲明在外層的隱式代碼塊中。 我們可以在內(nèi)嵌的循環(huán)體(顯式代碼塊)中聲明同名變量來遮擋在InitSimpleStatement中聲明的變量。 比如下面的代碼打印出012,而不是0。
for i := 0; i < 3; i++ {
fmt.Print(i)
i := i // 這里聲明的變量i遮擋了上面聲明的i。
// 右邊的i為上面聲明的循環(huán)變量i。
i = 10 // 新聲明的i被更改了。
_ = i
}
一條break語句可以用來提前跳出包含此break語句的最內(nèi)層for循環(huán)。 下面這段代碼同樣逐行打印出0到9十個數(shù)字。
i := 0
for {
if i >= 10 {
break
}
fmt.Println(i)
i++
}
一條continue語句可以被用來提前結(jié)束包含此continue語句的最內(nèi)層for循環(huán)的當(dāng)前循環(huán)步(步尾語句仍將得到執(zhí)行)。 比如下面這段代碼將打印出13579。
for i := 0; i < 10; i++ {
if i % 2 == 0 {
continue
}
fmt.Print(i)
}
?switch-case
?流程控制代碼塊是另外一種多分支代碼塊。
一個switch-case流程控制代碼塊的完整形式為:
switch InitSimpleStatement; CompareOperand0 {
case CompareOperandList1:
// do something
case CompareOperandList2:
// do something
...
case CompareOperandListN:
// do something
default:
// do something
}
其中?switch
?、?case
?和?default
?是三個關(guān)鍵字。
在一個switch-case流程控制代碼塊中,
InitSimpleStatement
?部分必須為一條簡單語句,它是可選的。CompareOperand0
?部分必須為一個表達式(如果它沒被省略的話,見下)。 此表達式的估值結(jié)果總是被視為一個類型確定值。如果它是一個類型不確定值,則它被視為類型為它的默認(rèn)類型的類型確定值。 因為這個原因,此表達式不能為類型不確定的?nil
?值。 ?CompareOperand0
?常被稱為switch表達式。CompareOperandListX
?部分(?X
?表示?1
?到?N
?)必須為一個用(英文)逗號分隔開來的表達式列表。 其中每個表達式都必須能和?CompareOperand0
?表達式進行比較。 每個這樣的表達式常被稱為case表達式。 如果其中case表達式是一個類型不確定值,則它必須能夠自動隱式轉(zhuǎn)化為對應(yīng)的switch表達式的類型,否則編譯將失敗。每個?case CompareOperandListX:
?部分和?default:
?之后形成了一個隱式代碼塊。 每個這樣的隱式代碼塊和它對應(yīng)的?case CompareOperandListX:
?或者?default:
?形成了一個分支。 每個分支都是可選的。
每個?switch-case
?流程控制代碼塊中最多只能有一個?default
?分支(默認(rèn)分支)。
除了剛提到的分支代碼塊,每個?switch-case
?流程控制至少包括其它兩個代碼塊。 其中一個是隱式的,另一個是顯式的。此顯式的代碼塊內(nèi)嵌在隱式的代碼塊之中。 所有的分支代碼塊都內(nèi)嵌在此顯式代碼塊之中(因此也間接內(nèi)嵌在剛提及的隱式代碼塊中)。
?switch-case
?代碼塊屬于可跳出流程控制。 ?break
?可以使用在一個?switch-case
?流程控制的任何分支代碼塊之中以提前跳出此?switch-case
?流程控制。
當(dāng)一個?switch-case
?流程控制被執(zhí)行到的時候,其中的簡單語句?InitSimpleStatement
?將率先被執(zhí)行。 隨后switch表達式?CompareOperand0
?將被估值(僅一次)。上面已經(jīng)提到,此估值結(jié)果一定為一個類型確定值。 然后此結(jié)果值將從上到下從左到右和各個?CompareOperandListX
?表達式列表中的各個case表達式逐個依次比較(使用?==
?運算符)。 一旦發(fā)現(xiàn)某個表達式和?CompareOperand0
?相等,比較過程停止并且此表達式對應(yīng)的分支代碼塊將得到執(zhí)行。 如果沒有任何一個表達式和?CompareOperand0
?相等,則?default
?默認(rèn)分支將得到執(zhí)行(如果此分支存在的話)。
一個switch-case流程控制的例子:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
switch n := rand.Intn(100); n%9 {
case 0:
fmt.Println(n, "is a multiple of 9.")
// 和很多其它語言不一樣,程序不會自動從一個
// 分支代碼塊跳到下一個分支代碼塊去執(zhí)行。
// 所以,這里不需要一個break語句。
case 1, 2, 3:
fmt.Println(n, "mod 9 is 1, 2 or 3.")
break // 這里的break語句可有可無的,效果
// 是一樣的。執(zhí)行不會跳到下一個分支。
case 4, 5, 6:
fmt.Println(n, "mod 9 is 4, 5 or 6.")
// case 6, 7, 8:
// 上一行可能編譯不過,因為6和上一個case中的
// 6重復(fù)了。是否能編譯通過取決于具體編譯器實現(xiàn)。
default:
fmt.Println(n, "mod 9 is 7 or 8.")
}
}
在上例中,?rand.Intn
?函數(shù)將返回一個從?0
?到所傳實參之間類型為?int
?的隨機數(shù)。
注意,編譯器可能會不允許一個?switch-case
?流程控制中有任何兩個case表達式可以在編譯時刻確定相等。 比如,當(dāng)前的官方標(biāo)準(zhǔn)編譯器(1.19版本)認(rèn)為上例中的?case 6, 7, 8
?一行是不合法的(如果此行未被注釋掉)。但是其它編譯器未必這么認(rèn)為。 事實上,當(dāng)前的官方標(biāo)準(zhǔn)編譯器允許重復(fù)的布爾case表達式在同一個switch-case流程控制中出現(xiàn), 而gccgo(v8.2)允許重復(fù)的布爾和字符串類型的case表達式在同一個?switch-case
?流程控制中出現(xiàn)。
上面的例子中的前兩個case分支中的注釋已經(jīng)解釋了,和很多其它語言不一樣,每個分支代碼塊的結(jié)尾不需要一條break語句就可以自動跳出當(dāng)前的switch-case流程控制。 那么如何讓執(zhí)行從一個case分支代碼塊的結(jié)尾跳入下一個分支代碼塊?Go提供了一個fallthrough關(guān)鍵字來完成這個任務(wù)。 比如,在下面的例子中,所有的分支代碼塊都將得到執(zhí)行(從上到下)。
rand.Seed(time.Now().UnixNano())
switch n := rand.Intn(100) % 5; n {
case 0, 1, 2, 3, 4:
fmt.Println("n =", n)
fallthrough // 跳到下個代碼塊
case 5, 6, 7, 8:
// 一個新聲明的n,它只在當(dāng)前分支代碼快內(nèi)可見。
n := 99
fmt.Println("n =", n) // 99
fallthrough
default:
// 下一行中的n和第一個分支中的n是同一個變量。
// 它們均為switch表達式"n"。
fmt.Println("n =", n)
}
請注意:
fallthrough
?語句必須為一個分支代碼塊中的最后一條語句。fallthrough
?語句不能出現(xiàn)在一個?switch-case
?流程控制中的最后一個分支代碼塊中。比如,下面代碼的幾個fallthrough使用是不合法的。
switch n := rand.Intn(100) % 5; n {
case 0, 1, 2, 3, 4:
fmt.Println("n =", n)
// 此整個if代碼塊為當(dāng)前分支中的最后一條語句
if true {
fallthrough // error: 不是當(dāng)前分支中的最后一條語句
}
case 5, 6, 7, 8:
n := 99
fallthrough // error: 不是當(dāng)前分支中的最后一條語句
_ = n
default:
fmt.Println(n)
fallthrough // error: 不能出現(xiàn)在最后一個分支中
}
一個?switch-case
?流程控制中的?InitSimpleStatement
?語句和?CompareOperand0
?表達式都是可選的。 如果?CompareOperand0
?表達式被省略,則它被認(rèn)為類型為?bool
?類型的?true
?值。 如果?InitSimpleStatement
?語句被省略,其后的分號也可一并被省略。
上面已經(jīng)提到了一個switch-case流程控制中的所有分支都可以被省略,所以下面的所有流程控制代碼塊都是合法的,它們都可以被視為空操作。
switch n := 5; n {
}
switch 5 {
}
switch _ = 5; {
}
switch {
}
上例中的后兩個switch-case流程控制中的CompareOperand0表達式都為bool類型的true值。 同理,下例中的代碼將打印出hello。
switch { // <=> switch true {
case true: fmt.Println("hello")
default: fmt.Println("bye")
}
Go中另外一個和其它語言的顯著不同點是default分支不必一定為最后一個分支。 比如,下面的三個switch-case流程控制代碼塊是相互等價的。
switch n := rand.Intn(3); n {
case 0: fmt.Println("n == 0")
case 1: fmt.Println("n == 1")
default: fmt.Println("n == 2")
}
switch n := rand.Intn(3); n {
default: fmt.Println("n == 2")
case 0: fmt.Println("n == 0")
case 1: fmt.Println("n == 1")
}
switch n := rand.Intn(3); n {
case 0: fmt.Println("n == 0")
default: fmt.Println("n == 2")
case 1: fmt.Println("n == 1")
}
和很多其它語言一樣,Go也支持?goto
?跳轉(zhuǎn)語句。 在一個?goto
?跳轉(zhuǎn)語句中,?goto
?關(guān)鍵字后必須跟隨一個表明跳轉(zhuǎn)到何處的跳轉(zhuǎn)標(biāo)簽。 我們使用?LabelName:
?這樣的形式來聲明一個名為?LabelName
?的跳轉(zhuǎn)標(biāo)簽,其中?LabelName
?必須為一個標(biāo)識符。 一個不為空標(biāo)識符的跳轉(zhuǎn)標(biāo)簽聲明后必須被使用至少一次。
一條跳轉(zhuǎn)標(biāo)簽聲明之后必須立即跟隨一條語句。 如果此聲明的跳轉(zhuǎn)標(biāo)簽使用在一條?goto
?語句中,則當(dāng)此條?goto
?語句被執(zhí)行的時候,執(zhí)行將跳轉(zhuǎn)到此跳轉(zhuǎn)標(biāo)簽聲明后跟隨的語句。
一個跳轉(zhuǎn)標(biāo)簽必須聲明在一個函數(shù)體內(nèi),此跳轉(zhuǎn)標(biāo)簽的使用可以在此跳轉(zhuǎn)標(biāo)簽的聲明之后或者之前,但是此跳轉(zhuǎn)標(biāo)簽的使用不能出現(xiàn)在此跳轉(zhuǎn)標(biāo)簽聲明所處的最內(nèi)層代碼塊之外。
下面這個例子使用跳轉(zhuǎn)標(biāo)簽聲明和goto跳轉(zhuǎn)語句來實現(xiàn)了一個循環(huán):
package main
import "fmt"
func main() {
i := 0
Next: // 跳轉(zhuǎn)標(biāo)簽聲明
fmt.Println(i)
i++
if i < 5 {
goto Next // 跳轉(zhuǎn)
}
}
上面剛提到了一個跳轉(zhuǎn)標(biāo)簽的使用不能出現(xiàn)在此跳轉(zhuǎn)標(biāo)簽聲明所處的最內(nèi)層代碼塊之外,所以下面的代碼片段中的跳轉(zhuǎn)標(biāo)簽使用都是不合法的。
package main
func main() {
goto Label1 // error
{
Label1:
goto Label2 // error
}
{
Label2:
}
}
另外要注意的一點是,如果一個跳轉(zhuǎn)標(biāo)簽聲明在某個變量的作用域內(nèi),則此跳轉(zhuǎn)標(biāo)簽的使用不能出現(xiàn)在此變量的聲明之前。 關(guān)于變量的作用域,請閱讀后面的文章代碼塊和作用域
下面這個程序編譯不通過:
package main
import "fmt"
func main() {
i := 0
Next:
if i >= 5 {
// error: goto Exit jumps over declaration of k
goto Exit
}
k := i + i
fmt.Println(k)
i++
goto Next
Exit: // 此標(biāo)簽聲明在k的作用域內(nèi),但
// 它的使用在k的作用域之外。
}
剛提到的這條規(guī)則可能會在今后放寬。 目前,有兩種途徑可以對上面的程序略加修改以使之編譯通過。
第一種途徑是縮小變量k的作用域:
func main() {
i := 0
Next:
if i >= 5 {
goto Exit
}
// 創(chuàng)建一個顯式代碼塊以縮小k的作用域。
{
k := i + i
fmt.Println(k)
}
i++
goto Next
Exit:
}
第二種途徑是放大變量k的作用域:
func main() {
var k int // 將變量k的聲明移到此處。
i := 0
Next:
if i >= 5 {
goto Exit
}
k = i + i
fmt.Println(k)
i++
goto Next
Exit:
}
一個?goto
?語句必須包含一個跳轉(zhuǎn)標(biāo)簽名。 一個?break
?或者?continue
?語句也可以包含一個跳轉(zhuǎn)標(biāo)簽名,但此跳轉(zhuǎn)標(biāo)簽名是可選的。 包含跳轉(zhuǎn)標(biāo)簽名的?break
?語句一般用于跳出外層的嵌套可跳出流程控制代碼塊。 包含跳轉(zhuǎn)標(biāo)簽名的?continue
?語句一般用于提前結(jié)束外層的嵌套循環(huán)流程控制代碼塊的當(dāng)前循環(huán)步。
如果一條?break
?語句中包含一個跳轉(zhuǎn)標(biāo)簽名,則此跳轉(zhuǎn)標(biāo)簽必須剛好聲明在一個包含此?break
?語句的可跳出流程控制代碼塊之前。 我們可以把此跳轉(zhuǎn)標(biāo)簽名看作是其后緊跟隨的可跳出流程控制代碼塊的名稱。 此?break
?語句將立即結(jié)束此可跳出流程控制代碼塊的執(zhí)行。
如果一條?continue
?語句中包含一個跳轉(zhuǎn)標(biāo)簽名,則此跳轉(zhuǎn)標(biāo)簽必須剛好聲明在一個包含此?continue
?語句的循環(huán)流程控制代碼塊之前。 我們可以把此跳轉(zhuǎn)標(biāo)簽名看作是其后緊跟隨的循環(huán)流程控制代碼塊的名稱。 此?continue
?語句將提前結(jié)束此循環(huán)流程控制代碼塊的當(dāng)前步的執(zhí)行。
下面是一個使用了包含跳轉(zhuǎn)標(biāo)簽名的break和continue語句的例子。
package main
import "fmt"
func FindSmallestPrimeLargerThan(n int) int {
Outer:
for n++; ; n++{
for i := 2; ; i++ {
switch {
case i * i > n:
break Outer
case n % i == 0:
continue Outer
}
}
}
return n
}
func main() {
for i := 90; i < 100; i++ {
n := FindSmallestPrimeLargerThan(i)
fmt.Print("最小的比", i, "大的素數(shù)為", n)
fmt.Println()
}
}
更多建議: