Go語言 基本流程控制語法

2023-02-16 17:36 更新

Go中的流程控制語句和其它很多流行語言很類似,但是也有不少區(qū)別。 本篇文章將列出所有這些相似點和不同點。

Go中的流程控制語句簡單介紹

Go語言中有三種基本的流程控制代碼塊:

  • if-else條件分支代碼塊;
  • for循環(huán)代碼塊;
  • switch-case多條件分支代碼塊。

Go中另外還有幾種和特定種類的類型相關(guān)的流程控制代碼塊:

  • 容器類型相關(guān)的for-range循環(huán)代碼塊。
  • 接口類型相關(guān)的type-switch多條件分支代碼塊。
  • 通道類型相關(guān)的select-case多分支代碼塊。

和很多其它流行語言一樣,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-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

循環(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)流程控制中,如果InitSimpleStatementPostSimpleStatement兩部分同時被省略(可將它們視為空語句),則和它們相鄰的兩個分號也可被省略。 這樣的形式被稱為只有條件表達式的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)。 下面這段代碼同樣逐行打印出09十個數(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-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")
}

goto跳轉(zhuǎn)語句和跳轉(zhuǎn)標(biāo)簽聲明

和很多其它語言一樣,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:
}

包含跳轉(zhuǎn)標(biāo)簽的break和continue語句

一個?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)簽名的breakcontinue語句的例子。

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()
	}
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號