当前位置:首页 > Golang杂记 > 正文内容

golang的defer踩坑汇总

3个月前 (06-18)Golang杂记428

defer语句用于延迟函数调用,每次会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。延迟函数可以有参数:

  • 延迟函数的参数在defer语句出现时就已确定下来(传值的就是当前值)

  • return先赋值(对于命名返回值),然后执行defer,最后函数返回

  • 延迟函数执行按后进先出顺序执行

  • 延迟函数可操作主函数的变量名返回值(修改返回值)

  • defer后面的表达式可以是func或者是method的调用,如果defer的函数为nil,则会panic

日常开发中,使用不当很容易造成意外的“坑”。下面我整理了下常规使用场景下,defer的问题可能的踩坑汇总。

释放资源

defer语句用于延迟函数调用,每次会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。延迟函数可以有参数:

  • 延迟函数的参数在defer语句出现时就已确定下来(传值的就是当前值);

  • 延迟函数执行按后进先出顺序执行;

  • 延迟函数可操作主函数的具名返回值(修改返回值);

defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放、句柄关闭等问题。

package main
import (
	"fmt"
	"os"
)
func fileSize(filename string) int64 {
	f, err := os.Open(filename)
	if err != nil {
		return 0
	}
	// 延迟调用Close, 此时Close不会被调用
	defer f.Close()
	info, err := f.Stat()
	if err != nil {
		// defer机制触发, 调用Close关闭文件
		return 0
	}
	size := info.Size()
	// defer机制触发, 调用Close关闭文件
	return size
}
func main() {
	fmt.Println(fileSize("demo.txt"))
}

变量捕获

defer中的变量会被提前捕获,后续的修改不会影响到已捕获的值,举个例子:

package main
import (
	"fmt"
)
func main() {
	i := 0
	defer fmt.Println("Defer运行值:", i)
	i = 10 // 这里虽然修改了值,但是不会影响上面的i值
	fmt.Println("最后输出值:", i)
}

结果defer语句中打印的值是修改前的值。:

最后输出值: 10

Defer运行值: 0

变量名返回值

在defer中修改具体变量名返回值时,会影响到函数的实际返回值,继续举个例子:

package main

import (
    "fmt"
)

func ShowDefer() {
    fmt.Println("最后输出值:", deferValue())
}
func deferValue() (ret int) { // 注意这里返回res变量值
    ret = 0

    defer func() { // 会直接修改栈中对应的返回值
        ret += 10
        fmt.Println("Defer 运行值:", ret)
    }()
    ret = 2
    fmt.Println("Ret重置值:", ret)
    return //返回的ret最后是 其实是本次2+上面的ret+=10的结果
}

func main() {
    ShowDefer()
}

//Ret重置值: 2
//Defer 运行值: 12
//最后输出值: 12

非变量名返回值

当函数为非具体名返回值时,defer无法影响返回值(因在return时,对应返回值已存入栈中),继续举个例子:

package main

import (
    "fmt"
)

func ShowDefer() {
    fmt.Println("最后输出值:", deferValue())
}
func deferValue() int { // 非命名变量返回
    ret := 0
    defer func() {
    ret += 10
    fmt.Println("Defer 运行值:", ret)
}()
    ret = 2
    return ret // 这里直接返回ret2
}

func main() {
    ShowDefer()
}

//Defer 运行值: 12
//最后输出值: 2

经过上面的实践理解,我们来看下下面的笔试题:

笔试题一

package main

import "fmt"

func f() (result int) {
    defer func() {
    result *= 7
}()
    return 3
}
func main() {
    fmt.Println(f())
}

问题解析:这里return先给result赋值为3,之后执行defer,result变为21,最后返回21。

笔试题二

package main

import "fmt"

func f() int {
    result := 3
    defer func() {
        result *= 7
    }()
    return result
}

func main() {
    fmt.Println(f())
}

问题解析:这里return确定返回值3,之后defer才修改result,最后函数返回return确定的返回值3。

笔试题三

package main

import "fmt"
// 多个defer
func multiDefer() {
    for i := 3; i > 0; i-- {
    defer func(n int) {
        fmt.Print(n, " ")
    }(i)
    }

    for i := 33; i > 30; i-- {
        defer fmt.Print(i, " ")
    }
}

func main() {
    multiDefer()
}

问题解析:多个defer函数,按顺序逆序执行,这里输出31 32 33 1 2 3 。

笔试题四

package main

import "fmt"

var fun func() string

func main() {
    fmt.Println("hello monkey")
    defer fun()
}

问题解析:由于这里的defer指定的func为nil,所以会panic 。

笔试题五

package main

import "fmt"

func main() {
    for i := 3; i > 0; i-- {
        defer func() {
            fmt.Print(i, " ")
            }()
    }
}

问题解析:这里是极度容易踩坑的地方,由于defer这里调用的func没有参数,等执行的时候,i已经为0(按3 2 1逆序,最后一个i=1时,i--的结果最后是0),所以这里输出3个0 。

如果还不太好理解?

package main

import "fmt"

func main() {
    for i := 3; i > 1; i-- { // 循环满足条件的是 3 2,
        defer func() { // 因为func 没有参数,defer运行最后i--即 2-- 结果为 1
            fmt.Print(i, " ") // 循环2次 结果均为 1
        }()
    }
}//输出 1 1

按照常规的思维理解应该是这样:

package main

import "fmt"

func main() {
    for i := 3; i > 0; i-- {
    defer func(i int) {
        fmt.Print(i, " ")
        }(i)
    }
}

感兴趣的朋友可以细细品下。

    扫描二维码推送至手机访问。

    版权声明:本文由周伯通的博客发布,如需转载请注明出处。

    本文链接:https://www.zhoubotong.site/post/50.html

    分享给朋友:

    相关文章

    关于Go的内存对齐

    关于Go的内存对齐

    今天看到群里有人提到内存对齐的东西,网上查阅了golang相关的内存对齐资料,特意整理了下,希望对大家有帮助。看完这篇介绍。我们将获得以下知识点:1.什么是内存对齐?2.为什么需要内存对齐?3.如何进...

    Go channel 协程为什么是安全的

        Channel跟java/php的 thread不一样,首先channel是协程不是线程。channel不会产生新的线程,自然不会涉及到新的进程或者线程...

    NewReplacer使用技巧

            上次写博客至今有段时间了,这些日子,认真过,努力过,职场中不管有哪些让人失意或不快的事,终归到底,是自己...

    Go easyjson使用技巧

    Go easyjson使用技巧

    如果使用go语言自带的json库,使用的是反射,而go语言中反射性能较低。easyjson就是一个比较好的替代方案。esayjson安装(https://gitcode.net/mirrors/mai...

    Go语言中的零值

    开箱即用什么叫开箱即用呢?因为Go语言的零值让程序变得更简单了,有些场景我们不需要显示初始化就可以直接用,举几个例子:切片,他的零值是nil,即使不用make进行初始化也是可以直接使用的,例如:pac...

    发表评论

    访客

    看不清,换一张

    ◎欢迎参与讨论,请在这里发表您的看法和观点。