乐趣区

关于go:Go中defer语句解析

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 指令执行前

  • 联合 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 产生,更有可能靠近问题的实质

参考

  • Go 官网文档
  • Go 语言圣经
  • 李文周的博客
退出移动版