控制结构
Go 的控制结构与 C 语言相关,但在重要方面有所不同。没有
do
或 while
循环,只有稍微通用的 for
循环;switch
更灵活;if
和 switch
接受类似于 for
的可选初始化语句;break
和 continue
语句可以带可选标签,以标识要跳出或继续的部分;此外,还有新的控制结构,包括类型开关和多路通信复用器 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
在第一条语句中被声明,但在第二条语句中只是被重新赋值。这意味着调用 f.Stat
使用的是上面声明的现有 err
变量,并且只是给它赋了一个新值。在
:=
声明中,即使变量 v
已经声明,也可以再次出现,只要满足以下条件:- 该声明与现有的
v
声明在同一作用域内(如果v
已在外部作用域中声明,则该声明会创建一个新变量),
- 初始化中的对应值可以赋值给
v
,并且
- 该声明创建了至少一个其他变量。
这种不寻常的特性出于实用考虑,使得在长的
if-else
链中使用单一的 err
值变得容易。你会经常看到这种用法。值得注意的是,在 Go 中,函数参数和返回值的作用域与函数体相同,尽管它们在语法上出现在包含函数体的大括号之外。
for
Go 的
for
循环与 C 的相似,但并不相同。它统一了 for
和 while
,并且没有 do-while
。有三种形式,只有一种形式带有分号。// 类似于 C 的 for for init; condition; post { } // 类似于 C 的 while for condition { } // 类似于 C 的 for(;;) for { }
短声明使得在循环中直接声明索引变量变得容易。
sum := 0 for i := 0; i < 10; i++ { sum += i }
如果你正在遍历一个数组、切片、字符串或映射,或者从通道中读取数据,可以使用
range
子句来管理循环。for key, value := range oldMap { newMap[key] = value }
如果你只需要范围中的第一个项(键或索引),可以省略第二项:
for key := range m { if key.expired() { delete(m, key) } }
如果你只需要范围中的第二项(值),可以使用空白标识符(下划线)来丢弃第一项:
sum := 0 for _, value := range array { sum += value }
空白标识符有许多用途,稍后会详细介绍。
对于字符串,
range
会为你做更多的工作,通过解析 UTF-8 来分解单个 Unicode 码点。错误的编码会消耗一个字节并产生替代符号 U+FFFD。以下循环:for pos, char := range "日本\\x80語" { // \\x80 是非法的 UTF-8 编码 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
循环中运行多个变量,应该使用并行赋值(尽管这会排除 ++
和 --
)。// 反转数组 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 的更通用。表达式不必是常量或整数,案例从上到下评估,直到找到匹配项。如果 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 }
没有自动的贯穿,但可以用逗号分隔的列表呈现案例。
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
尽管在 Go 中不如某些其他 C 类语言常见,但可以使用
break
语句提前终止 switch
。有时,有必要跳出外层循环而不是 switch
,在 Go 中,可以通过在循环上放置标签并“跳转”到该标签来实现。这段示例展示了两种用法。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) } }
【译者注:这个类似于 C 的 goto 语句,如果你不知道 goto 语言的话,就直接忘记它吧,goto 在 C 语言中一直被诟病。不过 go 语言保留了它最好用的功能——跳出多层循环】
当然,
continue
语句也接受可选标签,但仅适用于循环。最后,这里有一个比较字节切片的例程,使用了两个
switch
语句:// Compare 返回两个字节切片的比较结果, // 按字典序比较。 // 如果 a == b,结果为 0;如果 a < b,结果为 -1;如果 a > b,结果为 +1 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 }
type switch(类型开关)
switch
还可以用来发现接口变量的动态类型。这种类型开关使用类型断言的语法,并在括号内使用关键字 type
。如果开关在表达式中声明了一个变量,该变量在每个条款中将具有相应的类型。在这种情况下,复用名称也是惯用的,实际上是在每种情况下声明一个具有相同名称但不同类型的新变量。var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\\n", t) // %T 打印 t 的类型 case bool: fmt.Printf("boolean %t\\n", t) // t 的类型是 bool case int: fmt.Printf("integer %d\\n", t) // t 的类型是 int case *bool: fmt.Printf("pointer to boolean %t\\n", *t) // t 的类型是 *bool case *int: fmt.Printf("pointer to integer %d\\n", *t) // t 的类型是 *int }
函数
多个返回值
Go 的一个不寻常的特性是函数和方法可以返回多个值。这种形式可以改善 C 程序中的一些笨拙习惯,例如使用
-1
表示 EOF
的带内错误返回以及通过地址修改传递的参数。在 C 中,写入错误通过负计数信号,错误代码被隐藏在一个易变的位置。Go 中的
Write
可以同时返回计数和错误:“是的,你写了一些字节,但不是全部,因为你填满了设备”。来自 os
包的 Write
方法的签名是:func (file *File) Write(b []byte) (n int, err error)
如文档所述,它返回写入的字节数和一个非零的
error
,当 n
不等于 len(b)
时。这是一种常见的风格,错误处理部分会有更多示例。类似的做法消除了需要传递指针到返回值以模拟引用参数的需求。以下是一个简单的函数,用于从字节切片中的某个位置获取数字,返回该数字及下一个位置。
func nextInt(b []byte, i int) (int, int) { for ; i < len(b) && !isDigit(b[i]); i++ { } x := 0 for ; i < len(b) && isDigit(b[i]); i++ { x = x*10 + int(b[i]) - '0' } return x, i }
你可以像这样使用它来扫描输入切片
b
中的数字:for i := 0; i < len(b); { x, i = nextInt(b, i) fmt.Println(x) }
命名结果参数
Go 函数的返回或结果“参数”可以命名,并可以像普通变量一样使用。当命名时,它们在函数开始时初始化为其类型的零值;如果函数执行没有参数的
return
语句,则当前的结果参数值将作为返回值使用。这些名称不是强制性的,但可以使代码更简洁明了:它们也是文档。如果我们为
nextInt
命名结果,哪个返回的 int
是哪个就变得显而易见。func nextInt(b []byte, pos int) (value, nextPos int) {
因为命名结果在初始化时被绑定到无装饰的返回中,所以它们可以简化并澄清。以下是一个使用它们的
io.ReadFull
的版本:func ReadFull(r Reader, buf []byte) (n int, err error) { for len(buf) > 0 && err == nil { var nr int nr, err = r.Read(buf) n += nr buf = buf[nr:] } return }
defer
Go 的
defer
语句会调度一个函数调用(被延迟的函数),在执行 defer
的函数返回之前立即运行。这是一种独特但有效的处理方式,例如在函数返回的过程中必须释放的资源。经典的例子包括解锁互斥锁或关闭文件。// Contents 返回文件的内容作为字符串。 func Contents(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() // f.Close 会在我们完成时运行。 var result []byte buf := make([]byte, 100) for { n, err := f.Read(buf[0:]) result = append(result, buf[0:n]...) // append 在后面讨论。 if err != nil { if err == io.EOF { break } return "", err // 如果我们在这里返回,f 将被关闭。 } } return string(result), nil // 如果我们在这里返回,f 将被关闭。 }
延迟调用像
Close
这样的函数有两个优点。首先,它保证你不会忘记关闭文件,这在稍后编辑函数以添加新返回路径时是很容易犯的错误。其次,它意味着关闭操作与打开操作紧密相连,这比将其放在函数末尾要清晰得多。延迟函数的参数(如果函数是方法,则包括接收者)在defer 执行时被评估,而不是在调用 执行时被评估。除了避免担心变量在函数执行过程中值的变化外,这意味着单个延迟调用位置可以延迟多个函数执行。以下是一个简单的例子。
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
延迟的函数以 LIFO 顺序执行,因此这段代码将在函数返回时打印
4 3 2 1 0
。一个更合理的例子是跟踪程序中的函数执行。我们可以像这样写几个简单的跟踪例程:func trace(s string) { fmt.Println("entering:", s) } func untrace(s string) { fmt.Println("leaving:", s) } // 这样使用它们: func a() { trace("a") defer untrace("a") // 做一些事情.... }
我们可以更好地利用延迟函数参数在执行时被评估的事实。跟踪例程可以设置未跟踪例程的参数。这段示例:
func trace(s string) string { fmt.Println("entering:", s) return s } func un(s string) { fmt.Println("leaving:", s) } func a() { defer un(trace("a")) fmt.Println("in a") } func b() { defer un(trace("b")) fmt.Println("in b") a() } func main() { b() }
将输出:
entering: b in b entering: a in a leaving: a leaving: b
对于习惯于其他语言中的块级资源管理的程序员来说,
defer
可能显得奇怪,但它最有趣和强大的应用恰恰来自于它不是基于块而是基于函数的特性。在关于 panic
和 recover
的部分中,我们将看到它的另一种可能性。【译者注:defer 函数使用到了栈的概念,它是先进后出的,其含义就是先执行的 defer 函数晚于后执行的 defer 函数。如果不清楚此部分的话,建议看一下队列和栈的定义与区别】