• Go语言中合理使用 defer 提高代码可读性和可靠性
  • 发布于 1个月前
  • 38 热度
    0 评论
defer
defer 语句经常用于成对的操作,比如 打开文件/关闭文件 连接网络/断开网络, 合理地使用 defer 不仅可以提高代码可读性,也降低了忘记释放资源造成的泄漏等问题。正确使用 defer 语句的地方是在成功获取资之后。

断开网络连接
package main

import (
    "log"
    "net/http"
)

func main() {
    resp, err := http.Get("https://www.duidaima.com")
    
    // 此时资源有可能获取失败,执行 Close 导致 panic 
    // resp.Body.Close()
    
    if err != nil {
        panic(err)
    }
    
    defer func() {
        err = resp.Body.Close() // 关闭资源
        if err != nil {
            log.Fatal(err)
        }
    }()
}
关闭文件句柄
package main

import (
    "os"
)

func main() {
    name := "/etc/hosts"
    file, err := os.Open(name)
    // 堆代码 www.duidaima.com
    // 此时资源有可能获取失败,执行 Close 导致 panic
    // file.Close()
    
    if err != nil {
        panic(err)
    }

    defer func() {
        err = file.Close() // 关闭文件句柄
        if err != nil {
            panic(err)
        }
    }()

    hosts := make([]byte, 1024)
    _, err = file.Read(hosts)
    if err != nil {
        panic(err)
    }
}

计算程序耗时
package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    // 堆代码 www.duidaima.com
    // 错误的写法,等于注册 defer 函数的时候就已经计算好了输出值
    // defer fmt.Printf("executed time (%s)\n", time.Since(start))
    
    // 正确的写法
    defer func() {
        fmt.Printf("executed time (%s)\n", time.Since(start))
    }()

    time.Sleep(3 * time.Second) // 模拟程序耗时
}

// $ go run main.go
// 输出如下
/**
  executed time (3.0021534s)
*/
defer 不会执行的情况
os.Exit 会直接退出程序,不用调用已经注册的 defer 函数。
package main

import "os"

func main() {
    println("hello world")

    defer func() {
        println("hello defer")
    }()

    os.Exit(0)
}

// $ go run main.go
// 输出如下 
/**
  hello world
*/
通过输出结果可以看到,defer 并未执行,字符串 hello defer 没有输出,原因在于: 调用 os.Exit() 函数之后,程序会立即终止, 所有后面的代码和 defer 函数都不会执行。

recover
recover 函数调用有着严格的要求,必须在 defer 函数中直接调用 recover,否则 panic 将无法被捕获。如果 defer 函数中调用的是经过包装的 recover 函数,panic 将同样无法被捕获。

recover 必须在 defer 中调用
错误的做法
package main
import "fmt"
func main() {
    if r := recover(); r != nil { // 无法捕获到 panic
        fmt.Printf("panic = %v\n", r)
    }
    panic("some error")
}

// $ go run main.go
// 输出如下
/**
  panic: some error

  ...
  ...

  exit status 2

*/
正确的做法
将 recover 函数放置在 defer 函数中调用。
package main
import "fmt"
func main() {
    defer func() {
        if r := recover(); r != nil { // 可以捕获到 panic
            fmt.Printf("panic = %v\n", r)
        }
    }()

    panic("some error")
}

// $ go run main.go
// 堆代码 www.duidaima.com
// 输出如下 
/**
  panic = some error
*/
recover 必须在 defer 中直接调用
错误的做法
package main

import "fmt"

func myRecover() {
    if r := recover(); r != nil { // 无法捕获到 panic
        fmt.Printf("panic = %v\n", r)
    }
}

func main() {
    defer func() {
        myRecover()
    }()

    panic("some error")
}

// $ go run main.go
// 输出如下
/**
  panic: some error

  ...
  ...

  exit status 2

*/

错误的原因在于: defer 以匿名函数的方式运行,本身就等于包装了一层函数, 内部的 myRecover 函数包装了 recover 函数,等于又加了一层包装,变成了两层包装,这时最外层的 panic 就无法被捕获了。

正确的做法 - 1
defer 直接调用 myRecover 函数,这样减去了一层包装,panic 就可以被捕获了。
package main
import "fmt"
func myRecover() {
    if r := recover(); r != nil { // 无法捕获到 panic
        fmt.Printf("panic = %v\n", r)
    }
}

func main() {
    defer myRecover()

    panic("some error")
}

// $ go run main.go
// 输出如下 
/**
  panic = some error
*/
正确的做法 - 2
将 recover 函数放置在 defer 函数中调用,panic 就可以被捕获了。
package main
import "fmt"
func main() {
    defer func() {
        if r := recover(); r != nil {
           // 堆代码 www.duidaima.com
           // 可以捕获到 panic
            fmt.Printf("panic = %v\n", r)
        }
    }()

    panic("some error")
}

// $ go run main.go
// 输出如下 
/**
  panic = some error
*/

多个 panic 只有一个被捕获
错误的做法
package main

import "fmt"

func foo() {
    defer func() {
        println("recover 1")
        if err := recover(); err != nil { // 无法捕获到 panic
            fmt.Printf("[1] recovered %d\n", err)
        }
    }()

    defer func() {
        println("recover 2")
        if err := recover(); err != nil { // 无法捕获到 panic
            fmt.Printf("[2] recovered %d\n", err)
        }
    }()

    defer func() {
        println("recover 3")
        if err := recover(); err != nil { // 可以捕获到 panic
            fmt.Printf("[3] recovered %d\n", err)
        }
    }()

    defer func() {
        println("panic 1")
        panic(1)
    }()

    defer func() {
        println("panic 2")
        panic(2)
    }()

    defer func() {
        println("panic 3")
        panic(3)
    }()
}

func main() {
    foo()
}

// $ go run main.go
// 输出如下 
/**
  panic 3
  panic 2
  panic 1
  recover 3
  [3] recovered 1
  recover 2
  recover 1
*/
通过输出结果可以看到,即使抛出了多个 panic, 也只有最后一个被捕获。因为第一个 recover 函数执行完后,会影响到后面的 recover 函数 (第一个 recover 捕获错误后,后面的 recover 不会捕获到任何错误)。

正确的做法
如果希望抛出的多个 panic 全部被捕获,应该在 recover 函数执行完后再依次执行 panic, 需要保证 panic -> recover -> panic -> recover ... 这样的链式关系。
package main
import "fmt"
func foo() {
    defer func() {
        println("recover 1")
        if err := recover(); err != nil { 
         // 堆代码 www.duidaima.com
         // 可以捕获到 panic
            fmt.Printf("[1] recovered %d\n", err)
        }
    }()

    defer func() {
        println("recover 2")
        if err := recover(); err != nil { // 可以捕获到 panic
            fmt.Printf("[2] recovered %d\n", err)
            panic(err) // 捕获的同时继续抛出
        }
    }()

    defer func() {
        println("recover 3")
        if err := recover(); err != nil { // 可以捕获到 panic
            fmt.Printf("[3] recovered %d\n", err)
            panic(err) // 捕获的同时继续抛出
        }
    }()

    defer func() {
        println("panic 1")
    }()

    defer func() {
        println("panic 2")
    }()

    defer func() {
        println("panic 3")
        panic(3)
    }()
}

func main() {
    foo()
}

// $ go run main.go
// 输出如下 
/**
  panic 3
  panic 2
  panic 1
  recover 3
  [3] recovered 3
  recover 2
  [2] recovered 3
  recover 1
  [1] recovered 3
*/
小结
• 正确使用 defer 语句的地方是在成功获取资之后
• os.Exit 会直接退出程序,不用调用已经注册的 defer 函数
• recover 必须在 defer 函数中调用且必须直接调用
• 多个 panic 注册后,如果 recover, 那么只有 1 个 panic 会被捕获
用户评论