引入

defer是Go语言中的一个关键字(延迟调用),一般用于释放资源和连接、关闭文件、释放锁等。和defer类似的有java的finally和C++的析构函数,这些语句一般是一定会执行的(某些特殊情况后文会提到),不过析构函数析构的是对象,而defer后面一般跟函数或方法。

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

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer fmt.Println(i)
    }
}

输出:

    4
    3
    2
    1
    0

所有的defer语句会放入栈中,在入栈的时候会进行相关的值拷贝(也就是下面的“对应的参数会实时解析”)。

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

简单示例:

package main

import "fmt"

func main() {
	i := 1
	fmt.Println("i =", i)
	defer fmt.Print(i)
}

输出:

i = 1
1

defer后面的语句最后才会执行,后面会讲当defer存在时return的执行逻辑。

辨析:defer后面跟无参函数、有参函数和方法:

package main

import "fmt"

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

输出:

3、a = 2
2、a = 1
1、a = 1

解释:

方法中的参数a,有参函数中的参数v,会请求参数,直接把参数代入,所以输出的都是1。a++变成2之后,3个defer语句以后声明先执行的顺序执行,无参函数中使用的a现在已经是2了,故输出2。

3、可读取函数返回值(return返回机制)

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

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

package main

import "fmt"

func f(i int) int{
	return i
	defer fmt.Print("i =", i)
	return i+1
}

func main() {
	f(1)
}

没有输出,因为return i之后函数就已经结束了,不会执行defer。
(1)无名返回值:

package main

import (
	"fmt"
)

func a() int {
	var i int
	defer func() {
		i++
		fmt.Println("defer2:", i) 
	}()
	defer func() {
		i++
		fmt.Println("defer1:", i) 
	}()
	return i
}

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

输出:

defer1: 1
defer2: 2
return: 0

解释:
返回值由变量i赋值,相当于返回值=i=0。第二个defer中i++ = 1, 第一个defer中i++ = 2,所以最终i的值是2。但是返回值已经被赋值了,即使后续修改i也不会影响返回值。最终返回值返回,所以main中打印0。

(2)有名返回值:

package main

import (
	"fmt"
)

func b() (i int) {
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	return i //或者直接写成return
}

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

输出:

defer1: 1
defer2: 2
return: 2

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

(3)函数返回值为地址

package main

import (
	"fmt"
)

func c() *int {
	var i int
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	return &i
}

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

输出:

defer1: 1
defer2: 2
return: 2

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

再看一个例子:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

最初返回值r的值是1,虽然defer语句中函数的参数名也叫r(这里我记作r’),但传参的时候相当于r‘=r(值传递),函数内的语句相当于r’=r‘+5,所以返回值r并没有被修改,最终的返回值仍是1。

4、defer与闭包

package main

import "fmt"

type Test struct {
	name string
}
func (t *Test) pp() {
	fmt.Println(t.name)
}
func main() {
	ts := []Test{{"a"}, {"b"}, {"c"}}
	for _, t := range ts {
		defer t.pp()
	}
}

输出:

c
c
c

解释:
for结束时t.name=“c”,接下来执行的那些defer语句中用到的t.name的值均为”c“。

修改代码为:

package main

import "fmt"

type Test struct {
	name string
}
func pp(t Test) {
	fmt.Println(t.name)
}
func main() {
	ts := []Test{{"a"}, {"b"}, {"c"}}
	for _, t := range ts {
		defer pp(t)
	}
}

输出:

c
b
a

解释:
defer语句中的参数会实时解析,所以在碰到defer语句的时候就把该时的t代入了。

再次修改代码:

package main

import "fmt"

type Test struct {
	name string
}
func (t *Test) pp() {
	fmt.Println(t.name)
}

func main() {
	ts := []Test{{"a"}, {"b"}, {"c"}}
	for _, t := range ts {
		tt := t
		println(&tt)
		defer tt.pp()
	}
}

输出:

0xc000010200
0xc000010210
0xc000010220
c
b
a

解释:

:=用来声明并赋值,连续使用2次a:=1就会报错,但是在for循环内,可以看出每次tt:=t时,tt的地址都不同,说明他们是不同的变量,所以并不会报错。每次都有一个新的变量tt:=t,所以每次在执行defer语句时,对应的tt不是同一个(for循环中实际上生成了3个不同的tt),所以输出的结果也不相同。

5、defer用于关闭文件和互斥锁

文件:

unc ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.close()
    return ReadAll()
}

互斥锁:

var mu sync.Mutex
var m = make(map[string]int)
 
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}

6、“解除”对所在函数的依赖

package main

import "fmt"
import "time"

type User struct {
	username string
}

func (this *User) Close() {
	fmt.Println(this.username, "Closed !!!")
}

func main() {
	u1 := &User{"jack"}
	defer u1.Close()
	u2 := &User{"lily"}
	defer u2.Close()
	time.Sleep(10 * time.Second)
	fmt.Println("Done !")

}

输出:

Done !
lily Closed !!!
jack Closed !!!

解释:
defer后面跟无参函数,u1.Close()和u2.Close()要等sleep和fmt.Println(“Done !”)之后才可以执行,也就是在函数最终返回之前执行。

修改代码为:

package main

import "fmt"
import "time"

type User struct {
	username string
}

func (this *User) Close() {
	fmt.Println(this.username, "Closed !!!")
}

func f(u *User) {
	defer u.Close()
}

func main() {
	u1 := &User{"jack"}
	f(u1)
	u2 := &User{"lily"}
	func() { defer u2.Close() }()

	time.Sleep(10 * time.Second)
	fmt.Println("Done !")
}

输出:

jack Closed !!!
lily Closed !!!
Done !

这样的使用方式,似乎不太合理,但却有存在的必要性。大多数情况下,可以用于 u1,u2 之类非常消耗内存,或者cpu,其后执行时间过程且没有太多关联的情况。既保留了defer的功能特性,也满足范围精确控制的条件!(算是奇技淫巧吧😂)

7、defer与panic

(1)在panic语句后面的defer语句不被执行

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

输出:

panic: panic
goroutine 1 [running]:
main.panicDefer()
    E:/godemo/testdefer.go:17 +0x39
main.main()
    E:/godemo/testdefer.go:13 +0x20
Process finished with exit code 2

可以看到 defer 语句没有执行。
(2)在panic语句前的defer语句会被执行

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

输出:

defer before panic
panic: panic
goroutine 1 [running]:
main.deferPanic()
    E:/godemo/testdefer.go:19 +0x95
main.main()
    E:/godemo/testdefer.go:14 +0x20
Process finished with exit code 2

defer 语句输出了内容。
Go中的panic类似其它语言中的抛出异常,panic后面的代码不再执行(panic语句前面的defer语句会被执行)。

8、调用os.Exit时defer不会被执行

func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

当调用os.Exit()方法退出程序时,defer并不会被执行,上面的defer并不会输出。

[参考]:

http://www.topgoer.com/函数/延迟调用defer.html

https://blog.csdn.net/eclipser1987/article/details/12089271

https://www.cnblogs.com/aiandbigdata/p/10822123.html

https://blog.csdn.net/chr1991/article/details/104771526?utm_medium=distribute.pc_relevant.none-task-blog-title-1&spm=1001.2101.3001.4242

https://www.jianshu.com/p/79c029c0bd58

https://blog.csdn.net/qq_21816375/article/details/78161603

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐