0x00 defer 使用测试
先来看下面测试中的函数,分别返回什么?
func Test1() int {
result := 0
defer func() {
result++
}()
return result
}
func Test2() (result int) {
defer func() {
result++
}()
return
}
func Test3() (result int){
defer func(result int) {
result++
}(result)
result = 0
return
}
对 slice 和 map 使用 defer 时,返回值会更具迷惑性,在看下面几个函数之前,先回顾下slice
的特性。
slice 的底层是用数组实现的,是对数组一个连续片段的引用,所以 slice 也是引用类型。如果追加的数组元素超过其容量,则会分配一个新的地址给这个数组。
// slice数据结构定义
type slice struct {
array unsafe.Pointer
len int
cap int
}
现在,再看下面几个函数分别返回什么,他们的 cap 和 len 分别是多少?
func Slice1() []int {
mp := make([]int , 0)
defer func() {
mp = append(mp, 2)
}()
mp = append(mp, 1)
return mp
}
func Slice2() []int {
mp := make([]int ,0, 2)
defer func() {
mp = append(mp, 2)
}()
mp = append(mp, 1)
return mp
}
func Slice3() []int {
mp := make([]int, 0, 2)
defer func(mp []int) {
mp = append(mp, 2)
}(mp)
mp = append(mp, 1)
return mp
}
func Slice4() (mp []int) {
mp = make([]int, 0)
defer func() {
mp = append(mp, 2)
}()
mp = append(mp, 1)
return mp
}
func Slice5() (mp []int) {
mp = make([]int, 0, 1)
defer func() {
mp = append(mp, 2)
mp = append(mp, 3)
}()
mp = append(mp, 1)
return
}
下面是 Test 和 Slice 函数的答案,是否符合你的预期呢。
test1: 0
test2: 1
test3: 0
slice1: [1] len: 1 cap: 1
slice2: [1] len: 1 cap: 2
slice3: [2] len: 1 cap: 2
slice4: [1 2] len: 2 cap: 2
slice5: [1 2 3] len: 3 cap: 4
0x01 defer 使用规则
defer 是在 return 之前执行的,而 return xxx 这一条语句并不是一条原子指令。函数返回的过程是这样的:先给返回值赋值,然后调用 defer 表达式,最后才是返回到调用函数中。
因此函数Slice2
和Slice5
可以被修改为下面的结构,相对的RSlice2
和RSlice5
的返回内容就更容易看出了。
func RSlice2() []int {
mp := make([]int ,0, 2)
mp = append(mp, 1)
ret := mp // 对返回结果赋值
func() {
mp = append(mp, 2)
}()
return ret
}
func RSlice5()(mp []int) {
mp = make([]int, 0, 1)
mp = append(mp, 1)
// 有名返回值,不会再重新赋值
func() {
mp = append(mp, 2)
mp = append(mp, 3)
}()
return mp
}
defer 的使用规则,可以总结如下:
- 当 defer 被声明时,其参数就会被实时解析
- defer 执行顺序为先进后出
- defer 可以读取有名返回值
0x02 defer 适用场景
defer 可以看做是 golang 提供的语法糖,以 gin 框架为例,看下 defer 的几个常用的场景。
1.资源回收
由于 defer 在函数返回前才执行,因此可以讲资源回收的操作在代码上提前,和资源创建的代码对应起来,以免遗漏。
// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, src)
return err
}
但是使用 defer 关闭资源时,如果不注意也很容易留下坑,来看下面一段代码。
这段代码的问题是如果 paths 很大,defer 会一直不去释放打开的文件资源,导致文件句柄耗尽。 改动的办法是讲文件打开和关闭的逻辑抽离到单独的函数中,保证资源及时释放。
func TestFileOpen(paths []string) {
for _,path := range paths{
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
// 业务处理...
}
}
2.捕获panic
golang 没有提供常见的 try catch 功能,而是通过 defer 和 panic 来实现类似的异常捕获功能。
实际应用中,例如一个 http 服务器,某个 http 请求的异常,通常不应挂掉整个 http 服务,因此 gin 提供了recover
中间件,在出现异常时,记录日志并返回特定的错误信息给前端。
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// 日志记录等...
}
}()
c.Next()
}
}
3.修改返回值
这块不再举例,可以参考文档开始的几个示例 :)