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 execute0func executedefer 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 3B 10 2 12BB 10 12 22AA 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.Mutexvar 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语句的执行// 并不影响返回值 因而返回5func f1() int {    x := 5    defer func() {        x++    }()    return x}// 指定了返回值为变量x 因而return 5 理论执行为 x = 5 x++ return x 因而返回6func f2() (x int) {    defer func() {    // --5--    fmt.Printf("--%d-- \n", x)        x++    }()    return 5}// 指定返回变量为y return x 理论执行的是 y = x x++ return y 因而返回5func 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语言圣经
  • 李文周的博客