控制结构
Go 提供了下面这些条件结构和分支结构:
- if-else 结构
- switch 结构
- select 结构,用于 channel 的选择
可以使用迭代或循环结构来重复执行一次或多次某段代码(任务):
- for (range) 结构
一些如 break
和 continue
这样的关键字可以用于中途改变循环的状态。
此外,你还可以使用 return
来结束某个函数的执行,或使用 goto
和标签来调整程序的执行位置。
if-else
if
可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):
1 | if initialization; condition { |
例如:1
2
3
4val := 10
if val > max {
// do something
}
你也可以这样写:1
2
3if val := 10; val > max {
// do something
}
测试多返回值函数的错误
Go 语言的函数经常使用两个返回值来表示执行是否成功。这样一来,就很明显需要用一个 if 语句来测试执行结果;由于其符号的原因,这样的形式又称之为 comma,ok 模式(pattern)。
switch 结构
1 | switch var1 { |
一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch
代码块,也就是说不需要特别使用 break
语句来表示结束。
如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough
关键字来达到目的。
同样可以使用 return
语句来提前结束代码块的执行。
可选的 default
分支可以出现在任何顺序,但最好将它放在最后。它的作用类似于 if-else
语句中的 else
,表示不符合任何已给出条件时,执行相关语句。
switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。
switch 语句的第三种形式是包含一个初始化语句:
1 | switch result := calculate(); { |
for 结构
1 | for 初始化语句; 条件语句; 修饰语句 {} |
不需要括号 ()
将它们括起来。例如:for (i = 0; i < 10; i++) { }
,这是无效的代码。
for
结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while
循环),基本形式为:
1 | for 条件语句 {} |
如果 for
循环的头部没有条件语句,那么就会认为条件永远为 true
。for true { }
,但一般情况下都会直接写 for { }
。无限循环的经典应用是服务器,用于不断等待和接受新的请求。
for-range 结构
语法上很类似其它语言中 foreach
语句,它可以迭代任何一个集合(包括数组和 map,)可以获得每次迭代所对应的索引。
1 | for ix, val := range coll { } |
标签与 goto
for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:
)结尾的单词。标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母。
使用标签和 goto
语句是不被鼓励的:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。
如果必须使用goto
,应当只使用正序的标签(标签位于 goto
语句之后),但注意标签和goto
语句之间不能出现定义新变量的语句,否则会导致编译失败。
函数
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main()
函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
Go 里面有三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法(Methods)
函数被调用的基本格式如下:
1 | pack1.Function(arg1, arg2, …, argn) |
Function
是 pack1
包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。
目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
函数参数与返回值
通过 return
关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return
或 panic
结尾。
- 按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable
)传递给函数,这就是按引用传递,比如 Function(&arg1
),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。
传递变长参数
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
1 | func myFunc(a, b, arg ...int) {} |
如果参数被存储在一个 slice 类型的变量 slice
中,则可以通过 slice...
的形式来传递参数调用变参函数。
但是如果变长参数的类型并不是都相同的呢?使用 5 个参数来进行传递并不是很明智的选择,有 2 种方案可以解决这个问题:
- 使用结构
定义一个结构类型,假设它叫Options
,用以存储所有可能的参数:1
2
3
4
5type Options struct {
par1 type1,
par2 type2,
...
}
函数 F1 可以使用正常的参数 a 和 b,以及一个没有任何初始化的 Options
结构: F1(a, b, Options {})
。如果需要对选项进行初始化,则可以使用F1(a, b, Options {par1:val1, par2:val2})
。
- 使用空接口
如果一个变长参数的类型没有被指定,则可以使用默认的空接口interface{}
,这样就可以接受任何类型的参数。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个for-range
循环以及switch
结构对每个参数的类型进行判断:
1 | func typecheck(..,..,values … interface{}) { |
defer 和追踪
关键字 defer
允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为return
语句同样可以包含一些操作,而不是单纯地返回某个值)。
当有多个 defer
行为被注册时,它们会以逆序执行(类似栈,即后进先出)
关键字 defer
允许我们进行一些函数执行完成后的收尾工作,例如:
关闭文件流
1
2// open a file
defer file.Close()解锁一个加锁的资源
1 | mu.Lock() |
- 打印最终报告
1 | printHeader() |
- 关闭数据库连接
1 | // open a database connection |
内置函数
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数,在部署环境中建议使用 fmt 包 |
complex、real imag | 用于创建和操作复数 |
使用闭包调试
当在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime
或 log
包中的特殊函数来实现这样的功能。包 runtime
中的函数 Caller()
提供了相应的信息,因此可以在需要的时候实现一个 where()
闭包函数来打印函数执行的位置:
1 | where := func() { |
也可以设置 log
包中的 flag
参数来实现:
1 | log.SetFlags(log.Llongfile) |
或使用一个更加简短版本的 where
函数:
1 | var where = log.Print |