【go从入门到精通】探索延迟调用(defer)用法和陷阱

作者简介:

        高科,先后在 IBM PlatformComputing从事网格计算,淘米网,网易从事游戏服务器开发,拥有丰富的C++,go等语言开发经验,mysql,mongo,redis等数据库,设计模式和网络库开发经验,对战棋类,回合制,moba类页游,手游有丰富的架构设计和开发经验。 (谢谢你的关注)-------------------------------------------------------------------------------------------------------------------------------
         从名字就可以推断出,Go编程语言中使用的defer关键字是用来延迟执行一个要执行的函数(方法)的。

了解 Defer 及其目的

        在许多编程语言中,defer类似于ensure或 之类的概念finally。它允许设置一个函数调用,无论当前函数如何退出,无论是由于成功执行还是panic,该函数调用都将被执行。主要目的defer是提高代码的可读性、可维护性和资源管理。通过defer清理任务,你可以保持代码井井有条,并确保有效处理资源密集型操作。

defer的使用

使用的语法defer很简单:

defer functionCall(arguments)

defer每当调用包含语句的函数时,指定的语句functionCall不会立即执行。相反,它被添加到延迟函数列表中,一旦封闭函数返回,这些函数将以相反的顺序执行。

首先我们来看下这段代码:

package main
import "fmt"
func strWorld() string {
 return "World!"
}
func strHello() string {
 return "Hello"
}
func main() {
 fmt.Println("Let's check defer keyword")
 defer fmt.Println(strWorld())
 fmt.Println(strHello())
}

结果如下:

Let's check defer keyword
Hello
World!

 defer 具有类似堆栈的 LIFO(List In Fist Out)结构,因此后面添加的 defer 会先执行。

 如果将示例代码更改如下:

package main
import "fmt"
func strWorld() string {
 return "World!"
}
func strWorld2() string {
 return "World 2"
}
func strHello() string {
 return "Hello"
}
func main() {
 fmt.Println("Let's check defer keyword")
 defer fmt.Println(strWorld())
 fmt.Println(strHello())
 defer fmt.Println(strWorld2())
}

结果变化如下所示。

Let's check defer keyword
Hello
World 2
World!

可以看到先执行strWorld2(),后执行strWorld()。

因panic而推迟

defer也可以在优雅地处理panic方面发挥至关重要的作用。当发生panic时,defer函数在程序终止之前仍然会执行。可以利用它来执行必要的清理任务或在从panic中恢复时记录有用的信息。下面是一个代码片段来演示这一点:

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    panic("Something went terribly wrong!")
}

在此示例中,延迟函数捕获紧急消息并在程序panic之前打印它,为调试提供有价值的信息。

defer方法调用

defer语句不仅适用于函数,也适用于方法调用。你可以推迟方法的执行,就像使用常规函数一样。当你想要在方法执行结束时执行操作(无论方法的结果如何)时,这非常有用。让我们看一个片段来演示这一点:

package main 

import  "fmt" 

type Counter struct { 
    count int
 } 

func  (c *Counter) Increment() { 
    c.count++ 
    defer c.printCount() 
} 

func  (c *Counter) printCount() { 
    fmt.Println( "当前计数:" , c.count) 
} 

func  main () { 
    counter := &Counter{} 
    counter.Increment() 
}

在此示例中,该printCount方法在方法内被延迟Increment。因此,计数将在Increment方法执行结束时打印。

defer特性:

根据刚才的代码示例,我们对defer的特性做一个简单的归纳总结: 

    1. 关键字 defer 用于注册延迟调用。
    2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
    3. 多个defer语句,按先进后出的方式执行。
    4. defer语句中的变量,在defer声明时就决定了。

defer用途:

    1. 关闭文件句柄
    2. 锁资源释放
    3. 数据库连接释放
    4. 一些异常捕获

让我们深入研究一个代码示例,演示如何defer在 Go 中进行文件句柄管理。

package main 

import ( 
    "fmt" 
    "os"
 ) 

func  main () { 
    f := createFile( "/tmp/defer.txt" ) 
    defer closeFile(f) 
    writeFile(f) 
} 

func  createFile (p string ) *os.File { 
    fmt.Println( "创建" ) 
    f, err := os.Create(p) 
    if err != nil { 
        panic (err) 
    } 
    return f 
} 

func  writeFile (f *os.File) { 
    fmt.Println( "写入" ) 
    fmt.Fprintln(f, "data" ) 
} 

func  closeFile (f *os.File) { 
    fmt.Println( "关闭" ) 
    err := f.Close() 
    if err != nil { 
        fmt.Fprintf(os .Stderr, "错误: %v\n" , err) 
        os.Exit( 1 ) 
    } 
}

当运行此代码时,你将看到以下输出:

创建
写入
结束

Defer 如何简化资源管理

在给定的示例中,我们创建一个文件,向其中写入数据,并在写入完成后关闭该文件。我们来分解一下这个过程:

  1. 我们调用该createFile函数,它返回一个文件对象并将“creating”打印到控制台。
  2. 在退出之前createFile,我们推迟 的执行closeFile(f)。这确保了当所有其他语句都已执行时,该closeFile函数将在封闭函数 ( ) 的末尾被调用。main
  3. 接下来,我们调用该writeFile函数,它将“数据”写入文件并将“写入”打印到控制台。
  4. 最后,当main即将返回时,closeFile(f)执行 defer,在控制台打印“close”,并有效地关闭文件。

使用 Defer 处理错误

关闭文件或释放其他资源时,正确处理错误至关重要。即使在延迟函数中,也必须检查错误以防止任何意外行为。在我们的示例中,该closeFile函数在使用 关闭文件时检查错误f.Close()。如果发生错误,则会显示该错误,并且程序将退出并显示错误代码。

此错误处理可确保程序运行可靠,并提供足够的信息来诊断和修复清理期间发生的问题。

defer的陷阱

defer的功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

defer 碰上闭包

因此如果先前面的资源先释放了,后面的语句就没法执行了。

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
} 

输出结果:

    4
    3
    2
    1
    0

我们改成闭包,看看 

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer func() { fmt.Println(i) }()
    }
} 

输出结果:

    4
    4
    4
    4
    4
其实go说的很清楚,函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.

defer f.Close

这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.Close()
    }
} 

输出结果:

    c  closed
    c  closed
    c  closed

这个输出并不会像我们预计的输出c b a,而是输出c c c

可是按照前面的go spec中的说明,应该输出c b a才对啊.

那我们换一种方式来调用一下.

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func Close(t Test) {
    t.Close()
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer Close(t)
    }
} 

输出结果:

    c  closed
    b  closed
    a  closed

这个时候输出的就是c b a

当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a

看似多此一举的声明

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        t2 := t
        defer t2.Close()
    }
} 

输出结果:

    c  closed
    b  closed
    a  closed
通过以上例子,可以得出下面的结论:

使用 defer 的函数的参数是在执行 defer 语句时计算的,而不是在调用函数时计算的defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

package main

func test(x int) {
    defer println("a")
    defer println("b")

    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()

    defer println("c")
}

func main() {
    test(0)
} 

输出结果:

    c
    b
    a
    panic: runtime error: integer divide by zero

*延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

package main

func test() {
    x, y := 10, 20

    defer func(i int) {
        println("defer:", i, y) // y 闭包引用
    }(x) // x 被复制

    x += 10
    y += 100
    println("x =", x, "y =", y)
}

func main() {
    test()
}  

输出结果:

    x = 20 y = 120
    defer: 10 120

*滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

package main

import (
    "fmt"
    "sync"
    "time"
)

var lock sync.Mutex

func test() {
    lock.Lock()
    lock.Unlock()
}

func testdefer() {
    lock.Lock()
    defer lock.Unlock()
}

func main() {
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            test()
        }
        elapsed := time.Since(t1)
        fmt.Println("test elapsed: ", elapsed)
    }()
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            testdefer()
        }
        elapsed := time.Since(t1)
        fmt.Println("testdefer elapsed: ", elapsed)
    }()

}

输出结果:

    test elapsed:  223.162µs
    testdefer elapsed:  781.304µs

defer 与 文件关闭

让我们演示一个文件复制的例子:函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

上面的代码虽然能够工作,但是隐藏一个bug。如果第一个os.Open调用成功,但是第二个os.Create调用失败,那么会在没有释放src文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加src.Close()调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过defer语句来确保每个被正常打开的文件都能被正常关闭:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

defer语句可以让我们在打开文件时马上思考如何关闭文件。不管函数如何返回,文件关闭语句始终会被执行。同时defer语句可以保证,即使io.Copy发生了异常,文件依然可以安全地关闭。

defer 与 return

package main

import "fmt"

func foo() (i int) {

    i = 0
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

func main() {
    foo()
}

输出结果:

    2

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。

defer nil 函数

package main

import (
    "fmt"
)

func test() {
    var run func() = nil
    defer run()
    fmt.Println("runs")
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    test()
} 

输出结果:

runs
runtime error: invalid memory address or nil pointer dereference

解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

在错误的位置使用 defer

当 http.Get 失败时会抛出异常。

package main

import "net/http"

func do() error {
    res, err := http.Get("http://www.google.com")
    defer res.Body.Close()
    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
} 

输出结果:

    panic: runtime error: invalid memory address or nil pointer dereference

因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常

解决方案

总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer

package main

import "net/http"

func do() error {
    res, err := http.Get("http://xxxxxxxxxx")
    if res != nil {
        defer res.Body.Close()
    }

    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
} 

在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。

解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

不检查错误

在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer f.Close()
    }

    // ..code...

    return nil
}

func main() {
    do()
}  

改进一下

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                // log etc
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
} 

再改进一下

通过命名的返回变量来返回 defer 内的错误。

package main

import "os"

func do() (err error) {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {
                err = ferr
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
} 

释放相同的资源

如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。

package main

import (
    "fmt"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }

    return nil
}

func main() {
    do()
} 

输出结果:
defer close book.txt err close ./another-book.txt: file already closed

当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭

解决方案:

package main

import (
    "fmt"
    "io"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }(f)
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }(f)
    }

    return nil
}

func main() {
    do()
} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/547636.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flink network buffer

Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一&#xff0c;是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask)&#xff0c;因此&#xff0c;对于 Flink 作业的性能包括吞吐与延迟都至关重要。与 TaskManager 和 JobManager 之间通过基于 A…

Linux标准c库操作(4.15)

fopen函数“const char *mode”参数选项。 结果&#xff1a; 标准库c写入结构体到文件&#xff1a; #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <string.h> #in…

如何在Vue3中使用H.265视频EasyPlayer.js流媒体播放器?

H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;可支持H.264与H.265编码格式&#xff0c;性能稳定、播放流畅&#xff0c;能支持WebSocket-FLV、HTTP-FLV&#xff0c;HLS&#xff08;m3u8&#…

Java | Leetcode Java题解之第28题找出字符串中第一个匹配项的下标

题目&#xff1a; 题解&#xff1a; class Solution {public int strStr(String haystack, String needle) {int n haystack.length(), m needle.length();if (m 0) {return 0;}int[] pi new int[m];for (int i 1, j 0; i < m; i) {while (j > 0 && needl…

Linux学习之路 -- 进程篇 -- PCB介绍 -- 进程的孤儿和僵尸状态

前面介绍了进程的各种状态&#xff0c;下面介绍比较特殊的两种状态 -- 孤儿和僵尸&#xff08;僵死&#xff09;。 一、僵尸状态 我们创建进程的目的其实就是想要进程帮我们执行一些任务&#xff0c;当任务被执行完后&#xff0c;进程的使命其实就已经完成了。此时我们就需要…

牛客Linux高并发服务器开发学习第一天

Linux开发环境搭建 安装Xshell 7远程连接虚拟机的Ubuntu 安装Xftp 7可以传输文件(暂时还没使用) 安装VMware Tools可以直接从Windows系统向虚拟机Linux系统拖拽文件实现文件交互。 安装CScode可以远程连接Linux系统进行代码的编写。&#xff08;Windows系统与Linxu系统公钥…

(十)C++自制植物大战僵尸游戏设置功能实现

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/m0EtD 游戏设置 游戏设置功能是一个允许玩家根据个人喜好和设备性能来调整游戏各项参数的重要工具。游戏设置功能是为了让玩家能够根据自己的需求和设备性能来调整游戏&#xff0c;以获得最佳的游戏体验。不同的游戏和平…

【机器学习300问】69、为什么深层神经网络比浅层要好用?

要回答这个问题&#xff0c;首先得知道神经网络都在计算些什么东西&#xff1f;之前我在迁移学习的文章中稍有提到&#xff0c;跳转链接在下面&#xff1a; 为什么其他任务预训练的模型参数&#xff0c;可以在我这个任务上起作用&#xff1f;http://t.csdnimg.cn/FVAV8 …

学习Rust的第三天:猜谜游戏

基于Steve Klabnik的《The Rust Programming Language》一书。今天我们在rust中建立一个猜谜游戏。 Introduction 介绍 We will build a game that will pick a random number between 1 to 100 and the user has to guess the number on a correct guess the user wins. 我们将…

AI音乐,8大变现方式——Suno:音乐版的ChatGPT - 第505篇

悟纤之歌 这是利用AI为自己制作的一首歌&#xff0c;如果你也感兴趣&#xff0c;可以花点时间阅读下本篇文章。 ​ 导读 随着新一代AI音乐创作工具Suno V3、Stable audio2.0、天工SkyMusic的发布&#xff0c;大家玩自创音乐歌曲&#xff0c;玩的不亦乐乎。而有创业头脑的朋友…

C语言 | Leetcode C语言题解之第32题最长有效括号

题目&#xff1a; 题解&#xff1a; int longestValidParentheses(char* s) {int n strlen(s);int left 0, right 0, maxlength 0;for (int i 0; i < n; i) {if (s[i] () {left;} else {right;}if (left right) {maxlength fmax(maxlength, 2 * right);} else if (…

HTML的超链接

前言&#xff1a; 如图&#xff0c;我们在浏览网页时经常可以看到这样的字体&#xff08;点击便跳转到了别的地方了&#xff09;&#xff0c;今日就和各位一起学习一下超链接的相关知识。 相关知识1&#xff1a; 超链接的标签为&#xff1a;a ~使用格式为&#xff1a; <a h…

SS3D翻译

SS3D AbstractIntroductionRelated WorkFully-Supervised 3D Object DetectionWeakly/Semi-Supervised 3D Object DetectionSparsely-Supervised 2D Object Detection MethodOverall FrameworkArchitecture of DetectorMissing-Annotated Instance Mining Module 缺失注释实例挖…

Kubernetes(k8s)集群搭建部署,master节点配置

目录 1.切换为root用户 2.关闭防火墙&#xff0c;关闭swap分区和禁用SElinux 3.安装docker 4.更改daemon.json文件&#xff0c;指定 Docker 守护进程使用的 cgroup 驱动程序 5.重启docker服务 6.配置kubernetes.repo 7.安装Kubelet、Kubeadm、Kubectl 8.设置开机自启 …

流量卡推广怎么申请一级代理

一、号卡推广的重要性 号卡推广对于通信运营商来说具有重要意义。首先&#xff0c;号卡推广是运营商获取新用户、扩大市场份额的重要手段。通过有效的推广策略&#xff0c;运营商可以吸引更多潜在用户&#xff0c;提高品牌知名度和用户黏性。其次&#xff0c;号卡推广有助于运…

java 读取xml文件

1、基本概念 &#xff08;1&#xff09;XML是EXtensible Markup Language 的缩写&#xff0c;翻译过来就是可扩展标记语言。所以很明显&#xff0c;XML和HTML一样都是标记语言&#xff0c;也就是说它们的基本语法都是标签。 &#xff08;2&#xff09;可扩展&#xff1a;XML允…

了解 RISC-V IOMMU

了解 RISC-V IOMMU 个人作为 IOMMU 初学者&#xff0c;从初学者的角度介绍我眼中 RISCV 的 IOMMU 如果有些描述不够专业&#xff0c;还请谅解&#xff0c;也欢迎讨论 部分内容来自 https://zhuanlan.zhihu.com/p/679957276&#xff08;对于 RISCV IOMMU 规范手册的翻译&#xf…

深入理解go语言中的切片

写在文章开头 从一个Java的开发角度来看&#xff0c;切片我们可以理解为Java中的ArrayList即一种动态数组的实现&#xff0c;本文会从源码的角度对切片进行深入剖析&#xff0c;希望对你有帮助。 Hi&#xff0c;我是 sharkChili &#xff0c;是个不断在硬核技术上作死的 java …

前端框架模板

前端框架模板 1、vue-element-admin vue-element-admin是基于element-ui 的一套后台管理系统集成方案。 **功能&#xff1a;**https://panjiachen.github.io/vue-element-admin-site/zh/guide/#功能 **GitHub地址&#xff1a;**GitHub - PanJiaChen/vue-element-admin: :t…

Win 运维 | Windows Server 系统事件日志浅析与日志审计实践

[ 重剑无锋&#xff0c;大巧不工。] 大家好&#xff0c;我是【WeiyiGeek/唯一极客】一个正在向全栈工程师(SecDevOps)前进的技术爱好者 作者微信&#xff1a;WeiyiGeeker 公众号/知识星球&#xff1a;全栈工程师修炼指南 主页博客: 【 https://weiyigeek.top 】- 为者常成&…