理清 defer 的执行机制和参数相关问题

Golang defer

defer是注册延迟调用的机制,return 或者 panic 结束之后执行,多个defer后进先出.

Defer的运行时的参数确定

在 Go 中,defer 对外部变量的引用主要有 两种方式

  1. 通过函数参数传值(值在 defer 声明时就确定)
  2. 通过闭包引用外部变量(值在函数执行时才读取)

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)
}