聊一聊 go 的 defer 关键字

237 阅读2分钟

1. 为什么使用 defer

假设我们有个需求,需要拷贝文件,可以使用下面的函数

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

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

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

上面的函数有一个 bug, 如果 dstName 创建失败, 程序就不会关闭 srcName, 从而导致资源泄露。我们可以使用 defer 关键字来解决这个问题, 下面的函数可以保证所有打开的文件都会被关闭

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

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

    return io.Copy(dst, src)
}

2. 什么是 defer

defer 延迟执行一个函数或方法调用, 执行的时机为所在函数执行 return、所在函数体执行完、所在 goroutine 发生 panic。defer 语句的执行有三个简单的原则:

  1. defer 函数的参数是在 defer 语句出现时赋值, 而不是在 defer 函数执行的时候赋值
func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
} // output: 0
  1. defer 所在函数体执行完之后,defer 声明的函数按照 “后进先出” 的顺序执行
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
} // output: 3210
  1. defer 声明的函数可以读取并赋值所在函数声明的返回值
func c() (i int) {
    defer func() { i++ }()
    return 1
} output: 2

3. defer 是怎么运行的

首先看下这几个例子, 来介绍下 defer 声明函数的执行时机:

func c(i int64) int64 {
   defer func() {
      i++ // output: 2
   }()
   return i
} // input: 1 output: 1

func d(i *int64) *int64 {
   defer func() {
      *i++
   }()
   return i
} // input: 1 output: 2

func e(i int64) (res int64) {
   defer func() {
      res++ //output: 2
   }()
   return i
} // input: 1 output: 2

func h(i int64) (res int64) {
   defer func(res int64) {
      res++ // output: 1
   }(res)
   return i
} // input: 1 output: 1

func k() {
   defer func() {
      fmt.Println("first defer func")
   }()
   panic("")
   defer func() {
      fmt.Println("second defer func")
   }()
}

从上面的 case 可以得出 defer 函数执行的规则

  1. defer 函数如果传值, 参数的值在 defer 语句出现时就已经确定, 发生值拷贝
  2. 多个 defer 函数按照后进先出的顺序执行
  3. defer 函数是一个闭包函数, 可以直接访问和修改所在函数的入参和出参
  4. defer 函数执行在 return 语句赋值返回值之后,返回函数调用者之前
  5. 如果 defer 函数出现在 panic 之后,则无法被执行

4. defer 的使用场景

  • 资源释放 如下面的代码所示, 利用 defer 来释放文件句柄
func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

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

    return io.Copy(dst, src)
}
  • 捕获 panic
    在函数的入口捕获 panic, 防止程序因 panic 而退出, 下面是在 gin 框架加了一个中间件
func PanicHandler() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      defer func() {
         if err := recover(); err != nil {
            ctx.Abort()
            logs.CtxError(ctx, "path: %s meet err: %v stack \n[%s]", ctx.Request.URL.Path, err, string(debug.Stack()))
            ctx.JSON(http.StatusInternalServerError, nil)
         }
      }()
      ctx.Next()
   }
}