Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang defer延迟语句

Golang defer延迟语句的实现

作者:hcraM41

defer拥有注册延迟调用的机制,本文主要介绍了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

解析:222 、111 是在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) 后增加更多的逻辑,这是完全不可控的。

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

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

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

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

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

其中,1 和 3 是return 语句生成的指令,2 是defer 语句生成的指令。可以看出:

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

解析:错误recover ,recover 需要在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 住 子goroutine 的 panic 。

原因是,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 ,从而保证能够recover 住panic 。但是这个方案并不完美,如果开发人员不使用GoSafe 函数来创建goroutine ,而是自己创建,并且在代码中出现了panic ,那么仍然会造成程序崩溃。

到此这篇关于Golang defer延迟语句的实现的文章就介绍到这了,更多相关Golang defer延迟语句内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文