Skip to content

Go Defer用法

Posted on:2019年11月16日 at 16:00 (6 min read)

0x00 defer 使用测试

先来看下面测试中的函数,分别返回什么?

func Test1() int {
	result := 0
	defer func() {
		result++
	}()
	return result
}

func Test2() (result int) {
	defer func() {
		result++
	}()
	return
}

func Test3() (result int){
	defer func(result int) {
		result++
	}(result)
	result = 0
	return
}

对 slice 和 map 使用 defer 时,返回值会更具迷惑性,在看下面几个函数之前,先回顾下slice的特性。

slice 的底层是用数组实现的,是对数组一个连续片段的引用,所以 slice 也是引用类型。如果追加的数组元素超过其容量,则会分配一个新的地址给这个数组。

// slice数据结构定义
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

img

现在,再看下面几个函数分别返回什么,他们的 cap 和 len 分别是多少?

func Slice1() []int {
	mp := make([]int , 0)
	defer func() {
		mp = append(mp, 2)
	}()
	mp = append(mp, 1)
	return mp
}

func Slice2() []int {
	mp := make([]int ,0, 2)
	defer func() {
		mp = append(mp, 2)
	}()
	mp = append(mp, 1)
	return mp
}

func Slice3() []int {
	mp := make([]int, 0, 2)
	defer func(mp []int) {
		mp = append(mp, 2)
	}(mp)
	mp = append(mp, 1)
	return mp
}

func Slice4() (mp []int) {
	mp = make([]int, 0)
	defer func() {
		mp = append(mp, 2)
	}()
	mp = append(mp, 1)
	return mp
}

func Slice5() (mp []int) {
	mp = make([]int, 0, 1)
	defer func() {
		mp = append(mp, 2)
		mp = append(mp, 3)
	}()
	mp = append(mp, 1)
	return
}

下面是 Test 和 Slice 函数的答案,是否符合你的预期呢。

test1: 0
test2: 1
test3: 0

slice1: [1] len: 1 cap: 1
slice2: [1] len: 1 cap: 2
slice3: [2] len: 1 cap: 2
slice4: [1 2] len: 2 cap: 2
slice5: [1 2 3] len: 3 cap: 4

0x01 defer 使用规则

defer 是在 return 之前执行的,而 return xxx 这一条语句并不是一条原子指令。函数返回的过程是这样的:先给返回值赋值,然后调用 defer 表达式,最后才是返回到调用函数中。

因此函数Slice2Slice5可以被修改为下面的结构,相对的RSlice2RSlice5的返回内容就更容易看出了。

func RSlice2() []int {
	mp := make([]int ,0, 2)
	mp = append(mp, 1)
	ret := mp // 对返回结果赋值
	func() {
		mp = append(mp, 2)
	}()
	return ret
}

func RSlice5()(mp []int)  {
	mp = make([]int, 0, 1)
	mp = append(mp, 1)
	// 有名返回值,不会再重新赋值
	func() {
		mp = append(mp, 2)
		mp = append(mp, 3)
	}()
	return mp
}

defer 的使用规则,可以总结如下:

0x02 defer 适用场景

defer 可以看做是 golang 提供的语法糖,以 gin 框架为例,看下 defer 的几个常用的场景。

1.资源回收

由于 defer 在函数返回前才执行,因此可以讲资源回收的操作在代码上提前,和资源创建的代码对应起来,以免遗漏。

// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	return err
}

但是使用 defer 关闭资源时,如果不注意也很容易留下坑,来看下面一段代码。

这段代码的问题是如果 paths 很大,defer 会一直不去释放打开的文件资源,导致文件句柄耗尽。 改动的办法是讲文件打开和关闭的逻辑抽离到单独的函数中,保证资源及时释放。

func TestFileOpen(paths []string) {
    for _,path := range paths{
        file, err := os.Open(path)
		if err != nil {
			panic(err)
		}
		defer file.Close()
       // 业务处理...
    }
}

2.捕获panic

golang 没有提供常见的 try catch 功能,而是通过 defer 和 panic 来实现类似的异常捕获功能。 实际应用中,例如一个 http 服务器,某个 http 请求的异常,通常不应挂掉整个 http 服务,因此 gin 提供了recover中间件,在出现异常时,记录日志并返回特定的错误信息给前端。

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// 日志记录等...
			}
		}()
		c.Next()
	}
}

3.修改返回值

这块不再举例,可以参考文档开始的几个示例 :)

0xFF 参考文档