Skip to content

Latest commit

 

History

History
961 lines (731 loc) · 30.6 KB

File metadata and controls

961 lines (731 loc) · 30.6 KB

三、Go 控制流程

Go 借用了 C 语言家族中的一些控制流语法。它支持所有预期的控制结构,包括if...elseswitchfor循环,甚至goto。然而,whiledo...while声明明显缺失。本章中的以下主题将研究 Go 的控制流元素,其中一些您可能已经熟悉,而另一些则带来了其他语言中没有的一组新功能:

  • if声明
  • switch声明
  • 类型Switch
  • for声明

if 语句

Go 中的[T0]语句借用了其他类似 C 语言的基本结构形式。当[T1]关键字后面的布尔表达式计算为[T2]时,该语句有条件地执行代码块,如以下缩略程序所示,该程序显示有关世界货币的信息:

import "fmt" 

type Currency struct { 
  Name    string 
  Country string 
  Number  int 
} 

var CAD = Currency{ 
    Name: "Canadian Dollar",  
    Country: "Canada",  
    Number: 124} 

var FJD = Currency{ 
    Name: "Fiji Dollar",  
    Country: "Fiji",  
    Number: 242} 

var JMD = Currency{ 
    Name: "Jamaican Dollar",  
    Country: "Jamaica",  
    Number: 388} 

var USD = Currency{ 
    Name: "US Dollar",  
    Country: "USA",  
    Number: 840} 

func main() { 
  num0 := 242 
  if num0 > 100 || num0 < 900 { 
    fmt.Println("Currency: ", num0) 
    printCurr(num0) 
  } else { 
    fmt.Println("Currency unknown") 
  } 

  if num1 := 388; num1 > 100 || num1 < 900 { 
    fmt.Println("Currency:", num1) 
    printCurr(num1) 
  } 
} 

func printCurr(number int) { 
  if CAD.Number == number { 
    fmt.Printf("Found: %+v\n", CAD) 
  } else if FJD.Number == number { 
    fmt.Printf("Found: %+v\n", FJD) 
  } else if JMD.Number == number { 
    fmt.Printf("Found: %+v\n", JMD) 
  } else if USD.Number == number { 
    fmt.Printf("Found: %+v\n", USD) 
  } else { 
    fmt.Println("No currency found with number", number) 
  } 
} 

golang.fyi/ch03/ifstmt.go

Go 中的[T0]语句与其他语言类似。但是,它在强制执行新规则的同时,也摆脱了一些语法规则:

  • 测试表达式周围的括号不是必需的。虽然下面的if语句将被编译,但它不是惯用语句:

          if (num0 > 100 || num0 < 900) { 
            fmt.Println("Currency: ", num0) 
            printCurr(num0) 
          } 
  • 改为使用以下内容:

          if num0 > 100 || num0 < 900 { 
            fmt.Println("Currency: ", num0) 
            printCurr(num0) 
          } 
  • 代码块始终需要大括号。以下代码段将不会编译:

          if num0 > 100 || num0 < 900 printCurr(num0) 
  • 但是,这将编译:

          if num0 > 100 || num0 < 900 {printCurr(num0)} 
  • 然而,在多行上编写[T0]语句是惯用的(无论语句块多么简单)。这鼓励了良好的风格和清晰度。以下代码段将编译,不会出现任何问题:

          if num0 > 100 || num0 < 900 {printCurr(num0)} 
  • 但是,语句的首选惯用布局是使用多行,如下所示:

          if num0 > 100 || num0 < 900 { 
            printCurr(num0) 
          }
  • if语句可以包括可选的else块,当if块中的表达式计算为false时执行。else块中的代码必须用多行大括号括起来,如以下代码片段所示:

          if num0 > 100 || num0 < 900 { 
            fmt.Println("Currency: ", num0) 
            printCurr(num0) 
          } else { 
            fmt.Println("Currency unknown") 
          } 
  • else关键字后面可能紧跟着另一条if语句,形成if...else...if链,如前面列出的源代码

          if CAD.Number == number { 
            fmt.Printf("Found: %+v\n", CAD) 
          } else if FJD.Number == number { 
            fmt.Printf("Found: %+v\n", FJD) 
          } 

    中的函数printCurr()中所用

if...else...if语句链可以根据需要增长,也可以通过可选的else语句终止,以表示所有其他未测试的条件。同样,这是在printCurr()函数中完成的,该函数使用if...else...if块测试四个条件。最后,它包括一个else语句块,用于捕获任何其他未测试的条件:

func printCurr(number int) { 
  if CAD.Number == number { 
    fmt.Printf("Found: %+v\n", CAD) 
  } else if FJD.Number == number { 
    fmt.Printf("Found: %+v\n", FJD) 
  } else if JMD.Number == number { 
    fmt.Printf("Found: %+v\n", JMD) 
  } else if USD.Number == number { 
    fmt.Printf("Found: %+v\n", USD) 
  } else { 
    fmt.Println("No currency found with number", number) 
  } 
}

然而,在 Go 中,编写如此深刻的[T0]代码块的惯用、更简洁的方法是使用无表情的[T1]语句。这将在后面的开关语句部分中介绍。

if 语句初始化

if语句支持复合语法,其中测试表达式前面有一条初始化语句。在运行时,在计算测试表达式之前执行初始化,如本代码段(来自前面列出的程序)所示:

if num1 := 388; num1 > 100 || num1 < 900 { 
  fmt.Println("Currency:", num1) 
  printCurr(num1) 
}  

初始化语句遵循普通变量声明和初始化规则。初始化变量的作用域绑定到if语句块,超过该语句块,变量将无法访问。这是 Go 中常用的习惯用法,本章介绍的其他流控制结构也支持这一习惯用法。

开关语句

Go 还支持类似于其他语言(如 C 或 Java)中的[T0]语句。Go 中的switch语句通过对case子句中的值或表达式求值来实现多路分支,如以下缩写源代码所示:

import "fmt" 

type Curr struct { 
  Currency string 
  Name     string 
  Country  string 
  Number   int 
} 

var currencies = []Curr{ 
  Curr{"DZD", "Algerian Dinar", "Algeria", 12}, 
  Curr{"AUD", "Australian Dollar", "Australia", 36}, 
  Curr{"EUR", "Euro", "Belgium", 978}, 
  Curr{"CLP", "Chilean Peso", "Chile", 152}, 
  Curr{"EUR", "Euro", "Greece", 978}, 
  Curr{"HTG", "Gourde", "Haiti", 332}, 
  ... 
} 

func isDollar(curr Curr) bool { 
  var bool result 
  switch curr { 
  default: 
    result = false 
  case Curr{"AUD", "Australian Dollar", "Australia", 36}: 
    result = true 
  case Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}: 
    result = true 
  case Curr{"USD", "US Dollar", "United States", 840}: 
    result = true 
  } 
  return result 
} 
func isDollar2(curr Curr) bool { 
  dollars := []Curr{currencies[2], currencies[6], currencies[9]} 
  switch curr { 
  default: 
    return false 
  case dollars[0]: 
    fallthrough 
  case dollars[1]: 
    fallthrough 
  case dollars[2]: 
    return true 
  } 
  return false 
} 

func isEuro(curr Curr) bool { 
  switch curr { 
  case currencies[2], currencies[4], currencies[10]: 
    return true 
  default: 
    return false 
  } 
} 

func main() { 
  curr := Curr{"EUR", "Euro", "Italy", 978} 
  if isDollar(curr) { 
    fmt.Printf("%+v is Dollar currency\n", curr) 
  } else if isEuro(curr) { 
    fmt.Printf("%+v is Euro currency\n", curr) 
  } else { 
    fmt.Println("Currency is not Dollar or Euro") 
  } 
  dol := Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344} 
  if isDollar2(dol) { 
    fmt.Println("Dollar currency found:", dol) 
  } 
} 

golang.fyi/ch03/switchstmt.go

Go 中的[T0]语句具有一些有趣的属性和规则,使其易于使用,并可对以下内容进行推理:

  • 从语义上讲,Go 的switch语句可以在两种上下文中使用:
    • 表达方式switch陈述
    • 类型switch语句
  • break语句可用于提前退出开关代码块。
  • switch语句可以在没有其他大小写表达式计算为匹配时包含默认大小写。只能有一个默认情况,它可以放置在开关块内的任何位置。

使用表达式开关

表达式开关非常灵活,可以在程序的控制流需要遵循多条路径的许多上下文中使用。表达式开关支持许多属性,如下所示:

  • 表达式开关可以测试任何类型的值。例如,以下代码段(来自上一个程序清单)测试类型为struct

          func isDollar(curr Curr) bool { 
            var bool result 
            switch curr { 
              default: 
              result = false 
              case Curr{"AUD", "Australian Dollar", "Australia", 36}: 
              result = true 
              case Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}: 
              result = true 
              case Curr{"USD", "US Dollar", "United States", 840}: 
              result = true 
            } 
            return result 
          } 

    的变量Curr

  • case子句中的表达式从左到右、从上到下求值,直到找到与switch表达式相等的值(或表达式)。

  • 当遇到第一个匹配switch表达式的情况时,程序将执行case块的语句,然后立即退出switch块。与其他语言不同,Gocase语句不需要使用中断来避免陷入下一个案例(请参见故障案例部分)。例如,调用isDollar(Curr{"HKD", "Hong Kong Dollar", "Hong Kong", 344})将匹配前面函数中的第二条case语句。代码将结果设置为true并立即退出switch代码块。

  • Case子句可以有多个值(或表达式),这些值(或表达式)之间用逗号分隔,并隐含一个逻辑OR运算符。例如,在下面的代码片段中,switch表达式curr使用一个 case 子句根据值currencies[2]currencies[4]currencies[10]进行测试,直到找到匹配项:

          func isEuro(curr Curr) bool { 
            switch curr { 
              case currencies[2], currencies[4], currencies[10]: 
              return true 
              default: 
              return false 
            } 
          } 
  • switch语句是在 Go 中编写复杂条件语句的更干净、更可取的惯用方法。当前面的代码片段与下面的代码片段进行比较时,这一点很明显,下面的代码片段使用if语句进行相同的比较:

          func isEuro(curr Curr) bool { 
            if curr == currencies[2] || curr == currencies[4],  
            curr == currencies[10]{ 
            return true 
          }else{ 
            return false 
          } 
        } 

漏洞百出的案例

Go 的case子句中没有自动的跳转,就像 C 或 Javaswitch语句中一样。回想一下,switch块将在执行其第一个匹配案例后退出。代码必须明确地放置fallthrough关键字,作为case块中的最后一条语句,以强制执行流通过后续的case块。下面的代码片段显示了一个在每个 case 块中都带有一个[T7]的[T6]语句:

func isDollar2(curr Curr) bool { 
  switch curr { 
  case Curr{"AUD", "Australian Dollar", "Australia", 36}: 
    fallthrough 
  case Curr{"HKD", "Hong Kong Dollar", "Hong Kong", 344}: 
    fallthrough 
  case Curr{"USD", "US Dollar", "United States", 840}: 
    return true 
  default: 
    return false 
  } 
} 

golang.fyi/ch03/switchstmt.go

当匹配一个 case 时,fallthrough语句级联到后续case块的第一个语句。因此,如果curr = Curr{"AUD", "Australian Dollar", "Australia", 36},则第一个案例将匹配。然后,流级联到第二个 case 块的第一个语句,这也是一个fallthrough语句。这导致执行第三个 case 块的第一条语句返回true。这在功能上等同于以下代码段:

switch curr {  
case Curr{"AUD", "Australian Dollar", "Australia", 36},  
     Curr{"HKD", "Hong Kong Dollar", "Hong Kong", 344},  
     Curr{"USD", "US Dollar", "United States", 840}:  
  return true 
default: 
   return false 
}  

无表情开关

Go 支持一种不指定表达式的switch语句形式。在此格式中,每个case表达式的计算结果必须为布尔值true。下面的缩写源代码说明了无表达式的switch语句的用法,如函数find()中所列。函数循环通过Curr值的切片,根据传入的struct函数中的字段值搜索匹配项:

import ( 
  "fmt" 
  "strings" 
) 
type Curr struct { 
  Currency string 
  Name     string 
  Country  string 
  Number   int 
} 

var currencies = []Curr{ 
  Curr{"DZD", "Algerian Dinar", "Algeria", 12}, 
  Curr{"AUD", "Australian Dollar", "Australia", 36}, 
  Curr{"EUR", "Euro", "Belgium", 978}, 
  Curr{"CLP", "Chilean Peso", "Chile", 152}, 
  ... 
} 

func find(name string) { 
  for i := 0; i < 10; i++ { 
    c := currencies[i] 
    switch { 
    case strings.Contains(c.Currency, name), 
      strings.Contains(c.Name, name), 
      strings.Contains(c.Country, name): 
      fmt.Println("Found", c) 
    } 
  } 
} 

golang.fyi/ch03/switchstmt2.go

请注意,在前面的示例中,函数find()中的switch语句不包含表达式。每个case表达式由逗号分隔,并且必须计算为布尔值,每个表达式之间包含一个隐含的OR运算符。前面的switch语句相当于下面使用if语句来实现相同的逻辑:

func find(name string) { 
  for I := 0; i < 10; i++ { 
    c := currencies[i] 
    if strings.Contains(c.Currency, name) || 
      strings.Contains(c.Name, name) || 
      strings.Contains(c.Country, name){ 
      fmt.Println""Foun"", c) 
    } 
  } 
} 

开关初始化器

switch关键字后面可能紧跟着一个简单的初始化语句,在此语句中可以声明和初始化switch代码块的局部变量。这种方便的语法在 initializer 语句和[T2]表达式之间使用分号来声明变量,变量可能出现在[T3]代码块的任何地方。下面的代码示例显示了如何通过初始化两个变量namecurr来实现这一点,作为switch声明的一部分:

func assertEuro(c Curr) bool {  
  switch name, curr := "Euro", "EUR"; {  
  case c.Name == name:  
    return true  
  case c.Currency == curr:  
    return true 
  }  
  return false  
} 

golang.fyi/ch03/switchstmt2.go

前面的代码段使用带有初始值设定项的无表达式[T0]语句。请注意,后面的分号表示初始化语句和开关的表达式区域之间的分隔。然而,在本例中,switch表达式为空。

型开关

考虑到 Go 强大的类型支持,该语言支持查询类型信息的能力就不足为奇了。类型switch是一条语句,它使用 Go 接口类型来比较值(或表达式)的底层类型信息。关于接口类型和类型断言的完整讨论超出了本节的范围。您可以在第 8 章方法、接口和对象中找到关于该主题的更多详细信息。

然而,为了完整起见,这里对类型开关进行了简短的讨论。现在,您需要知道的是,Go 提供了类型interface{}或空接口,作为一个超级类型,由类型系统中的所有其他类型实现。当一个值被指定为类型interface{}时,可以使用类型switch查询该值,如下面代码段中的函数findAny()所示,以查询其底层类型的信息:

func find(name string) { 
  for i := 0; i < 10; i++ { 
    c := currencies[i] 
    switch { 
    case strings.Contains(c.Currency, name), 
      strings.Contains(c.Name, name), 
      strings.Contains(c.Country, name): 
      fmt.Println("Found", c) 
    } 
  } 
}  

func findNumber(num int) { 
  for _, curr := range currencies { 
    if curr.Number == num { 
      fmt.Println("Found", curr) 
    } 
  } 
}  

func findAny(val interface{}) {  
  switch i := val.(type) {  
  case int:  
    findNumber(i)  
  case string:  
    find(i)  
  default:  
    fmt.Printf("Unable to search with type %T\n", val)  
  }  
} 

func main() { 
findAny("Peso") 
  findAny(404) 
  findAny(978) 
  findAny(false) 
} 

golang.fyi/ch03/switchstmt2.go

函数findAny()interface{}为参数。类型switch用于使用类型断言表达式确定变量val的基础类型和值:

switch i := val.(type) 

注意前面的类型断言表达式中使用了关键字type。将根据val.(type)查询的类型信息对每个 case 子句进行测试。变量i将被分配基础类型的实际值,并用于调用具有相应值的函数。调用默认块是为了防止分配给参数val的任何意外类型。然后可以使用不同类型的值调用函数findAny,如以下代码段所示:

findAny("Peso")  
findAny(404)  
findAny(978)  
findAny(false)  

用于语句的

作为与 C 族相关的语言,Go 还支持for循环式控制结构。然而,正如你现在可能已经预料到的那样,Go 的for语句以有趣的方式简单地工作。Go 中的for语句支持四种不同的习惯用法,如下表所示:

| **用于报表** | **用法** | | 条件 | 用于语义替换`while`和`do...while`循环:
for x < 10 { 
... 
}

| | 无限循环 | 可以省略条件表达式以创建无限循环:

for {
...
}

| | 传统的 | 这是带有初始值设定项、测试和更新子句的 C 族for循环的传统形式:

for x:=0; x < 10; x++ {
...
}

| | 射程 | 用于迭代表示存储在数组、字符串(符文数组)、切片、映射和通道中的项集合的表达式:

for i, val := range values {
...
}

|

注意,与 Go 中的所有其他控制语句一样,for语句在其表达式周围不使用括号。循环代码块的所有语句都必须用花括号括起来,否则编译器将产生错误。

条件

for条件使用的构造在语义上等同于其他语言中的while循环。它使用关键字for,后跟一个布尔表达式,只要计算结果为 true,循环就可以继续进行。以下缩写源代码列表显示了这种形式的for循环的示例:

type Curr struct {  
  Currency string  
  Name     string  
  Country  string  
  Number   int  
}  
var currencies = []Curr{  
  Curr{"KES", "Kenyan Shilling", "Kenya", 404},  
  Curr{"AUD", "Australian Dollar", "Australia", 36},  
... 
} 

func listCurrs(howlong int) {  
  i := 0  
  for i < len(currencies) {  
    fmt.Println(currencies[i])  
    i++  
  }  
} 

golang.fyi/ch03/forstmt.go

函数listCurrs()中的for语句只要条件表达式i < len(currencencies)返回true就进行迭代。必须注意确保i的值随每次迭代而更新,以避免创建意外的无限循环。

无限循环

当在for语句中省略布尔表达式时,循环将无限期运行,如下例所示:

for { 
  // statements here 
} 

这相当于其他语言(如 C 或 Java)中的[T0]或[T1]。

传统的 for 语句

Go 还支持传统形式的for语句,包括初始化语句、条件表达式和 update 语句,所有语句都用分号分隔。这是传统上在其他类似 C 语言中发现的语句形式。下面的源代码片段演示了在函数sortByNumber中使用传统的 for 语句:

type Curr struct {  
  Currency string  
  Name     string  
  Country  string  
  Number   int  
}  

var currencies = []Curr{  
  Curr{"KES", "Kenyan Shilling", "Kenya", 404},  
  Curr{"AUD", "Australian Dollar", "Australia", 36},  
... 
} 

func sortByNumber() {  
  N := len(currencies)  
  for i := 0; i < N-1; i++ {  
     currMin := i  
     for k := i + 1; k < N; k++ {  
    if currencies[k].Number < currencies[currMin].Number {  
         currMin = k  
    }  
     }  
     // swap  
     if currMin != i {  
        temp := currencies[i]  
    currencies[i] = currencies[currMin]  
    currencies[currMin] = temp  
     } 
  }  
} 

golang.fyi/ch03/forstmt.go

前面的示例实现了一个选择排序,通过比较每个struct值的Number字段对slice货币进行排序。for语句的不同部分使用以下代码片段突出显示(来自前面的函数):

The traditional for statement

事实证明,传统的for语句是到目前为止讨论的循环的其他形式的超集,如下表所示:

| **用于报表** | **说明** | |
k:=initialize()
for ; k < 10; 
++{
...
}

| 省略初始化语句。变量kfor语句之外初始化。然而,惯用的方法是用[T2]语句初始化变量。 | |

for k:=0; k < 10;{
...
}

| 此处省略update语句(在最后一个分号之后)。开发人员必须在其他地方提供更新逻辑,否则可能会创建无限循环。 | |

for ; k < 10;{
...
}

| 这相当于for条件表(前面讨论过)for k < 10 { ... }。同样,变量k应该在循环之前声明。必须小心更新k,否则可能会造成无限循环。 | |

for k:=0; ;k++{
...
}

| 这里,省略条件表达式。与前面一样,这将条件求值为true,如果在循环中没有引入适当的终止逻辑,将产生一个无限循环。 | |

for ; ;{ ... }

| 这相当于形式[T0],并产生一个无限循环。 |

for循环中的初始化语句和update语句是常规 Go 语句。因此,它们可以用于初始化和更新多个变量,正如 Go 所支持的那样。为了说明这一点,下一个示例在语句子句中同时初始化和更新两个变量w1w2

import ( 
  "fmt" 
  "math/rand" 
) 

var list1 = []string{ 
"break", "lake", "go",  
"right", "strong",  
"kite", "hello"}  

var list2 = []string{ 
"fix", "river", "stop",  
"left", "weak", "flight",  
"bye"}  

func main() {  
  rand.Seed(31)  
  for w1, w2:= nextPair();  
  w1 != "go" && w2 != "stop";  
  w1, w2 = nextPair() {  

    fmt.Printf("Word Pair -> [%s, %s]\n", w1, w2)  
  }  
}  

func nextPair() (w1, w2 string) {  
  pos := rand.Intn(len(list1))  
  return list1[pos], list2[pos]  
} 

golang.fyi/ch03/forstmt2.go

初始化语句通过调用函数nextPair()来初始化变量w1w2。该条件使用一个复合逻辑表达式,只要其计算结果为 true,该表达式就会保持循环运行。最后,变量w1w2在循环的每次迭代中都会通过调用nextPair()进行更新。

射程

最后,for语句支持另一种形式,它使用关键字range迭代计算为数组、切片、映射、字符串或通道的表达式。for range 循环具有以下通用形式:

对于<标识符列表>:=]范围<表达式>{…}

根据range表达式产生的类型,每次迭代最多可以产生两个变量,如下表所示:

| **范围表达式** | **范围变量** | | 循环阵列或切片:
for i, v := range []V{1,2,3} {
...
}

| 该范围产生两个值,其中i是循环索引,v是来自集合的值v[i]。有关阵列和切片的进一步讨论,请参见第 7 章复合类型。 | | 循环字符串值:

for i, v := range "Hello" {
...
}

| 该范围产生两个值,其中i是字符串中字节的索引,v是作为符文返回的v[i]处 UTF-8 编码字节的值。有关字符串类型的进一步讨论,请参见第 4 章数据类型。 | | 循环映射:

for k, v := range map[K]V {
...
}

| range产生两个值,其中k被分配K类型的映射键的值,v被存储在V类型的map[k]中。有关 map 的进一步讨论,请参见第 7 章复合类型。 | | 循环通道值:

var ch chan T
for c := range ch {
...
}

| 第 9 章并发对通道进行了充分的讨论。通道是能够接收和发射值的双向导管。for...range语句在每次迭代中将从通道接收到的每个值分配给变量c。 |

您应该知道,每次迭代发出的值都是存储在源中的原始项的副本。例如,在以下程序中,循环完成后切片中的值不会更新:

import "fmt" 

func main() { 
  vals := []int{4, 2, 6} 
  for _, v := range vals { 
    v-- 
  } 
  fmt.Println(vals) 
} 

要使用for...range循环更新原始值,请使用索引表达式访问原始值,如下所示。

func main() { 
  vals := []int{4, 2, 6} 
  for i, v := range vals { 
    vals[i] = v - 1 
  } 
  fmt.Println(vals) 
} 

在前面的示例中,在切片索引表达式vals[i]中使用值i来更新切片中存储的原始值。如果只需要访问数组、切片或字符串(或映射键)的索引值,则可以省略迭代值(赋值中的第二个变量)。例如,在下面的示例中,for...range语句只在每次迭代时发出当前索引值:

func printCurrencies() { 
  for i := range currencies { 
    fmt.Printf("%d: %v\n", i, currencies[i]) 
  } 
} 

golang.fyi/ch03/for-range-stmt.go

最后,在某些情况下,您可能对迭代生成的任何值都不感兴趣,而是对迭代机制本身感兴趣。引入了 for 语句的下一种形式(从 Go 的 1.4 版开始),以表示不带任何变量声明的 for 范围,如以下代码段所示:

func main() { 
  for range []int{1,1,1,1} { 
    fmt.Println("Looping") 
  } 
}  

前面的代码将在标准输出上打印"Looping"四次。当范围表达式位于通道上方时,有时会使用这种形式的for...range循环。它仅用于通知通道中存在值。

break、continue 和 goto 语句

Go 支持一组专门设计用于突然退出正在运行的代码块的语句,例如 switch 和 for 语句,并将控制权转移到代码的不同部分。这三条语句都可以接受标签标识符,该标识符在代码中指定要传输控制的目标位置。

标签标识符

在深入本节的核心部分之前,有必要先看看这些语句使用的标签。在 Go 中声明标签需要标识符后跟冒号,如以下代码段所示:

DoSearch: 

给标签命名是一个风格问题。但是,应遵循上一章中介绍的标识符命名准则。标签必须包含在函数中。Go 编译器将不允许未使用的标签在代码中悬空。与变量类似,如果声明了标签,则必须在代码中引用它。

中断声明

与其他类似 C 的语言一样,Gobreak语句终止并退出最里面的switchfor语句代码块,并将控制权转移到运行程序的另一部分。break语句可以接受一个可选的标签标识符,该标识符在封闭函数中指定一个标签位置,程序将在该位置继续运行。以下是break语句需要记住的标签属性:

  • 标签必须在break语句所在的同一运行函数中声明
  • 声明的标签后面必须紧跟着嵌套中断的封闭控制语句(一个for循环或switch语句)

如果一个break语句后面有一个标签,控制权将被转移,而不是转移到标签所在的位置,而是转移到紧跟在标签块后面的语句。如果没有提供标签,break语句会突然退出,并将控制权转移到其封闭的for语句(或switch语句)块后面的下一个语句。

下面的代码是一个过度夸张的线性搜索,它演示了[T0]语句的工作原理。它执行单词搜索,并在切片中找到单词的第一个实例后退出:

import ( 
  "fmt" 
) 

var words = [][]string{  
  {"break", "lake", "go", "right", "strong", "kite", "hello"},  
  {"fix", "river", "stop", "left", "weak", "flight", "bye"},  
  {"fix", "lake", "slow", "middle", "sturdy", "high", "hello"},  
}  

func search(w string) {  
DoSearch:  
  for i := 0; i < len(words); i++ {  
    for k := 0; k < len(words[i]); k++ {  
      if words[i][k] == w {  
        fmt.Println("Found", w)  
        break DoSearch  
      }  
    }  
  }  
}  

golang.fyi/ch03/breakstmt.go

在前面的代码片段中,break DoSearch语句将基本上退出最内层的for循环,并导致执行流在最外层标记的for语句之后继续,在本例中,该语句将简单地结束程序。

continue 语句

continue语句使控制流立即终止封闭的for循环的当前迭代,并跳到下一个迭代。continue语句也可以采用可选标签。该标签具有与break声明类似的属性:

  • 标签必须在continue语句所在的同一运行函数中声明
  • 声明的标签后面必须紧跟一个包含 continue 语句的for循环语句

当存在时,在for语句块内到达continue语句,for循环将突然终止,控制权将转移到标记为for的最外层循环块继续。如果未指定标签,continue语句将简单地将控制转移到其封闭的for循环块的开始,以继续下一次迭代。

为了说明这一点,让我们回顾一下前面的单词搜索示例。此版本使用了一个continue语句,这会导致搜索在切片中找到多个已搜索单词:

func search(w string) {  
DoSearch:  
  for i := 0; i < len(words); i++ {  
    for k := 0; k < len(words[i]); k++ {  
      if words[i][k] == w {  
        fmt.Println("Found", w)  
        continue DoSearch  
      }  
    }  
  }  
} 

golang.fyi/ch03/breakstmt2.go

continue DoSearch语句使最内层循环的当前迭代停止,并将控制转移到标记的外部循环,使其继续进行下一次迭代。

goto 声明

goto语句更灵活,因为它允许将流控制传输到函数内部的任意位置,在该位置定义了目标标签。goto语句导致突然将控制转移到goto语句引用的标签。下面以一个简单但功能性的示例展示了 Go 的goto语句的作用:

import "fmt" 

func main() {  
  var a string 
Start:  
  for {  
    switch {  
    case a < "aaa":  
      goto A  
    case a >= "aaa" && a < "aaabbb":  
      goto B  
    case a == "aaabbb":  
      break Start  
    }  
  A:  
    a += "a"  
    continue Start  
  B:  
    a += "b"  
    continue Start  
  }  
fmt.Println(a) 
} 

golang.fyi/ch03/gotostmt.go

代码使用goto语句跳转到main()函数的不同部分。注意,goto语句可以针对代码中任何地方定义的标签。出于完整性考虑,Start:标签的多余用法保留在代码中,在本上下文中没有必要(因为如果没有标签,continue 将具有相同的效果)。以下内容为使用goto声明提供了一些指导:

  • 避免使用goto语句,除非所实现的逻辑只能通过goto分支实现。这是因为过度使用[T2]语句会使代码更难推理和调试。
  • 尽可能将goto语句及其目标标签放在同一封闭代码块中。
  • 避免在[T0]语句会导致流跳过新变量声明或导致重新声明它们的地方放置标签。
  • Go 将允许您从内部跳转到外部封闭代码块。
  • 如果试图跳转到对等方或封闭的代码块,则是编译错误。

总结

本章提供了 Go 中控制流机制的演练,包括ifswitchfor语句。虽然 Go 的流控制结构看起来简单易用,但它们功能强大,实现了现代语言所期望的所有分支原语。每一个概念都向读者介绍了大量的细节和例子,以确保主题的清晰。下一章通过向读者介绍 Go 类型系统,继续我们对 Go 基本原理的研究。