The Way to Go(二)

控制结构

Go 提供了下面这些条件结构和分支结构:

  • if-else 结构
  • switch 结构
  • select 结构,用于 channel 的选择

可以使用迭代或循环结构来重复执行一次或多次某段代码(任务):

  • for (range) 结构

一些如 breakcontinue 这样的关键字可以用于中途改变循环的状态。

此外,你还可以使用 return 来结束某个函数的执行,或使用 goto 和标签来调整程序的执行位置。

if-else

if可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):

1
2
3
if initialization; condition {
// do something
}

例如:

1
2
3
4
val := 10
if val > max {
// do something
}

你也可以这样写:

1
2
3
if val := 10; val > max {
// do something
}

测试多返回值函数的错误

Go 语言的函数经常使用两个返回值来表示执行是否成功。这样一来,就很明显需要用一个 if 语句来测试执行结果;由于其符号的原因,这样的形式又称之为 comma,ok 模式(pattern)。

switch 结构

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说不需要特别使用 break 语句来表示结束。

如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。

同样可以使用 return 语句来提前结束代码块的执行。

可选的 default 分支可以出现在任何顺序,但最好将它放在最后。它的作用类似于 if-else语句中的 else,表示不符合任何已给出条件时,执行相关语句。

switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。

switch 语句的第三种形式是包含一个初始化语句:

1
2
3
4
5
6
7
8
switch result := calculate(); {
case result < 0:
...
case result > 0:
...
default:
// 0
}

for 结构

1
for 初始化语句; 条件语句; 修饰语句 {}

不需要括号 () 将它们括起来。例如:for (i = 0; i < 10; i++) { },这是无效的代码。

for 结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while 循环),基本形式为:

1
for 条件语句 {}

如果 for 循环的头部没有条件语句,那么就会认为条件永远为 truefor 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)

Functionpack1 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。

函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。

目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。

函数参数与返回值

通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 returnpanic结尾。

  • 按值传递(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 种方案可以解决这个问题:

  1. 使用结构
    定义一个结构类型,假设它叫 Options,用以存储所有可能的参数:
    1
    2
    3
    4
    5
    type Options struct {
    par1 type1,
    par2 type2,
    ...
    }

函数 F1 可以使用正常的参数 a 和 b,以及一个没有任何初始化的 Options 结构: F1(a, b, Options {})。如果需要对选项进行初始化,则可以使用F1(a, b, Options {par1:val1, par2:val2})

  1. 使用空接口
    如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{},这样就可以接受任何类型的参数。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:
1
2
3
4
5
6
7
8
9
10
11
func typecheck(..,..,values … interface{}) {
for _, value := range values {
switch v := value.(type) {
case int: …
case float: …
case string: …
case bool: …
default: …
}
}
}

defer 和追踪

关键字 defer允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

当有多个 defer行为被注册时,它们会以逆序执行(类似栈,即后进先出)

关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:

  1. 关闭文件流

    1
    2
    // open a file  
    defer file.Close()
  2. 解锁一个加锁的资源

1
2
mu.Lock()  
defer mu.Unlock()
  1. 打印最终报告
1
2
printHeader()  
defer printFooter()
  1. 关闭数据库连接
1
2
// open a database connection  
defer disconnectFromDB()

内置函数

名称 说明
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 用于创建和操作复数

使用闭包调试

当在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtimelog 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

1
2
3
4
5
6
7
8
9
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

也可以设置 log 包中的 flag 参数来实现:

1
2
log.SetFlags(log.Llongfile)
log.Print("")

或使用一个更加简短版本的 where 函数:

1
2
3
4
5
6
7
8
var where = log.Print
func func1() {
where()
... some code
where()
... some code
where()
}
------ 本文结束 ------
0%