5. 函数
5.1 特性
- 不支持:重载、嵌套函数和默认参数
- 支持:无需声明原型、不定长度变参、多返回值、命名返回值参数、匿名函数和闭包
5.2 多返回值
在Go中,一个函数可以返回多个值。
一个函数内部可以将另一个有多返回值的函数作为返回值。
可以将一个返回多参数的函数作为该函数的参数。
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
func add(a, b, c int) (d, e int) {
d = a+b
e = b+e
return
}
上述代码中,如果d,e如果没有被修改,那么就返回零值。
5.3 错误
很难保证一个函数能够没有错误的运行,所以一定要将可能预见的错误进行返回。
如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。
导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型。error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。
处理错误的五种策略
运行中的错误有很多种,对于不同的错误应该采取不同的策略
- 最常用的方式是传播错误。也就是从底层函数不断的返回,并且在这个过程中给错误加上一些信息,确保错误最后传回main函数的时候能够分析出错误从哪里传导出来的。
- 偶然错误,进行重试。 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
- 程序无法继续运行,输出错误信息并结束程序。 需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
- 有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数
- 直接忽略掉错误。用于即便有错误也不会影响程序的整体情况。
5.4 函数值
函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。
函数类型的零值是nil。调用值为nil的函数值会引起panic错误
函数值之间是不可比较的,也不能用函数值作为map的key。
举个简单有趣的例子
func add(x, y int) int { return x+y}
func sub(x, y int) int { return x-y}
func op(x, y int, f func(int, int) int) int {
x++
y--
return f(x, y)
}
func main() {
x, y := 10, 7
fmt.Println(op(x, y, add))
fmt.Println(op(x, y, sub))
}
在上面函数op中f就是一个函数值。当然函数值也是可以像下面一样操作的。
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int
5.5 匿名函数
匿名函数从名字上的意义来看就是没有名字的函数。它的一个重要的应用场景就是闭包:比如函数A中定义了匿名函数B,并将匿名函数B做参数返回,其中B可以对A中的变量进行操作。所以在函数A外部接收到B,便可以通过B来对A中变量进行操作。
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
一个更难但更有趣的闭包:
func A(x int) (func(int) int, func(int) int) {
A := func(i int) int {
x += i
return x
}
B := func(i int) int {
x -= i
return x
}
return A, B
}
func main() {
add, sub := A(10)
fmt.Println(add(10)) // 10+10=20
fmt.Println(sub(2)) // 20-2=18
}
捕获迭代变量
虑这个样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
5.6 可变参数
参数数量可变的函数称为为可变参数函数。
可变参数一定要在函数参数列表中的最后一个。
func dsum(d int, args ...int) int {
result := 0
for _, v := range args {
result += v
}
return d * result
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(dsum(2, arr...)) // 30
// 对于数组可以利用切片生成切片
arr1 := [...]int{1, 2, 3, 4, 5}
fmt.Println(dsum(2, arr1[:]...)) // 30
}
5.7 defer
特性:
- 执行的方式类似于其他语言的析构函数,在函数体执行结束后,按照调用的顺序的相反顺序逐个执行
- 即使函数发生严重错误也会执行
- 支持匿名函数的调用
- 常用于资源清理,文件关闭,解锁以及记录时间等操作
- 通过与匿名函数结合可在return之后修改函数计算结果
- 如果函数体内某个变量作为defer时匿名函数的参数,则在定义defer即已经获得拷贝,否则则是引用了某个变量的地址
func main() {
for i:=0; i<3; i++ {
defer fmt.Println(i)
}
}
结果为:2,1,0.符合上面所述特性的第一点
不过使用defer有一个问题,就是上面所提到的最后一点,可以看看下面的程序,尝试下想想可能会发生的结果:
func main() {
for i:=0; i<3; i++ {
defer func() {
fmt.Println(i)
}()
}
for i:=0; i<3; i++ {
defer func(i int) {
fmt.Println(i)
}(i) // 传参
}
}
结果:
2
1
0
3
3
3
首先,在结果中输出的前三个变量是第二个for的结果,后面是三个是第一个for的结果。(不明白的回去看上面的例子,结合特性的第一点)
第一个for之所以会输出3个3,是因为这里产生了闭包,使用的i是func外的变量i,所以地址并没有改变。
而第二个for中定义的函数其实自己声明了变量,所以将外部的变量i传参给func的时候是进行了值拷贝,所以func内部的i和外部的i是两个地址。
分析一下下面函数,看看是不是和自己预期的一样:
func main() {
var fs = [4]func(){}
for i:=0; i<4; i++ {
defer fmt.Println("defer i = ", i)
defer func() {fmt.Println("defer_closure i = ", i)}()
fs[i] = func() {fmt.Println("closure i = ", i)}
}
for _ , f := range fs {
f()
}
}
结果:
closure i = 4
closure i = 4
closure i = 4
closure i = 4
closure i = 4
defer i = 3
closure i = 4
defer i = 2
closure i = 4
defer i = 1
closure i = 4
defer i = 0
5.8 panic和recover
go语言中使用panic来触发错误。一旦触发panic,之后所有的操作都会暂停。
func A() {
fmt.Println("A")
}
func B() {
fmt.Println("B")
panic("panic in B")
}
func C() {
fmt.Println("C")
}
func main() {
A()
B()
C()
}
结果如下:可以看到C函数并不会被执行
A
B
panic: panic in B
在触发panic函数之前有defer函数了,那么被defer的函数能够在触发panic之后正常运行完。
例如将上面的函数B改写为以下:
func B() {
fmt.Println("B")
defer func() {
fmt.Println("Func in B")
}()
panic("panic in B")
}
结果如下:
A
B
Func in B
panic: panic in B
可以看到defer的匿名函数会在panic之前执行,C函数并不会被执行
在编程中,我们能够预想到一些错误,这些错误能通过处理后能够保证程序继续正确运行而不会中断,那么可以使用recover,使即便触发panic程序依然能够继续执行。
继续修改上面的函数B:
func B() {
fmt.Println("B")
defer func() {
if err := recover(); err!=nil {
fmt.Println("recover from panic:", err)
}
}()
panic("panic in B")
}
上述代码中, err是panic的内容,整个程序运行结果如下:
A
B
recover from panic: panic in B
C
可以看到程序恢复正常运行,并且运行完整个流程了。
主要参考资料:《Go语言圣经》
撩我?
我的公众号:Kyda