深入Go语言 - 9

内建函数、导入、panic

目录 [−]

  1. 内建函数
  2. import
  3. 包的初始化
  4. error、panic和recover

本章介绍Go语言的其它部分,算是Go语言规范的深入学习的收尾工作。

内建函数

前面几章我们已经认识了几个内建的函数,这里我们将所有的内建函数罗列一下。

如果你查看它们的定义的话,你可以访问builtin/builtin.go,但这些内建的函数只有声明,没有方法体,它们不是标准的Go类型,你不能把它们作为函数值进行参数传递,只能出现在调用表达式中。事实上这些内建类型并不真的在builtin包中,只是为了生成文档的需要。

你可以在任意的包中调用这些内建函数,不必引入特定的包如"builtin"。

1、close: 关闭channel
2、len(s):得到字符串、数组、数值指针、slice、map、chan的长度
3、cap(s):得到数组、数组指针的长度,得到slice、channel的容量
4、new(T): 生成类型T的零值指针,注意它返回的是指针 *T
5、make: 生成slice、map、channel对象

调用              类型 T     结果

make(T, n)       slice      slice of type T with length n and capacity n
make(T, n, m)    slice      slice of type T with length n and capacity m

make(T)          map        map of type T
make(T, n)       map        map of type T,初始可以容纳 n 元素的空间

make(T)          channel    不带缓存的channel of type T,比如我们在声明信号channel的时候
make(T, n)       channel    带缓存的 channel of type T, 缓存大小为 n

6、append(s S, x ...T) S: 增加0到n个元素到slice中,返回新的slice
7、copy(dst, src []T) int: 复制源src slice的元素到目标dst slice中,返回复制的元素的数量n, n是src和dst长度的最小值。字符串也可以作为src,但是T的类型必须是byte
8、delete(m,k): 删除map中的一个映射, m为nil或者m[k]不存在也不会panic,而是一个空操作
9、complexrealimag: 复数操作
10、panicrecover: 报告panic和处理panic,后面讲
11、printprintln: 尽量不用这两个函数,因为保证将来它们还会留在Go语言中,使用fmt.Print、fmt.Println

Go的代码文件中都会有包(package)的定义,在import声明的前面。

同一个文件夹下的所有的文件都要使用同一个包名(当然,你如果单独编译每一个文件的话,可以不遵守,但是要编译整个项目,必须遵守)。

但是测试文件可以叫另外的包名,比如正常代码的包名为"package abc",测试代码和示例代码的包名为"package abc_test". Go标准库中混用了这两种风格。

main包是一个特殊的包,必须声明一个main函数,main函数无参数,无返回值,它用来创建可执行程序。

包名不一定和文件夹的名字保持一致,经常我们的项目的名称很长,不太适合做包名,所以包名可以用一个简短的名称,但是如果可能,尽量用一样的名字,这样在下载库的时候就能直到包的名字了。

import

import用来引入所需要的类型,允许你访问另外的包中的导出类型。

以下四种形式都是可以接受的mport:

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin
import _ "lib/math"

为了避免同名的包名冲突,你可以为导入的包名起一个名字,比如上例中的"m"。
你也可以使用.,这样无需包名标识符,可以直接使用这个包下的导出类型。
最后一个情况是使用空标识符,主要是想利用包的初始化,而不是使用它的导出类型。

import不能导入包自己,不管是直接的还是间接的(循环引用)。你也不能直接导入一个包而不使用它的导出类型,所幸一些工具可以自动帮我们修正import的错误,或者自动帮我们导入,比如goimports

import可以导入相对路径,如"import \"../foo/bar\"",但是强烈你不要这么做,这不是常用的导入风格。

import可以用小括号括起来导入多个包。

包的初始化

包变量的初始化顺序和它们声明的顺序一致,但是也得考虑它们的依赖。
循环依赖初始化也不可以:

1
2
3
4
5
package abc
var i int = j
var j int = k
var k int = i

包变量的初始化之后可以调用一个init函数实现其它的初始化过程,你可以显示地定义这个函数,它可以出现在同一个包下的多个文件中,执行的顺序由编译器决定。
如果包导入了其它包,则导入的包会先初始化。如果多个包都导入同一个包,则导入的包只被初始化一次。

error、panic和recover

Go预定义了error类型,虽然它的首字母不是大写的,但是确可以在任何包下使用:

1
2
3
type error interface {
Error() string
}

而errors包定义了生成简单Error的方法errors.New(text String) error。如果你想自定义Error类型,你可以实现error接口。

运行时Error,比如数组索引越界会触发一个运行时的panic,它等价调用panic函数,panic的值是一个runtime.Error:

1
2
3
4
5
6
package runtime
type Error interface {
error
// and perhaps other methods
}

而panic的处理是在一个defer函数中执行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
defer func() {
fmt.Println("foo")
}()
defer func() {
if x := recover(); x != nil {
fmt.Println(x)
}
}()
defer func() {
fmt.Println("bar")
}()
panic("trigger panic")
fmt.Println("end")
}

注意defer的执行顺序,前面已经讲过,和它们声明的顺序相反,所以输出结果为:

1
2
3
bar
trigger panic
foo

最后一句没有机会继续执行,因为recover执行完后函数就终止了。
这带来一个问题,如果函数有返回值,recover后函数的返回值是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
i := z()
fmt.Println(i)
}
func z() int {
defer func() {
fmt.Println("foo")
}()
defer func() {
if x := recover(); x != nil {
fmt.Println(x)
}
}()
defer func() {
fmt.Println("bar")
}()
panic("trigger panic")
fmt.Println("end")
return 100
}

输出结果是:

1
2
3
4
bar
trigger panic
foo
0

可以函数的返回是返回类型的零值。

当然说零值也不完全正确,如果函数有命名的返回参数,并且命名的返回参数在panic之前赋值了的话,返回的结果还是最后的赋值结果,下面的代码中函数的返回结果为50:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
i := z()
fmt.Println(i)
}
func z() (r int) {
defer func() {
fmt.Println("foo")
}()
defer func() {
if x := recover(); x != nil {
fmt.Println(x)
}
}()
defer func() {
fmt.Println("bar")
}()
r = 50
panic("trigger panic")
fmt.Println("end")
return 100
}

panic如果没有处理,会传递给它的调用者,这就是panic的bubble。当然如果可以预见panic,最好的处理方式就是在本函数内进行处理,因为你不能控制外部调用者的行为,而且外部调用者不一定知道会有panic发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
defer func() {
if x := recover(); x != nil {
fmt.Println("recover from main:", x)
}
}()
f1()
}
func f1() {
fmt.Println("start f1")
f2()
fmt.Println("end f1")
}
func f2() {
fmt.Println("start f2")
f3()
fmt.Println("end f2")
}
func f3() {
fmt.Println("start f3")
panic("triggered from f3")
fmt.Println("end f3")
}

上面的代码输出:

1
2
3
4
start f1
start f2
start f3
recover from main: triggered from f3

如果连main都没有 recover,则程序异常退出。

还有一个不太引人注意的地方就是如果在recover中产生panic会怎么样?还是看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
defer func() {
if x := recover(); x != nil {
fmt.Println("recover from main:", x)
}
}()
f1()
}
func f1() {
defer func() {
if x := recover(); x != nil {
fmt.Println("recover from f1:", x)
panic("triggered from f1")
}
}()
f2()
}
func f2() {
panic("triggered from f2")
}

函数f2产生的panic会被函数f1处理, f1在recover的过程中产生一个新的panic,这个panic会把它的调用者main捕获。
所以recover产生的panic会往上传递。