golang中的defer使用方式及实战技巧
golang是一门简洁、高效、并发友好的编程语言,它提供了许多独特的特性,让程序员可以更容易地编写优雅和健壮的代码。其中一个特性就是defer语句,它可以让我们在函数返回之前执行一些清理或收尾的操作,比如关闭文件、释放资源、解锁互斥量等。defer语句的语法很简单,就是在要延迟执行的函数或方法前加上关键字defer,如下所示:
func main() {
f, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close() // 延迟关闭文件
}
在这个例子中,我们打开了一个文件,并在函数返回之前延迟关闭它,这样可以确保不会因为忘记关闭文件而造成资源泄露。
一、多个defer的执行顺序
defer语句的执行顺序是按照后进先出(LIFO)的原则,也就是说,最后一个defer语句会最先执行,第一个defer语句会最后执行。例如:
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
}
这个程序的输出是:
C
B
A
二、参数赋值的时间
参数的赋值对于有的同学来说一只都是一个难题,那defer是什么时候确定参数的值的呢?我们先来比较下面两个语句输出的差异:
func main() {
x := 1
defer fmt.Println(x)
x = 2
}
这个程序的输出是:
1
x := 1
defer func() {
fmt.Println(x)
}()
x = 2
这个程序的输出是:
2
如果熟悉go异步编程的小伙伴应该不会觉得惊讶。原因defer 调用的函数参数的值在 defer 定义时就确定了, 而 defer 函数内部所使用的变量的值需要在这个函数运行时才确定。所以如果我们想在第二个例子中输出1
的话,需要做如下的改造:
func main() {
x := 1
defer func(x int) {
fmt.Println(x) // x = 1
}(x)
x = 2
}
三、执行表达式
除了可以延迟执行函数或方法外,defer语句还可以延迟执行任意的表达式,只要它们能被求值为一个函数调用。例如:
func main() {
defer println("Hello", "World") // 等价于 defer func() { println("Hello", "World") }()
}
这个程序的输出是:
Hello World
defer语句有很多实际的应用场景,下面我们介绍一些常见的使用技巧。
四、不可在循环中使用
defer 会延迟到函数返回前执行,如果函数不返回则不会执行,所以如果在一个循环中执行defer,可能并不会跟达到循环结束即释放资源的的预期,例如如下的函数便是一个错误的示例:
func readFiles(ch <-chan string) error {
for path := range ch {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// Do something with file
}
return nil
}
五、实际案例
1.延迟释放资源
这是最常见也最基本的使用技巧,就是在获取到某种资源后,立即使用defer语句来延迟释放它,这样可以避免因为忘记释放资源而导致内存泄露、文件占用、数据库连接过多等问题。例如,在打开文件、数据库连接、网络连接、互斥锁等场景中,都可以使用defer语句来保证资源的正确释放。
// 延迟释放文件资源
func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close() //延迟释放文件资源
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil
}
// 延迟关闭数据库连接
func dbQuery(db *sql.DB, query string) (*sql.Rows, error) {
conn, err := db.Conn(context.Background())
if err != nil {
return nil, err
}
defer conn.Close() // 延迟关闭数据库连接
rows, err := conn.QueryContext(context.Background(), query)
if err != nil {
return nil, err
}
return rows, nil
}
// 延迟关闭响应体
func httpGet(url string) (*http.Response, error) {
client := &http.Client{}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close() // 延迟关闭响应体
// do something with resp
return resp, nil
}
//延迟解锁
func lockMutex(mu *sync.Mutex) {
mu.Lock() // 加锁
defer mu.Unlock() // 延迟解锁
// do something with mu
}
2.延迟处理错误
有时候,我们可能需要在函数返回之前对某些错误进行处理,比如记录日志、发送通知、回滚事务等。这时候,我们可以使用defer语句来延迟处理错误,而不是在每个可能出错的地方都重复写相同的处理逻辑。例如:
func processFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
var result error // 用于保存最终的错误
defer func() {
if result != nil {
log.Println("process file failed:", result) // 延迟记录日志
}
}()
data, err := ioutil.ReadAll(f)
if err != nil {
result = err // 保存错误
return err
}
// do something with data
return nil
}
在这个例子中,我们使用了一个名为result的变量来保存最终的错误,然后在defer语句中判断result是否为空,如果不为空,就记录日志。这样,无论函数是正常返回还是提前返回,都可以保证错误被正确处理。
3.延迟修改返回值
在golang中,函数可以有多个返回值,而且可以给返回值命名。这样的话,我们就可以在defer语句中修改返回值,从而实现一些特殊的效果。例如:
func add(a, b int) (sum int) {
defer func() {
sum += a + b // 延迟修改返回值
fmt.Println("sum =", sum)
}()
return a + b
}
func main() {
fmt.Println(add(1, 2))
}
这个程序的输出是:
sum = 6
6
在这个例子中,我们给返回值命名为sum,并在defer语句中对它进行了修改。这样,函数的最终返回值就是修改后的值。这种技巧可以用来实现一些特殊的功能,比如计算函数的执行时间、跟踪函数的调用栈、实现异常捕获等。例如:
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
func slowFunc() {
time.Sleep(time.Second * 2)
}
func main() {
defer timeTrack(time.Now(), "slowFunc") // 延迟计算函数执行时间
slowFunc()
}
这个程序的输出是:
slowFunc took 2.000123456s
4.延迟执行匿名函数
有时候,我们可能需要在函数返回之前执行一些额外的操作,比如打印日志、释放资源、修改状态等。如果这些操作只需要在一个地方执行,那么我们可以直接写在函数的最后;但如果这些操作需要在多个地方执行,那么我们就可以使用defer语句来延迟执行一个匿名函数,从而避免重复代码。例如:
func processUser(id int) error {
user, err:= getUser(id)
if err != nil {
return err
}
defer func() {
log.Println("process user done:", user.Name) // 延迟打印日志
releaseUser(user) // 延迟释放用户
}()
// do something with user
return nil
}
在这个例子中,我们使用了一个匿名函数来延迟执行两个操作:打印日志和释放用户。这样,无论函数是正常返回还是提前返回,都可以保证这两个操作被执行。
5.延迟执行可变参数函数
有时候,我们可能需要在函数返回之前执行一些可变参数的函数,比如fmt.Println、log.Printf等。这时候,我们可以使用defer语句来延迟执行这些函数,而不需要额外定义一个匿名函数来包裹它们。例如:
func printSum(a, b int) {
defer fmt.Println("sum =", a + b) // 延迟打印和
}
func logError(err error) {
defer log.Printf("error: %v", err) // 延迟记录错误
}
在这个例子中,我们直接使用了defer语句来延迟执行fmt.Println和log.Printf,而不需要像下面这样写:
func printSum(a, b int) {
defer func() {
fmt.Println("sum =", a + b)
}()
}
func logError(err error) {
defer func() {
log.Printf("error: %v", err)
}()
}
这样可以让代码更简洁和清晰。
六、最后
用好defer,会大大提高我们的代码可读性和稳健性,但是不合理的使用也可能会出现灾难性的结果,在平时的编码过程中,我们需要特别注意defer的各种特性,这样才能写出优秀的代码。
#go##golang##高并发##defer#