理清 defer 的执行机制和参数相关问题
Golang defer
defer是注册延迟调用的机制,return 或者 panic 结束之后执行,多个defer后进先出.
Defer的运行时的参数确定
在 Go 中,defer 对外部变量的引用主要有 两种方式:
- 通过函数参数传值(值在 defer 声明时就确定)
- 通过闭包引用外部变量(值在函数执行时才读取)
1 通过函数参数(值在 defer 时就拷贝)
package main
import "fmt"
func main() {
x := 10
//这里 x 已经被拷贝到参数 v。
defer func(v int) {
fmt.Println("defer:", v)
}(x)
x = 20
fmt.Println("main:", x)
}
参数会立刻求值,如果参数是函数,那么会立刻执行函数里面的代码
输出
main: 20
defer: 10
会立即执行test()
func test() int {
fmt.Println("hello")
return 1
}
func main() {
defer fmt.Println(test())
fmt.Println("world")
}
输出
hello
world
1
2 闭包引用外部变量(运行时读取)
package main
import "fmt"
func main() {
x := 10
//闭包引用 x,最后读取 x=20
//闭包 捕获变量地址,不是值。
defer func() {
fmt.Println("defer:", x)
}()
x = 20
fmt.Println("main:", x)
}
输出
main: 20
defer: 20
与上面对应的延迟执行
func test() int {
fmt.Println("hello")
return 1
}
func main() {
defer func () {fmt.Println(test())}()
fmt.Println("world")
}
输出
world
hello
1
练习1
func test() (x int) {
defer func() {
x = x + 1
}()
defer func(x int) {
x = x + 1
}(x)
x = 1
return x
}
最后输出:2
第一步:进入函数
命名返回值初始化
x = 0
第二步:注册第一个 defer1
闭包引用外部变量 x,此时只是注册,不执行
第三步:注册第二个 defer2
这里参数会立即求值,设置参数 x = 0
第四步:执行代码
x=1
第五步:执行 return
设置返回值 x=1
第六步执行 defer2
x: 0 → 1,参数副本,不会影响外部 x
第七步执行 defer1
x: 1 → 2,这里引用的是 外部 x
第八步执行
返回x =2
练习2
func test() {
for i := 0; i < 3; i++ {
// 情况 A:无闭包(直接传参)
defer fmt.Print(i)
// 情况 B:有闭包(直接引用 i)
defer func() {
fmt.Print(i)
}()
}
}
func main() {
test()
}
输出
//如果是golang 1.22版本之前,每次迭代变量 i 地址都是一样的,因此闭包的使用的都是一个地址值为3
3 2 3 1 3 0
//如果是golang 1.22以及版本之后,每次迭代都会创建一个新的变量 i,每次迭代的 i 都有自己独立的内存地址
2 2 1 1 0 0
练习3
func f()(r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
输出 5
t = 5
return t 表示r=t
执行defer t= t+5=10
最后return r
延迟语句如何配合恢复语句
有些时候,需要从异常中恢复
panic 会停掉当前正在执行的程序,而不只是当前线程。 在这之前,它会有序地执行完当前线程 defer 列表里的语句,其他协程里定义的 defer 语句不作保证。所以在 defer 里定义一个 recover 语句,防止程序直接挂掉,就可以起到类似 Java 里 try…catch 的效果
func main() {
defer fmt.Println("defer main")
var user = os.Getenv("USER_")
go func() {
defer func() {
fmt.Println("defer caller")
//panic 最终会被 recover 捕获到
if err := recover(); err != nil {
fmt.Println("recover success, err: ", err)
}
}()
func() {
defer func() {
fmt.Println("defer here")
}()
if user == "" {
panic("should set user env")
}
fmt.Println("after panic")
}()
}()
time.Sleep(100)
fmt.Println("end of main function")
}
输出
defer here
defer caller
recover success. err: should set user env.
end of main function
defer main
注意事项
1.recover 只能在 defer 中生效
func main() {
panic("error")
r := recover() // 不会执行
fmt.Println(r)
}
2. recover 不能注册时执行
func main() {
//注册就调用了recover(),后续无法捕获到panic
defer fmt.Println(recover())
defer recover()
panic("error")
}
正确
defer func() {
fmt.Println(recover())
}()
3.recover 必须在同一个 goroutine
func main() {
defer func() {
//recover 只能捕获当前 goroutine 的 panic,无法执行
fmt.Println("recover:", recover())
}()
go func() {
panic("goroutine panic")
}()
time.Sleep(time.Second)
}