Golang中多用途的defer

defer顾名思义就是延迟执行,那么defer在Golang中该如何使用以及何时使用呢?

A "defer" statement invokes a function whose executionis deferred to the moment the surrounding function returns,

Golang的官方时这么定义的。

1.那么在什么情况下会调用defer延迟过的函数呢?

从文档中可以知道主要有两种情况:

  1. 当函数执行了return 语句后
  2. 当函数处于panicing状态,也就是程序中panic回溯上来到当前函数范围内时

2. 如何使用defer呢?

defer可以当作一个修饰符,其不是用来修饰函数定义的,而是用来修饰函数调用的。当你调用一个函数时,在正常的书写方式的前面加上 defer 之后,该函数调用就被延迟了,具体我们来看个例子:

func TestPanic(){
    defer func (){fmt.Println("defer function be invoked")}()
    fmt.Println("The first one to be invoke")
    return
}

运行后会发现结果为:

The first one to be invoke
defer function be invoked

这里我们用到了一个匿名函数,但是其原型就是 fun() 形式的调用。这里符合上面的在return 语句之后执行了我们defer过的函数。

3.defer主要用于什么场景

defer最主要的使用场景有两个,一个时资源的释放,如关闭打开的文件,另一个是和panic 以及 recover 组合起来模拟try...catch 功能;。除此之外defer还可以用来对return命名返回值作修改。

资源释放当我们打开文件作操作后,很容易忘记释放文件资源。假设现在B资源要依赖A资源。如下:

resA, err:= getA()
if err!=nil {
    return
}
resB,err := getB(resA)
if err != nile {
    return
}

这里我们就容易产生bug,在获取B资源出错时,没有释放获取成功的A资源。这里我们添加:

resA, err:= getA()
defer ReleaseA()
if err!=nil {
    return
}
resB,err := getB(resA)
defer ReleaseB()
if err != nile {
    return
}

就可以保证,无论时在哪个位置返回,其上的资源都会得到释放。

  • 异常处理defer还有最大的用处就是和panic以及recover组合成try...catch结构。

看个示例,来自官网的:

func protect(g func()) {
    defer func() {
        log.Println("done")  // Println executes normally even if there is a panic
        if x := recover(); x != nil {
            log.Printf("run time panic: %v", x)
        }
    }()
    log.Println("start")
    g()
}

当g()中通过panic抛出错误时,会在defer中用recover进行捕获。也就是在子函数中的panic触发了其处于panicing状态,从而当panic回溯到当前函数时调用本函数的defer修饰的函数。

当然这里把g()替换成panic()也是可行的,就没有panic回溯了,直接时本函数中的panic触发使其处在panicing状态,从而调用defer修饰的函数。

  • 修改返回值

如同上面的例子,如果对于有异常和没有异常返回不同的值,那么该如何操作。传统的方法是:

try {
    ...
} catch (exception1){
    return 1;
}
return 2
...

由于Golang中不能在defer里面返回值,所以我们不能用上面的逻辑,在主函数和defer里面返回不同的结果。这个时候我们可以借助命名返回值来帮忙。在Golang中使用

func InvokeF()(rst int){
    defer func(){
        if err:=recover();err!=nil {
            if err== exception1 {
                rst = -1
            }
        }
    }()
    rst = f()
    return
}

当正常时,返回的rst为f()调用结果,当出现异常时,rst的值会被修改,从而达到目的。

4.defer中的坑

  • 栈式调用

defer的调用顺序时栈式的,也就是后修饰的函数会被先调用。如果熟悉进程和线程API的化,这里defer的调用就和at_exit()有点类似,在退出时栈式调用注册过的退出函数。在使用中我们会发现这样的方式是符合我们代码原意的。假设A资源依赖B资源。那么我们这样:

resA ,err := getA()
defer releaseA()
...
resB,err := getB(resA)
defer releaseB()

一来,我们在获取资源时就注册defer,语义上跟符合人类语言。二来,当A资源获取失败时仅释放A的环境;当B资源获取失败时,释放B和A的资源,且先释放依赖A的B。这样代码更为整洁易读。

  • 出现时计算

首先要注意的是,defer修饰的函数调用中如果有参数,那么参数取的是当前计算时的值,而不是在return或者panic时该变量的值。

func main() {
    i := 2;
    deferFun := func (i int){fmt.Printf("i in panic is %d \n",i)}
    defer deferFun(i+1)
    i += 12
    fmt.Printf("i befor return is %d \n",i)
    return
}

得到结果为:

ibeforreturnis14iinpanicis3

注意,这里只是执行了参数的计算,而并没有执行函数体,函数体时在return之后执行的。

  • 丢弃返回值

defer修饰过的函数调用的返回值时丢弃的,因此不要想着再去使用其返回值。本来也没有地方去使用。

Golang中多用途的defer,布布扣,bubuko.com

时间: 2024-11-08 01:09:30

Golang中多用途的defer的相关文章

Golang中使用log(一):Golang 标准库提供的Log

Golang的标准库提供了log的机制,但是该模块的功能较为简单(看似简单,其实他有他的设计思路).不过比手写fmt. Printxxx还是强很多的.至少在输出的位置做了线程安全的保护.其官方手册见Golang log (天朝的墙大家懂的).这里给出一个简单使用的例子: package main import ( "log" ) func main(){ log.Fatal("Come with fatal,exit with 1 \n") } 编译运行后,会看到程

Go_18: Golang 中三种读取文件发放性能对比

Golang 中读取文件大概有三种方法,分别为: 1. 通过原生态 io 包中的 read 方法进行读取 2. 通过 io/ioutil 包提供的 read 方法进行读取 3. 通过 bufio 包提供的 read 方法进行读取 下面通过代码来验证这三种方式的读取性能,并总结出我们平时应该使用的方案,以便我们可以写出最优代码: package main import ( "os" "io" "bufio" "io/ioutil"

Golang中使用log(二):Golang 标准库log的实现

前一篇文章我们看到了Golang标准库中log模块的使用,那么它是如何实现的呢?下面我从log.Logger开始逐步分析其实现. 其源码可以参考官方地址 1.Logger结构 首先来看下类型Logger的定义: type Logger struct { mu sync.Mutex // ensures atomic writes; protects the following fields prefix string // prefix to write at beginning of each

golang中省略返回值造成内存泄漏

我已经两次因为不恰当的省略go中的函数返回值,一次造成MySql的too many connection错误,一次造成严重的内存泄漏.所以在这里大家分享一下这个问题和解决办法,也提醒自己以后不要再犯类似的错了. 众所周知,go中的函数可以返回多个值.但很多时候我们并不需要所有的值,而且go中定义了一个变量必须使用才可以,不然会报错.所以对于不需要的返回值,一般的操作方法就是省略: for _,value := range slice{ //.... } 一个典型就是上面的range.range可

Golang 中三种读取文件发放性能对比

Golang 中读取文件大概有三种方法,分别为: 1. 通过原生态 io 包中的 read 方法进行读取 2. 通过 io/ioutil 包提供的 read 方法进行读取 3. 通过 bufio 包提供的 read 方法进行读取 下面通过代码来验证这三种方式的读取性能,并总结出我们平时应该使用的方案,以便我们可以写出最优代码: package main import ( "os" "io" "bufio" "io/ioutil"

如何关闭Golang中的HTTP连接 How to Close Golang's HTTP connection

我们的一个服务是用Go写的,在测试的时候发现几个小时之后它就会core掉,而且core的时候没有打出任何堆栈信息,简单分析后发现该服务中的几个HTTP服务的连接数不断增长,而我们的开发机的fd limit只有1024,当该服务所属进程的连接数增长到系统的fd limit的时候,它被操作系统杀掉了... 这个服务中,我们会定期向一个HTTP服务器发起POST请求,因为请求非常不频繁,所以想采用短连接的方式去做.请求代码大概长这样: func dialTimeout(network, addr st

golang中四种方式实现子goroutine与主协程的同步

如何实现子goroutine与主线程的同步 第一种方式:time.sleep(),这种方式很太死板,就不演示了. 第二种方式:使用channel机制,每个goroutine传一个channel进去然后往里写数据,在再主线程中读取这些channel,直到全部读到数据了子goroutine也就全部运行完了,那么主goroutine也就可以结束了.这种模式是子线程去通知主线程结束. package main import ( "fmt" ) func main() { var chanTes

在Golang中使用Redis

周五上班的主要任务是在公司老平台上用redis处理一个队列问题,顺便复习了一下redis操作的基础知识,回来后就想着在自己的博客demo里,用redis来优化一些使用场景,学习一下golang开发下redis的使用. Redis简单介绍 简介 关于Redis的讨论,其实在现在的后台开发中已经是个老生常谈的问题,基本上也是后端开发面试的基本考察点.其中 Redis的背景介绍和细节说明在这里就不赘述.不管怎么介绍,核心在于Redis是一个基于内存的key-value的多数据结构存储,并可以提供持久化

golang中的Mutex和RWMutex

????sync中有两种锁 Mutex和RWMutex,Mutex的读和写必须都进行排队,只能一个完成了在进行下一个,RWMutex读可以并行,写只能一个一个进行,当有读时,需要所有的读全部关闭后才能进行写操作,有写 时,需要等写操作完成了才能进行读操作(读并行,写单一).(sync中有一个map对象是线程安全的,默认的map不是线程安全的). Mutex 先看下面的代码: package main import ( "fmt" "sync" "time&