Go语言中 defer 的用法

Go语言中 defer 的用法

  defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放资源和连接、关闭文件、释放锁、捕获panic等。defer后面一般跟函数或方法,Go语言机制担保一定会执行defer语句中的代码。

一、defer触发时机

  Go官方文档中对defer的执行时机做了阐述,分别是:

  • 包裹defer的函数返回时
  • 包裹defer的函数执行到末尾时
  • 所在的goroutine发生panic时

二、defer执行逻辑

1、 多个defer语句按先进后出的方式执行

  多个 defer 调用顺序是先进后出,defer后的操作可以理解为压入栈中。
在这里插入图片描述

func main() {
	// 它是一个“栈”的关系,也就是先进后出
	defer func1() // 第三个执行
	defer func2() // 第二个执行
	defer func3() // 第一个执行
}
func func1() {
	fmt.Println("defer 1")
}
func func2() {
	fmt.Println("defer 2")
}
func func3() {
	fmt.Println("defer 3")
}

在这里插入图片描述

2、defer声明时,对应的参数会实时解析

  defer后面跟方法、有参函数、无参函数。

func test(a int) { //无返回值函数
	defer fmt.Println("defer1:a =", a)                    //方法
	defer func(v int) { fmt.Println("defer2:a =", v) }(a) //有参函数
	defer func() { fmt.Println("defer3:a =", a) }()       //无参函数
	a++
}
func main() {
	test(1)
}

在这里插入图片描述

解释:
  方法中的参数a,有参函数中的参数v,会请求参数,直接把参数代入,所以输出的都是1。a++变成2之后,无参函数中使用的a现在已经是2了,故输出2。

3、defer、return、返回值三者的执行逻辑

  return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值(可能和最初的返回值不相同)退出。

  当defer语句放在return后面时,就不会被执行。

return 的实现逻辑:
1、第一步给返回值赋值(若是有名返回值直接赋值,匿名返回值则先声明再赋值) ;
2、第二步调用RET返回指令并传入返回值,RET会检查是否存在defer语句,若存在就先逆序插播 defer语句 ;
3、最后 RET 携带返回值退出函数 。
可以看出, return 不是一个原子操作,函数返回值与 RET 返回值并不一定一致。

  defer在return之后执行,但在函数退出之前,defer可以修改返回值。

  • 匿名返回(返回值没有指定命名),执行 return 语句后,Go会创建一个临时变量保存返回值,defer修改的是临时变量,没有修改返回值。
func test() int {
	var i int
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	return i
}

func main() {
	fmt.Println("return:", test())
}

在这里插入图片描述

解释:
  返回值由变量 i 赋值,相当于 返回值 = i = 0。defer2 中 i++ = 1,所以defer2输出1;defer1 中 i++ = 2,所以defer1输出2,但是返回值已经被赋值了,即使后续修改 i 也不会影响返回值,所以最终返回 0 。

  • 命名返回(指定返回值命名func test() (t int)),执行 return 语句时,并不会再创建临时变量保存,defer修改的是返回值。
func test() (i int) {
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	return i
}

func main() {
	fmt.Println("return:", test())
}

在这里插入图片描述

解释:
  这里已经指明了返回值就是 i ,所以后续对 i 进行修改都相当于在修改返回值,所以最终返回2。

  • 函数返回值为指针,指向变量所在的地址,defer修改变量,指针指向的地址不变,地址对应的内容发生了改变,返回值改变。
func test() *int {
	var i int
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	return &i
}

func main() {
	fmt.Println("return:", *test())
}

在这里插入图片描述

解释:
  此时的返回值是一个指针(地址),这个指针 = &i,相当于指向变量 i 所在的地址,两个defer语句都对 i 进行了修改,那么返回值指向的地址的内容也发生了改变,所以最终返回2。

  • 特殊例子:defer没有修改有名返回值,因为 r 作为参数,传入defer 内部时会发生值拷贝,地址会变,defer修改的是新地址的变量,不是原来的返回值。
func test() (r int) {
	defer func(r int) {
		r++
		fmt.Println("defer:", r)
	}(r)
	return r
}

func main() {
	fmt.Println("return:", test())
}

在这里插入图片描述

解释:
  最初返回值 r = 0, r 作为参数传入defer 内部会发生值拷贝,相对于一个新的变量 r’,defer 内部内的语句相当于 r’ = r’ +5,r 并没有被修改,所以最终返回 0 。

4、defer 与 panic 的执行逻辑

  panic 其实是一个终止函数栈执行的过程,类似其它语言中的抛出异常,但是在函数退出前都会执行defer里面的函数,直到所有的函数都退出后,才会执行panic。
  在panic语句后面的defer语句不被执行,在panic语句前的defer语句会被执行(早于panic),panic触发defer出栈。可以在defer中使用recover捕获异常,panic 后依然有效。

func main() {
	defer fmt.Println("defer before panic")
	panic("panic")
	defer fmt.Println("defer after panic")
}

在这里插入图片描述

5、defer 与 recover

  recover 是 go 提供的一个用来截获 panic 信息,重新获取协程控制的函数。

使用要求:

  • recover 必须在 defer 函数中使用,但是不能被 defer 直接调用;
//以下捕获失败
defer recover()
defer fmt.Prinntln(recover)
defer func(){
    func(){
        recover() //无效,嵌套两层
    }()
}()

//以下捕获有效
defer func(){
    recover()
}()

func except(){
    recover()
}
func test(){
    defer except()
    panic("runtime error")
}
  • 多个 panic 仅有最后一个可以被 recover捕获。后面的 panic 会覆盖掉之前的。
func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	defer func() {
		panic("panic three")
	}()
	defer func() {
		panic("panic two")
	}()
	panic("panic one")
}

在这里插入图片描述

  • recover 只能恢复同一个协程中的 panic ,不能跨协程捕获panic 信息,所以 recover 必须与可能发生panic的协程在同一个协程中才生效。panic 在子协程中,而 recover 在主协程中,recover 捕获不到 panic,最终会导致所有的协程全部挂掉,程序会整体退出。

错误示例:

func Test() {
	panic("发生异常了!!!")
}

func main() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()

	go Test()
	time.Sleep(5 * time.Second)
	fmt.Println("main()不能正常执行!!!")
}

在这里插入图片描述

正确示例:

func Test() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	panic("发生异常了!!!")
}

func main() {
	go Test()
	time.Sleep(5 * time.Second)
	fmt.Println("main()能正常执行!!!") 
}

在这里插入图片描述

三、defer 其他用处

1、关闭文件

func main() {
	// 只读方式打开指定目录下的文件 os.Open()
	file, err := os.Open("C:\\Users\\lenovo\\Desktop\\test.txt")
	if err != nil {
		fmt.Println("open file failed!!! err: ", err)
		return
	}
	// 关闭文件 close()
	defer file.Close()
}

2、释放互斥锁

var mu sync.Mutex
var m = make(map[string]int)

func lookup(key string) int {
	mu.Lock()
	defer mu.Unlock()
	return m[key]
}

func main() {
	lookup("test")
}

四、defer 使用中的一些坑

坑1:defer在匿名返回值和命名返回值函数中的不同表现

  • 匿名返回,执行 return 语句后,Go会创建一个临时变量保存返回值,defer修改的是临时变量,没有修改返回值。
  • 命名返回,执行 return 语句时,并不会再创建临时变量保存,defer修改的是返回值。

坑2:在for循环中使用defer可能导致的性能问题

func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

  defer在紧邻创建资源的语句后执行,看上去逻辑没有什么问题,但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。

  所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。

坑3:判断执行没有err之后,再defer释放资源

  一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确写法:

resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

坑4:调用os.Exit时defer不会被执行

  当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}
// defer并不会输出

坑5:recover 不能跨协程捕获 panic 信息

  • recover 必须在 defer 函数中使用,但是不能被 defer 直接调用;
  • 多个 panic 仅有最后一个可以被 recover 捕获,后面的panic 会覆盖掉之前的;
  • recover 只能恢复同一个协程中的 panic ,不能跨协程捕获 panic 信息。
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值