search.png
关于我
menu.png
go defer使用

defer 用于延迟调用

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

defer的理解

defer和很多语言的finally关键字有点相似,都是在执行完某段代码之后必须会执行到的,finally块即使catch块有return他还是会被执行的。
defer声明了一个在函数返回时需要执行的函数栈,每次声明都是将对应的函数压入栈中,等到return前再去执行。
defer常常被用在关闭句柄、资源回收释放、锁的释放。

func testDefer1(i int) int {
    defer func() {
        i += 3
        fmt.Println(i)
    }()
    defer func() {
        i += 2
        fmt.Println(i)
    }()
    defer func() {
        i += 1
        fmt.Println(i)
    }()

    return i
}


testDefer1(1)

上例的打印结果是:
2
4
7

需要小心的是,defer不是编译期压入栈的,而是运行时,因此在defer前return,defer将不会执行:


func testDefer2(num int) {
    if num < 10 {
        return
    }
    defer fmt.Println(num)
}

testDefer2(1)

上例不会打印任何东西。

defer不配合闭包的情况,其值在声明时就已经确定,因此需要格外小心:

func testDefer3(num int) {
    defer fmt.Println(num)

    num += 10
}

testDefer3(1)

上例打印的是1而不是11.

defer在for range中调用存在内存泄漏的风险,这是因为range是值拷贝的,close函数使用指针执行,当defer执行时,循环结束,s的指针指向了最后一个A,因此输出结果将是三个 clsose 3,如果将a切片的类型改为*A指针、或者将close改为非指针调用这一切都不会发生,而是正常的close 3、close 2、close 1:

type A struct {
    name string
}
func (a *A) close() {
    fmt.Println("close", a.name)
}
func testDefer4() {
    a := []A{
        {"1"},
        {"2"},
        {"3"},
    }
    for _, s := range a {
        defer s.close()
    }

}


testDefer4()

在go1.14之前的版本,defer的性能损耗较大,因此要避免循环中大量调用defer。当然现在已经好很多了:


var lock sync.Mutex

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

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

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

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

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

testDefer5()

结果:

test elapsed:  9.2467ms
testdefer elapsed:  11.9323ms

defer 配合命名返回值使用需要小心,如下例,输出的是2而不是1:

func testDefer6() (i int) {
    i = 1
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

testDefer6()

defer 关闭资源需要注意句柄是否变化:


func testDefer7() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                // 此时关闭的不是 book.txt而是 another-book.txt
                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
}

fmt.Println(testDefer7())

输出:

defer close book.txt err close another-book.txt: file already closed
<nil>

defer中抛出异常,剩下的defer仍然会执行:

// defer中抛出异常,剩下的defer仍然会执行
func testDefer8() {

    defer func() {
        fmt.Println(123)
    }()

    defer func() {
        panic("err")
    }()

}

testDefer8()

123
panic: err
...省略

defer中的闭包变量,如果在循环中会更改,同样有内存泄漏的可能:

func testDefer9() {

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

testDefer9()

打印的结果将都是4

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年03月10日 20:13:50

评论区#

还没有评论哦,期待您的评论!

关闭特效