Go中defer语句解析

defer语句解析流程

  • defer语句就是defer关键字后跟一个函数调用语句defer关键字的作用在于对关键字之后的函数调用语句进行延迟执行。当执行到defer语句时,函数和参数表达式得到计算,但是直到该语句所在的函数执行完毕时,defer语句才会执行

    • 所谓的函数执行完毕包含return结束或者是因为panic结束
    • 函数中可以有多个defer语句,函数返回时,defer语句执行的顺序与生命顺序相反
    • 当执行到defer语句时,函数和参数表达式得到计算,参考下边的两个例子

      // DeferTest defer语句测试
      func DeferTest() {
          var i int
          defer func(a int) {
              i = 12
              fmt.Println("defer execute")
          }(test())
          fmt.Println(i)
          fmt.Println("func execute")
      }
      
      func test() int {
          fmt.Println("test execute")
          return 0
      }
      test execute
      0
      func execute
      defer execute
      • 执行到defer语句时首先执行必要的参数表达式的计算,但是并不执行函数
      func BigSlowOperation() {
          // 不要丢了括号!
          defer trace("bigSlowOperation")()
          // ...
          time.Sleep(10 * time.Second)
      }
      
      // trace 函数跟踪,可以跟踪函数的进入和所有情况下的函数退出
      // 使用了闭包的特性
      func trace(msg string) func() {
          start := time.Now()
          log.Printf("enter %s", msg)
          return func() {
              log.Printf("exit %s (%s)", msg, time.Since(start))
          }
      }
      • 执行到defer语句时首先解析函数值,因此在函数进入时首先调用trace函数,函数退出时执行trace返回的函数值,这其中的计时又使用了闭包的特性
      func calc(index string, a, b int) int {
          ret := a + b
          fmt.Println(index, a, b, ret)
          return ret
      }
      
      /*
      A 1 2 3
      B 10 2 12
      BB 10 12 22
      AA 1 3 4
      */
      func main() {
          x := 1
          y := 2
        // 1. 首先执行参数中的calc("A", x, y) 输出 A 1 2 3 然后注册defer 语句 calc("AA", 1, 3)
          defer calc("AA", x, calc("A", x, y))
          x = 10
        // 2. 执行calc("B", x, y) 输出 B 10 2 12 然后注册defer语句 calc("BB", 10, 12)
          defer calc("BB", x, calc("B", x, y))
          y = 20
        // 3. 按照次序执行defer语句
        // 4. calc("BB", 10, 12) 输出 BB 10 12 22
        // 5. calc("AA", 1, 3) 输出 AA 1 3 4
      }
      • 注意区分defer语句的解析(保存上下文状态)与defer函数的执行
  • 由于defer语句延迟执行的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等

    • 资源的释放是一个函数中的重要任务,要考虑到每一种分支情况下的资源释放,但是当分支变得复杂的时候,资源的释放代码则变得冗余且容易丢失,因此使用defer语句是最佳选择
    • 一般情况下,执行资源创建之后,马上跟一个defer语句,以避免遗忘

      // ReadFile 读取文件
      func ReadFile(filename string) ([]byte, error) {
          file, err := os.Open(filename)
          if err != nil {
              return nil, err
          }
          defer file.Close()
          return io.ReadAll(file)
      }
      // 定义互斥锁
      var mu sync.Mutex
      var m = make(map[string]int)
      func lookup(key string)int {
          mu.Lock()
          // 上锁后马上定义defer锁的释放
          defer mu.Unlock()
          return m[key]
      }

defer执行时机

  • 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。defer语句执行的时机就在返回值赋值操作后,RET指令执行前

    image-20210922120121719

  • 结合defer语句的执行时机与函数中的匿名函数值可以读取函数中的上下文状态这一特性,可以使用defer 匿名函数自执行语句的形式观察函数的返回值

    func Double(a int) (result int) {
        defer func() { fmt.Printf("dounle(%d) is %d", a, result) }()
        return a * a
    }
    • 对于有许多return语句的函数来说,使用这种技巧去跟踪函数返回值很有用
  • 结合上边的观察函数返回值的使用方式,defer语句可以进一步的直接修改函数返回值

    func Triple(a int) (result int) {
        defer func() {
            result *= a
            fmt.Printf("triple(%d) is %d", a, result)
        }()
        return Double(a)
    }

注意事项

  • 下边给出几组案例,分析defer使用时的注意事项

返回值变量的影响

func main() {
  // 5
    fmt.Println(f1())
  // 6
    fmt.Println(f2())
  // 5
    fmt.Println(f3())
  // 5
    fmt.Println(f4())
  // 5
    fmt.Println(f5())
}

// 未指定返回参数,可以认为返回值应是一个临时变量,执行return x 时实际执行的是: temp := x x++ return temp所以defer语句的执行
// 并不影响返回值 因此返回5
func f1() int {
    x := 5
    defer func() {
        x++
    }()
    return x
}

// 指定了返回值为变量x 因此return 5 实际执行为 x = 5 x++ return x 因此返回6
func f2() (x int) {
    defer func() {
    // --5--
    fmt.Printf("--%d-- \n", x)
        x++
    }()
    return 5
}

// 指定返回变量为y return x 实际执行的是 y = x x++ return y 因此返回5
func f3() (y int) {
    x := 5
    defer func() {
        x++
    }()
    return x
}

// 解析defer语句时,已经确定变量x为初始化妆台即为0,返回值变量x设置为5后,执行匿名函数
// 此时匿名函数内部的局部变量被赋值为0,与返回值变量无关
func f4() (x int) {
    defer func(x int) {
    // --0--
    fmt.Printf("--%d-- \n", x)
        x++
    }(x)
    return 5
}

// 进一步验证f4的观点
func f5() (x int) {
  x = 1
    defer func(x int) {
    // --1--
    fmt.Printf("--%d-- \n", x)
        x++
    }(x)
    return 5
}

循环中使用defer

  • 注意不要在for循环中使用defer,可能会造成资源泄漏

    for _, filename := range filenames {
        f, err := os.Open(filename)
        if err != nil {
            return err
        }
        defer f.Close() // NOTE: risky; could run out of file descriptors
        // ...process f…
    }
    • 处在循环内的defer语句在函数结束前不会执行,因此在每一个循环中使用的文件描述符都不会被回收,直达全部文件遍历完,函数结束,或者文件描述符耗尽报错异常退出
  • 推荐的解决方法是将循环中资源操作的部分封装到一个单独的函数中,在循环中调用该函数即可

    for _, filename := range filenames {
        if err := doFile(filename); err != nil {
            return err
        }
    }
    func doFile(filename string) error {
        f, err := os.Open(filename)
        if err != nil {
            return err
        }
        defer f.Close()
        // ...process f…
    }

使用defer处理资源关闭

  • 实际上在很多情况下,使用defer语句处理资源关闭时,都是放弃了对于资源关闭时的异常的检查,因为实际上资源关闭的失败大部分情况下不是程序的错误,而是底层的某些错误,并且这些未回收的资源实际上可以交付给操作系统去回收,因此也就没有必要将错误信息返回给调用者,但是在某些场景下则不同:

    // Fetch 下载url对应的HTML页面并保存到本地
    // 返回文件名与文件长度以及可能的错误
    func Fetch(url string) (filename string, n int64, err error) {
        response, err := http.Get(url)
        if err != nil {
            return "", 0, err
        }
        defer response.Body.Close()
        // 取链接路径的最后一段作为文件名
        local := path.Base(response.Request.URL.Path)
        if local == "/" {
            local = "index.html"
        }
        file, err := os.Create(local)
        if err != nil {
            return "", 0, err
        }
        n, err1 := io.Copy(file, response.Body)
        if closeError := file.Close(); err1 == nil {
            err = closeError
        }
    
        return local, n, err
    
    }
    • 通过os.Create打开文件进行写入,在关闭文件时,没有对f.close采用defer机制,因为这会产生一些微妙的错误

      • 许多文件系统,尤其是NFS写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copyf.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质

参考


demoli
16 声望0 粉丝

bug创建者


« 上一篇
Java泛型总结
下一篇 »
Docker基础知识