09 函数
09 函数
函数是 Go 语言中的一等公民,支持多返回值、闭包、defer 等高级特性,在这门语言的开发中,基本上都是函数式编程,因此本节非常重要。
函数定义与调用
Go 使用 func
关键字定义函数:
func 函数名(参数列表) (返回值列表) {
...
}
基本示例
func add(x int, y int) int {
return x + y
}
// 多返回值需要用 () 包裹
func swap(a string, b string) (string, string) {
return b, a
}
func main() {
result := add(3, 5)
x, y := swap("hello", "world")
fmt.Println(result, x, y) // 8 world hello
}
参数类型简写
相邻同类型参数可以省略类型,会默认推导为后面写类型的那个参数的类型:
func calc(x, y int) int { return x + y }
可变参数
使用 ...T
表示可变参数,本质是切片,和 cpp 的展开/收集运算符是一样的,如果有多个参数,可变参数必须放在最后:
func sum(nums ...int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}
命名返回值
可以为返回值命名,函数内直接使用,本质上是直接 把返回值初始化为变量:
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // 自动返回命名变量,而不需要写具体的
}
函数类型作为变量
Go 中函数也是一种类型,可以赋值给变量:
type Calculator func(int, int) int
func main() {
var calc Calculator = add
result := calc(3, 5) // 8
}
高阶函数
函数可以作为参数,此时的函数被称为回调函数,也是非常经典的:
func operate(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
result := operate(10, 5, add)
fmt.Println(result) // 15
}
函数也可以作为返回值:
func makeAdder(base int) func(int) int {
return func(x int) int {
return base + x
}
}
func main() {
add10 := makeAdder(10)
fmt.Println(add10(5)) // 15
}
匿名函数
函数内部不能定义具名函数,只能定义匿名函数,多用于实现回调和闭包:
func main() {
// 立即执行函数
func(name string) {
fmt.Println("Hello", name)
}("World")
// 保存到变量
multiply := func(x, y int) int {
return x * y
}
fmt.Println(multiply(3, 4)) // 12
}
闭包
闭包可以理解为一个定义在函数内部的函数,它的本质也很简单,它包含两个部分: 一个是函数指针,指向定义的新函数,另外一个则是引用环境,这个引用环境会把所有捕获的外部变量从栈逃逸到堆区,从而确保即使调用处结束了,也可以使用外部变量。
因此闭包可以概括为: 函数 + 引用环境。
func counter() func() int {
count := 0 // 被闭包捕获的变量
return func() int {
count++ // 修改外部变量
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
}
也正是因为外部变量会逃逸到堆上,因此多个闭包实例会共享操作,和切片一样,也要注意操作的传递性。
在学闭包时不得不看的一个经典例子:
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
funcs[i] = func() int {
return i
}
}
你觉得打印出来会是什么?事实上,全部是 3,因为函数体内的这个 i 在循环结束时都变成了 3,这也是闭包在循环中的问题,正确的做法应该是这样的:
for i := 0; i < 3; i++ {
i := i
funcs[i] = func() int {
return i
}
}
defer
defer
可以延迟执行函数调用,遵循LIFO(后进先出)顺序,被 defer 定义的表达式会做两种行为:
- 如果有参数,立即计算所有参数的值,然后存入 defer 栈
- 如果没有参数,则直接存入 defer 栈
然后 defer 语句在何时执行呢? 一个函数的返回语句并不是原子的,分为 返回值赋值,函数返回 两步,defer 语句会在这两步之间进行,按照存入的 defer 栈弹出执行。
可以看一下如下几个例子,理解为什么输出结果是这样的话就没问题了:
func example() {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
fmt.Println("function body")
}
// 输出:
// function body
// third
// second
// first
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
// 5 6 5 5
defer 语句在编译时会转换为对 runtime.deferproc
的调用,将延迟函数信息存储在 goroutine 的 defer 链表中。函数返回时,运行时遍历链表执行这些函数。
panic 和 recover
Go 使用 panic/recover
处理运行时错误,此时需要遵守如下要求:
- recover 只在 defer 函数中有效
- 只能捕获当前 goroutine 的 panic
- 必须在 panic 的调用栈中
func safeDivide(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
result = 0
}
}()
if b == 0 {
panic("division by zero")
}
return a / b
}