Go 学习笔记(17)— 函数(03)[defer 定义、defer 特点、defer 释放资源]

1. defer 定义

Go 函数的关键字 defer 可以提供注册多个延迟调用,只能出现在函数内部,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,这些调用遵循先进后出的顺序在函数返回前被执行。也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

defer 常用于保证一些资源最终一定能够得到释放或者回收。

2. defer 使用

代码示例:

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("First")
	}()

	defer func() {
		fmt.Println("Second")
	}() 
    // defer 后面必须是函数或者方法的调用,否则报错:
    // expression in defer must be function call

	fmt.Println("This is main func body")

}

输出:

This is main func body
Second
First

3. defer 特点

3.1 defer 实参使用值拷贝传递

defer 函数的实参在注册时使用值拷贝传递进去,即 defer 后面的函数参数会被实时解析;

package main

import "fmt"

func main() {
	a := 10
	defer func(i int) {
		fmt.Println("defer func i is ", i)	// defer func i is  10
	}(a)

	a += 10
	fmt.Println("after defer a is ", a)	// after defer a is  20	

}

可以看到后面的 a += 10 并不影响 defer 函数的结果。

3.2 defer 必须先注册

defer 函数必须先注册才能执行,如果 defer 位于 return 语句之后,因为 defer 没有注册,不会被执行;

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("First")
	}()

	return

	// 后面的均不会执行
	defer func() {
		fmt.Println("Second")
	}()

	fmt.Println("This is main func body")

}

输出:

First

3.3 defer 遇到 os.Exit

当主动调用 os.Exit(int) 退出进程时, defer 即使已经注册,那么也不再被执行。

package main

import (
	"fmt"
	"os"
)

func main() {
	defer func() {
		fmt.Println("First")
	}()
	fmt.Println("This is main func body")
	os.Exit(1)
}

输出:

This is main func body
exit status 1

3.4 defer 语句放到错误检查语句之后

一般 defer 语句放到错误检查语句之后;

3.5 defer 尽量不要放到循环语句内部

defer 尽量不要放到循环语句内部;

3.6 defer 性能损耗

defer 相对普通函数调用有一定的性能损耗,具体参考 Go defer 会有性能损耗,尽量不能用?

3.7 defer 用于资源释放和错误处理

defer 通常用于释放资源或错误处理。

package main

import "os"

func test() error {
	f, err := os.Create("test.txt")
	if err != nil {
		return err
	}
	defer f.Close() // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
	f.WriteString("Hello, World!")
	return nil
}

func main() {
	test()

}

3.8 多个 defer 注册,按 FILO 次序执行

多个 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

defer栈

3.9 延迟调用参数在注册时求值或复制

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

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 关键字后面的表达式,是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。

我们同样用一个典型的例子来说明一下 defer 后表达式的求值时机:


func foo1() {
    for i := 0; i <= 3; i++ {
        defer fmt.Println(i)
    }
}

func foo2() {
    for i := 0; i <= 3; i++ {
        defer func(n int) {
            fmt.Println(n)
        }(i)
    }
}

func foo3() {
    for i := 0; i <= 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}

func main() {
    fmt.Println("foo1 result:")
    foo1()
    fmt.Println("\nfoo2 result:")
    foo2()
    fmt.Println("\nfoo3 result:")
    foo3()
}

这里,我们一个个分析 foo1、foo2 和 foo3 中 defer 后的表达式的求值时机。

首先是 foo1。foo1 中 defer 后面直接用的是 fmt.Println 函数,每当 deferfmt.Println 注册到 deferred 函数栈的时候,都会对 Println 后面的参数进行求值。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:

fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)

因此,当 foo1 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以 LIFO 次序出栈执行,这时的输出的结果为:

3
2
1
0

然后我们再看 foo2。foo2 中 defer 后面接的是一个带有一个参数的匿名函数。每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:

func(0)
func(1)
func(2)
func(3)

因此,当 foo2 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以 LIFO 次序出栈执行,因此输出的结果为:

3
2
1
0

最后我们来看 foo3。foo3 中 defer 后面接的是一个不带参数的匿名函数。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:

func()
func()
func()
func()

所以,当 foo3 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以 LIFO 次序出栈执行。匿名函数会以闭包的方式访问外围函数的变量 i,并通过 Println 输出 i 的值,此时 i 的值为 4,因此 foo3 的输出结果为:

4
4
4
4

通过这些例子,我们可以看到,无论以何种形式将函数注册到 defer 中,deferred 函数的参数值都是在注册的时候进行求值的。

3.10 滥用 defer 可能会导致性能问题

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

defer 是在函数退出时调用的。如果在 for 语句的每个迭代都使用 defer 设置 deferred 函数,这些deferred 函数会压入 runtime 实现的 defer 列表中。会占用内存资源,并且如果 forloop 次数很多,这个消耗将很可观。

3.11 defer 返回值被丢弃

defer 后边调用的函数如果有返回值,则这个返回值将会被丢弃。

package main

import "fmt"

func demo() int {
    defer func() int {
        return 100
    }()
    return 8
}

func main() {
    fmt.Println(demo())	// 8
}

上边的示例代码中,demo 函数的返回值是 8。defer 后边调用的函数的返回值并不能作为 demo 函数的返回值。

3.12 defer 改变有名返回参数的值

defer 可以改变有名返回参数的值

这是由于在 Go 语言中,return 是函数的返回标志,并不代表执行结束。return 语句并不是原子操作,最先为返回值赋值,然后执行 defer 命令,最后才是真正意义上的 return 操作。

如果是有名返回值,返回值变量其实可视为是引用赋值,可以能被 defer 修改。而在匿名返回值时,给 ret 的值相当于拷贝赋值,defer 命令时不能直接修改。

有名返回值:

func demo() (i int)

上面函数签名中的 i 就是有名返回值,如果 demo() 中定义了 defer 代码块,是可以改变返回值 i 的,函数返回语句 return i 可以简写为 return

这里综合了以上几种情况,在下面这个例子里列举了几种情况,

package main

import (
    "fmt"
)

func main() {
    fmt.Println("=========================")
    fmt.Println("return:", fun1())

    fmt.Println("=========================")
    fmt.Println("return:", fun2())
    fmt.Println("=========================")

    fmt.Println("return:", fun3())
    fmt.Println("=========================")

    fmt.Println("return:", fun4())
}

func fun1() (i int) {
    defer func() {
        i++
        fmt.Println("defer2:", i) // 打印结果为 defer2: 2
    }()

    // 规则二 defer执行顺序为先进后出

    defer func() {
        i++
        fmt.Println("defer1:", i) // 打印结果为 defer1: 1
    }()

    // 规则三 defer可以改变有名返回参数的值

    return 0 //这里实际结果为2。如果是return 100呢
}

func fun2() int {
    var i int
    defer func() {
        i++
        fmt.Println("defer2:", i) // 打印结果为 defer2: 2
    }()

    defer func() {
        i++
        fmt.Println("defer1:", i) // 打印结果为 defer1: 1
    }()
    return i
}

func fun3() (r int) {
    t := 5
    defer func() {
        t = t + 5
        fmt.Println(t)
    }()
    return t
}

func fun4() int {
    i := 8
    // 规则一 defer后面的函数参数会被实时解析
    defer func(i int) {
        i = 99
        fmt.Println(i)
    }(i)
    i = 19
    return i
}

在上面 fun1() (i int) 有名返回值情况下,return 最终返回的实际值和期望的 return 0 有较大出入。
因为在上面 fun1() (i int) 中,如果 return 100return 0 ,这样的区别在于 i 的值实际上分别是 100 或 0。而在上面中,如果 return 100,则因为改变了有名返回值 i,而 defer 可以读取有名返回值,所以返回值最终为 102,而 defer1 打印 101,defer 打印 102。因此一般直接写为 return

这点要注意,有时函数可能返回非我们希望的值,所以改为匿名返回也是一种办法。具体请看下面输出。

=========================
defer1: 1
defer2: 2
return: 2
=========================
defer1: 1
defer2: 2
return: 0
=========================
10
return: 5
=========================
99
return: 19

4. defer 释放资源

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

4.1 使用延迟并发解锁

在下面的例子中会在函数中并发使用 map ,为防止竞态问题,使用 sync.Mutex 进行加锁,参见下面代码:

var (
    // 一个演示用的映射
    valueByKey      = make(map[string]int)
    // map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。
    // 保证使用映射时的并发安全的互斥锁
    valueByKeyGuard sync.Mutex
)

// 根据键读取值
func readValue(key string) int {
    // 对共享资源加锁
    valueByKeyGuard.Lock()
    // 取值
    v := valueByKey[key]
    // 对共享资源解锁
    valueByKeyGuard.Unlock()
    // 返回值
    return v
}

使用 defer 语句对上面的语句进行简化,参考下面的代码。

func readValue(key string) int {

    valueByKeyGuard.Lock()
   
    // defer后面的语句不会马上调用, 而是延迟到函数结束时调用
    defer valueByKeyGuard.Unlock()

    return valueByKey[key]
}

4.2 使用延迟释放文件句柄


文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源。

在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源,参考下面的代码:

// 根据文件名查询其大小
func fileSize(filename string) int64 {

    // 根据文件名打开文件, 返回文件句柄和错误
    f, err := os.Open(filename)

    // 如果打开时发生错误, 返回文件大小为0
    if err != nil {
        return 0
    }

    // 取文件状态信息
    info, err := f.Stat()
   
    // 如果获取信息时发生错误, 关闭文件并返回文件大小为0
    if err != nil {
        f.Close()
        return 0
    }

    // 取文件大小
    size := info.Size()

    // 关闭文件
    f.Close()
   
    // 返回文件大小
    return size
}

在上面的例子中,第 25 行是对文件的关闭操作,下面使用 defer 对代码进行简化,代码如下:

func fileSize(filename string) int64 {

    f, err := os.Open(filename)

    if err != nil {
        return 0
    }

    // 延迟调用Close, 此时Close不会被调用
    defer f.Close() // defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。
    // 不能将这一句代码放在第 4 行空行处,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。

    info, err := f.Stat()

    if err != nil {
        // defer机制触发, 调用Close关闭文件
        return 0
    }

    size := info.Size()

    // defer机制触发, 调用Close关闭文件
    return size
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值