深入解析Golang中的defer延迟语句

文章目录

  • 一、defer的简单使用
  • 二、defer的函数参数与闭包引用
  • 三、defer的语句拆解
  • 四、defer中的recover
  • 一、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 ,那么仍然会造成程序崩溃。

    作者:hcraM41

    物联沃分享整理
    物联沃-IOTWORD物联网 » 深入解析Golang中的defer延迟语句

    发表回复