Golang defer延迟语句详解

一、defer的简单使用

defer 拥有注册延迟调用的机制,defer 关键字后面跟随的语句或者函数,会在当前的函数return 正常结束 或者 panic 异常结束 后执行。

但是defer 只有在注册后,最后才能生效调用执行,return 之后的defer 语句是不会执行的,因为并没有注册成功。

如下例子:

func main() {
	defer func() {
		fmt.Println(111)
	}()

	fmt.Println(222)
	return

	defer func() {
		fmt.Println(333)
	}()
}

执行结果:

222
111

解析:222111 是在return 之前注册的,所以如期执行,333 是在return 之后注册的,注册失败,执行不了。

defer 在需要资源释放的场景非常有用,可以很方便地在函数结束前执行一些操作。

比如在 打开连接/关闭连接 、加锁/释放锁、打开文件/关闭文件 这些场景下:

file, err := os.Open("1.txt")
if err != nil {
    panic(err)
}
if file != nil {
    defer file.Close()
}

这里要注意的是:在调用file.Close() 之前,需要判断file 是否为空,避免出现异常情况。

再来看一个错误示范,没有正确使用defer 的例子:

player.mu.Lock()
rand.Intn(number)
player.mu.Unlock()

这三行代码,存在两个问题:
1. 中间这行代码 rand.Intn(number) 是有可能发生panic 的,这就会导致没有正常解锁。
2. 这样的代码在项目中后续可能被其他人修改,在rand.Intn(number) 后增加更多的逻辑,这是完全不可控的。

LockUnlock 之间的代码一旦出现 panic ,就会造成死锁。因此,即使逻辑非常简单,使用defer 也是很有必要的,因为需求总在变化,代码也总会被修改。


二、defer的函数参数与闭包引用

defer 延迟语句不会马上执行,而是会进入一个栈,函数return 前,会按先进后出的顺序执行。

先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面的先执行了,那么后面函数的依赖就没有了,就可能会导致出错。

defer 函数定义时,对外部变量的引用有三种方式:值传参、指针传参、闭包引用。

  1. 值传参:在defer 定义时就把值传递给defer ,并复制一份cache起来,defer调用时和定义的时候值是一致的。
  2. 指针传参:在defer 定义时就把指针传递给defer ,defer调用时根据整个上下文确定参数当前的值。
  3. 闭包引用:在defer 定义时就把值引用传递给defer ,defer调用时根据整个上下文确定参数当前的值。

下面通过例子加深一下理解。

例子1:

func main() {
	var arr [4]struct{}

	for i := range arr {
		defer func() {
			fmt.Println(i)
		}()
	}
}

执行结果:

3
3
3
3

解析:因为defer 后面跟着的是一个闭包,根据整个上下文确定,for 循环结束后i 的值为3,因此最后打印了4个3。

例子2:

func main() {
	var n int

	// 值传参
	defer func(n1 int) {
		fmt.Println(n1)
	}(n)

	// 指针传参
	defer func(n2 *int) {
		fmt.Println(*n2)
	}(&n)

	// 闭包
	defer func() {
		fmt.Println(n)
	}()

	n = 4
}

执行结果:

4
4
0

解析:

defer 执行顺序和定义的顺序是相反的;

第三个defer 语句是闭包,引用的外部变量n ,defer调用时根据上下文确定,最终结果是4;

第二个defer 语句是指针传参,defer调用时根据整个上下文确定参数当前的值,最终结果是4;

第一个defer 语句是值传参,defer调用时和定义的时候值是一致的,最终结果是0;

例子3:

func main() {
	// 文件1
	f, _ := os.Open("1.txt")
	if f != nil {
		defer func(f io.Closer) {
			if err := f.Close(); err != nil {
				fmt.Printf("defer close file err 1 %v\n", err)
			}
		}(f)
	}

	// 文件2
	f, _ = os.Open("2.txt")
	if f != nil {
		defer func(f io.Closer) {
			if err := f.Close(); err != nil {
				fmt.Printf("defer close file err 2 %v\n", err)
			}
		}(f)
	}

	fmt.Println("success")
}

执行结果:

success

解析:先说结论,这个例子的代码没有问题,两个文件都会被成功关闭。这个是对defer 原理的应用,因为defer 函数在定义的时候,参数就已经复制进去了,这里是值传参,真正执行close() 函数的时候就刚好关闭的是正确的文件。如果不把f 当做值传参,最后两个close() 函数关闭的就是同一个文件了,都是最后打开的那个文件。

例子3的错误示范:

func main() {
	// 文件1
	f, _ := os.Open("1.txt")
	if f != nil {
		defer func() {
			if err := f.Close(); err != nil {
				fmt.Printf("defer close file err 1 %v\n", err)
			}
		}()
	}

	// 文件2
	f, _ = os.Open("2.txt")
	if f != nil {
		defer func() {
			if err := f.Close(); err != nil {
				fmt.Printf("defer close file err 2 %v\n", err)
			}
		}()
	}

	fmt.Println("success")
}

执行结果:

success
defer close file err 1 close 2.txt: file already closed

例子4:

// 值传参
func func1() {
	var err error
	defer fmt.Println(err)
	err = errors.New("func1 error")
	return
}

// 闭包
func func2() {
	var err error
	defer func() {
		fmt.Println(err)
	}()
	err = errors.New("func2 error")
	return
}

// 值传参
func func3() {
	var err error
	defer func(err error) {
		fmt.Println(err)
	}(err)
	err = errors.New("func3 error")
	return
}

// 指针传参
func func4() {
	var err error
	defer func(err *error) {
		fmt.Println(*err)
	}(&err)
	err = errors.New("func4 error")
	return
}

func main() {
	func1()
	func2()
	func3()
	func4()
}

执行结果:

<nil>
func2 error
<nil>
func4 error

解析:

第一个和第三个函数中,都是作为参数,进行值传参,err 在定义的时候就会求值,因为定义的时候值都是nil ,所以最后的结果都是nil

第二个函数的参数在定义的时候也求值了,但是它是个闭包,查看上下文发现最后值被修改为func2 error

第四个函数是指针传参,最后值被修改为func4 error

现实中,第三个函数闭包的例子是比较容易犯的错误,导致最后defer 语句没有起到作用,造成生产上的事故,需要特别注意。


三、defer的语句拆解

从返回值出发来拆解延迟语句 defer

return xxx

这条语句经过编译之后,实际上生成了三条指令:

1. 返回值 = xxx
2. 调用 defer 函数
3. 空的 return

其中,13return 语句生成的指令,2defer 语句生成的指令。可以看出:

return 并不是一条原子指令;defer 语句在第二步调用,这里可能操作返回值,从而影响最终结果。

接下来通过例子来加深理解。

例子1:

func func1() (r int) {
	t := 3
	defer func() {
		t = t + 3
	}()

	return t
}

func main() {
	r := func1()
	fmt.Println(r)
}

执行结果:

3

语句拆解:

func func1() (r int) {
	t := 3

	// 1.返回值=xxx:赋值指令
	r = t

	// 2.调用defer函数:defer在赋值与返回之前执行,这个例子中返回值r没有被修改过
	func() {
		t = t + 3
	}()

	// 3.空的return
	return
}

func main() {
	r := func1()
	fmt.Println(r)
}

解析:因为第二个步骤里并没有操作返回值r ,所以最终得到的结果是3

例子2:

func func2() (r int) {

	defer func(r int) {
		r = r + 3
	}(r)

	return 1
}

func main() {
	r := func2()
	fmt.Println(r)
}

执行结果:

1

语句拆解:

func func2() (r int) {

	// 1.返回值=xxx:赋值指令
	r = 1

	// 2.调用defer函数:因为是值传参,所以修改的r是个复制的值,不会影响要返回的那个r值。
	func(r int) {
		r = r + 3
	}(r)

	// 3.空的return
	return
}

func main() {
	r := func2()
	fmt.Println(r)
}

解析:因为第二个步骤里改变的是传值进去的r 值,是一个形参的复制值,不会影响实参r ,所以最终得到的结果是1

例子3:

func func3() (r int) {

	defer func() {
		r = r + 3
	}()

	return 1
}

func main() {
	r := func3()
	fmt.Println(r)
}

执行结果:

4

语句拆解:

func func3() (r int) {

	// 1.返回值=xxx:赋值指令
	r = 1

	// 2.调用defer函数:因为是闭包,捕获的变量是引用传递,所以会修改返回的那个r值。
	func() {
		r = r + 3
	}()

	// 3.空的return
	return
}

func main() {
	r := func3()
	fmt.Println(r)
}

解析:因为第二个步骤里改变的r 值是闭包,闭包中捕获的变量是引用传递,不是值传递,所以最终得到的结果是4


四、defer中的recover

代码中的panic 最终会被recover 捕获到。在日常开发中,可能某一条协议的逻辑触发了某一个bug 造成panic ,这时就可以用recover 去捕获panic ,稳住主流程,不影响其他协议的业务逻辑。

需要注意的是,recover 函数只在defer 的函数中直接调用才生效。

通过例子看recover 调用情况。

例子1:

func func1() {
	if err := recover(); err != nil {
		fmt.Println("func1 recover", err)
		return
	}
}

func main() {
	defer func1()
	panic("func1 panic")
}

执行结果:

func1 recover func1 panic

解析:正确recover ,因为在defer 中调用的,所以可以生效。

例子2:

func main() {
	recover()
	panic("func2 panic")
}

执行结果:

panic: func2 panic

goroutine 1 [running]:
main.main()
        C:/Users/ycz/go/ccc.go:5 +0x31
exit status 2

解析:错误recover ,直接调用recover ,返回nil

例子3:

func main() {
	defer recover()
	panic("func3 panic")
}

执行结果:

panic: func3 panic

goroutine 1 [running]:
main.main()
        C:/Users/ycz/go/ccc.go:5 +0x65
exit status 2

解析:错误recoverrecover 需要在defer 的函数里调用。

例子4:

func main() {
	defer func() {
		defer func() {
			recover()
		}()
	}()
	panic("func4 panic")
}

执行结果:

panic: func4 panic

goroutine 1 [running]:
main.main()
        C:/Users/ycz/go/ccc.go:9 +0x49
exit status 2

解析:错误recover ,不能在多重defer 嵌套里调用recover

另外需要注意的一点是,goroutine 无法 recover 住 子goroutinepanic

原因是,goroutine 被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他goroutine 共享任何的数据。

也就是说,无法让goroutine 拥有返回值,也无法让goroutine 拥有自身的ID 编号。

如果希望有一个全局的panic 捕获中心,那么可以通过channel 来实现,如下示例:

var panicNotifyManage chan interface{}

func StartGlobalPanicRecover() {
	panicNotifyManage = make(chan interface{})
	go func() {
		select {
		case err := <-panicNotifyManage:
			fmt.Println("panicNotifyManage--->", err)
		}
	}()
}

func GoSafe(f func()) {
	go func() {
		defer func() {
			if err := recover(); err != nil {
				panicNotifyManage <- err
			}
		}()
		f()
	}()
}

func main() {
	StartGlobalPanicRecover()
	f1 := func() {
		panic("f1 panic")
	}
	GoSafe(f1)
	time.Sleep(time.Second)
}

解析:GoSafe() 本质上是对go 关键字进行了一层封装,确保在执行并发单元前插入一个defer ,从而保证能够recoverpanic 。但是这个方案并不完美,如果开发人员不使用GoSafe 函数来创建goroutine ,而是自己创建,并且在代码中出现了panic ,那么仍然会造成程序崩溃。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值