💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] Go的控制结构与C的相关,但是有重要的区别。没有`do`或者`while`循环,只有一个稍微广义的`for`;`switch`更加灵活;`if`和`switch`接受一个像`for`那样可选的初始化语句;`break`和`continue`语句接受一个可选的标号来指定中断或继续什么;还有一些新的控制结构,包括类型switch和多路通信复用器(multiway communications multiplexer),`select`。语句也稍微有些不同:没有圆括号,并且控制结构体必须总是由大括号包裹。 ## If Go中,简单的`if`看起来是这样的: ~~~ if x > 0 { return y } ~~~ 强制的大括号可以鼓励大家在多行中编写简单的`if`语句。不管怎样,这是一个好的风格,特别是当控制结构体包含了一条控制语句,例如`return`或者`break`。 既然`if`和`switch`接受一个初始化语句,那么常见的方式是用来建立一个局部变量。 ~~~ if err := file.Chmod(0664); err != nil { log.Print(err) return err } ~~~ 在Go的库中,你会发现当`if`语句不会流向下一条语句时—也就是说,控制结构体结束于`break`,`continue`,`goto`或者`return`—则不必要的`else`会被省略掉。 ~~~ f, err := os.Open(name) if err != nil { return err } codeUsing(f) ~~~ 这个例子是一种常见的情况,代码必须防范一系列的错误条件。如果成功的控制流是沿着页面往下走,来消除它们引起的错误情况,那么代码会非常易读。由于错误情况往往会结束于`return`语句,因此代码不需要有`else`语句。 ~~~ f, err := os.Open(name) if err != nil { return err } d, err := f.Stat() if err != nil { f.Close() return err } codeUsing(f, d) ~~~ ## 重新声明和重新赋值 另外:上一章节的最后一个例子,展示了`:=`短声明形式的工作细节。该声明调用了`os.Open`进行读取, ~~~ f, err := os.Open(name) ~~~ 该语句声明了两个变量,`f`和`err`。几行之后,又调用了`f.Stat`进行读取, ~~~ d, err := f.Stat() ~~~ 这看起来像是又声明了`d`和`err`。但是,注意`err`在两条语句中都出现了。这种重复是合法的:`err`是在第一条语句中被声明,而在第二条语句中只是被*重新赋值*。这意味着使用之前已经声明过的`err`变量调用`f.Stat`,只会是赋给其一个新的值。 在`:=`声明中,变量`v`即使已经被声明过,也可以出现,前提是: * 该声明和`v`已有的声明在相同的作用域中(如果`v`已经在外面的作用域里被声明了,则该声明将会创建一个新的变量 §) * 初始化中相应的值是可以被赋给`v`的 * 并且,声明中至少有其它一个变量将被声明为一个新的变量 这种不寻常的属性纯粹是从实用主义方面来考虑的。例如,这会使得在一个长的`if-else`链中,很容易地使用单个`err`值。你会经常看到这种用法。 § 值得一提的是,在Go中,函数参数和返回值的作用域与函数体的作用域是相同的,虽然它们在词法上是出现在包裹函数体的大括号外面。 ## For Go的`for`循环类似于—但又不等同于—C的。它统一了`for`和`while`,并且没有`do-while`。有三种形式,其中只有一个具有分号。 ~~~ // Like a C for for init; condition; post { } // Like a C while for condition { } // Like a C for(;;) for { } ~~~ 短声明使得在循环中很容易正确的声明索引变量。 ~~~ sum := 0 for i := 0; i < 10; i++ { sum += i } ~~~ 如果你是在数组,切片,字符串或者map上进行循环,或者从channel中进行读取,则可以使用`range`子句来管理循环。 ~~~ for key, value := range oldMap { newMap[key] = value } ~~~ 如果你只需要range中的第一项(key或者index),则可以丢弃第二个: ~~~ for key := range m { if key.expired() { delete(m, key) } } ~~~ 如果你只需要range中的第二项(value),则可以使用*空白标识符*,一个下划线,来丢弃第一个: ~~~ sum := 0 for _, value := range array { sum += value } ~~~ 空白标识符有许多用途,这在[后面的章节](http://www.hellogcc.org/effective_go.html#blank)中会有介绍。 对于字符串,`range`会做更多的事情,通过解析UTF-8来拆分出单个的Unicode编码点。错误的编码会消耗一个字节,产生一个替代的符文(rune)U+FFFD。(名字(与内建类型相关联的)`rune`是Go的术语,用于指定一个单独的Unicode编码点。详情参见[the language specification](http://golang.org/ref/spec#Rune_literals))循环 ~~~ for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding fmt.Printf("character %#U starts at byte position %d\n", char, pos) } ~~~ 会打印出 ~~~ character U+65E5 '日' starts at byte position 0 character U+672C '本' starts at byte position 3 character U+FFFD '�' starts at byte position 6 character U+8A9E '語' starts at byte position 7 ~~~ 最后,Go没有逗号操作符,并且`++`和`--`是语句而不是表达式。因此,如果你想在`for`中运行多个变量,你需要使用并行赋值(尽管这样会阻碍使用`++`和`--`)。 ~~~ // Reverse a for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } ~~~ ## Switch Go的`switch`要比C的更加通用。表达式不需要为常量,甚至不需要为整数,case是按照从上到下的顺序进行求值,直到找到匹配的。如果`switch`没有表达式,则对`true`进行匹配。因此,可以—按照语言习惯—将`if`-`else`-`if`-`else`链写成一个`switch`。 ~~~ func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 } ~~~ switch不会自动从一个case子句跌落到下一个case子句。但是case可以使用逗号分隔的列表。 ~~~ func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false } ~~~ 虽然和其它类C的语言一样,使用`break`语句来提前中止`switch`在Go中几乎不怎么常见。不过,有时候是需要中断包含它的循环,而不是switch。在Go中,可以通过在循环上加一个标号,然后“breaking”到那个标号来达到目的。该例子展示了这些用法。 ~~~ Loop: for n := 0; n < len(src); n += size { switch { case src[n] < sizeOne: if validateOnly { break } size = 1 update(src[n]) case src[n] < sizeTwo: if n+1 >= len(src) { err = errShortInput break Loop } if validateOnly { break } size = 2 update(src[n] + src[n+1]<<shift) } } ~~~ 当然,`continue`语句也可以接受一个可选的标号,但是只能用于循环。 作为这个章节的结束,这里有一个对字节切片进行比较的程序,使用了两个`switch`语句: ~~~ // Compare returns an integer comparing the two byte slices, // lexicographically. // The result will be 0 if a == b, -1 if a < b, and +1 if a > b func Compare(a, b []byte) int { for i := 0; i < len(a) && i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } switch { case len(a) > len(b): return 1 case len(a) < len(b): return -1 } return 0 } ~~~ ## 类型switch switch还可用于获得一个接口变量的动态类型。这种*类型switch*使用类型断言的语法,在括号中使用关键字`type`。如果switch 在表达式中声明了一个变量,则变量会在每个子句中具有对应的类型。比较符合语言习惯的方式是在这些case里重用一个名字,实际上是在每个case里声名一个新的变量,其具有相同的名字,但是不同的类型。 ~~~ var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int } ~~~