《Go语言圣经》学习笔记:5.函数

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表示失败。

处理错误的五种策略

运行中的错误有很多种,对于不同的错误应该采取不同的策略

  1. 最常用的方式是传播错误。也就是从底层函数不断的返回,并且在这个过程中给错误加上一些信息,确保错误最后传回main函数的时候能够分析出错误从哪里传导出来的。
  2. 偶然错误,进行重试。 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
  3. 程序无法继续运行,输出错误信息并结束程序。 需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
  4. 有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数
  5. 直接忽略掉错误。用于即便有错误也不会影响程序的整体情况。

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
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章