Go基础:延迟调用defer、异常处理
Golang延迟调用:
defer特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
go语言 defer
go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
defer 是先进后出
这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
package mainimport "fmt"func main() {var whatever [5]struct{}for i := range whatever {defer fmt.Println(i)}
}
输出结果:
43210
defer 碰上闭包
package mainimport "fmt"func main() {var whatever [5]struct{}for i := range whatever {defer func() { fmt.Println(i) }()}
}
输出结果:
44444
其实go说的很清楚,我们一起来看看go spec如何说的
我们先看一下官方对defer
的解释:
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved a new but the actual function is not invoked.
Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.
翻译一下:
每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行;如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.
defer语句并不会马上执行,而是会进入一个栈,函数return前,会按先后出的顺序执行。也说是说最先被定义的defer语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行,那后面函数的依赖就没有了。
在defer函数定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。
- 作为函数参数,则在defer定义时就把值传递给defer,并被cache起来;
- 作为闭包引用的话,则会在defer函数真正调用时根据整个上下文确定当前的值。
defer后面的语句在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个“值”,那么就和定义的时候是一致的。如果此变量是一个“引用”,那么就可能和定义的时候不一致。
也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
defer f.Close
这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.
package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer t.Close()}
}
输出结果:
c closedc closedc closed
这个输出并不会像我们预计的输出c b a,而是输出c c c
可是按照前面的go spec中的说明,应该输出c b a才对啊.
那我们换一种方式来调用一下.
package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func Close(t Test) {t.Close()
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer Close(t)}
}
输出结果:
c closedb closeda closed
这个时候输出的就是c b a
当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a
看似多此一举的声明
package mainimport "fmt"type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {t2 := tdefer t2.Close()}
}
输出结果:
c closedb closeda closed
通过以上例子,结合
Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.
这句话。可以得出下面的结论:
defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package mainfunc test(x int) {defer println("a")defer println("b")defer func() {println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。}()defer println("c")
}func main() {test(0)
}
输出结果:
cbapanic: runtime error: integer divide by zero
*
延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。
package mainfunc test() {x, y := 10, 20defer func(i int) {println("defer:", i, y) // y 闭包引用}(x) // x 被复制x += 10y += 100println("x =", x, "y =", y)
}func main() {test()
}
输出结果:
x = 20 y = 120defer: 10 120
*
滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。
package mainimport ("fmt""sync""time"
)var lock sync.Mutexfunc test() {lock.Lock()lock.Unlock()
}func testdefer() {lock.Lock()defer lock.Unlock()
}func main() {func() {t1 := time.Now()for i := 0; i < 10000; i++ {test()}elapsed := time.Since(t1)fmt.Println("test elapsed: ", elapsed)}()func() {t1 := time.Now()for i := 0; i < 10000; i++ {testdefer()}elapsed := time.Since(t1)fmt.Println("testdefer elapsed: ", elapsed)}()}
输出结果:
test elapsed: 223.162µstestdefer elapsed: 781.304µs
defer陷阱
defer 与 closure
package mainimport ("errors""fmt"
)func foo(a, b int) (i int, err error) {defer fmt.Printf("first defer err %v\n", err)defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)defer func() { fmt.Printf("third defer err %v\n", err) }()if b == 0 {err = errors.New("divided by zero!")return}i = a / breturn
}func main() {foo(2, 0)
}
输出结果:
third defer err divided by zero!second defer err <nil>first defer err <nil>
解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
defer 与 return
package mainimport "fmt"func foo() (i int) {i = 0defer func() {fmt.Println(i)}()return 2
}func main() {foo()
}
输出结果:
2
解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。
defer nil 函数
package mainimport ("fmt"
)func test() {var run func() = nildefer run()fmt.Println("runs")
}func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}}()test()
}
输出结果:
runs
runtime error: invalid memory address or nil pointer dereference
解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。
在错误的位置使用 defer
当 http.Get 失败时会抛出异常。
package mainimport "net/http"func do() error {res, err := http.Get("http://www.google.com")defer res.Body.Close()if err != nil {return err}// ..code...return nil
}func main() {do()
}
输出结果:
panic: runtime error: invalid memory address or nil pointer dereference
因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常
解决方案
总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer
package mainimport "net/http"func do() error {res, err := http.Get("http://xxxxxxxxxx")if res != nil {defer res.Body.Close()}if err != nil {return err}// ..code...return nil
}func main() {do()
}
在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。
解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。
通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。
不检查错误
在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉
package mainimport "os"func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer f.Close()}// ..code...return nil
}func main() {do()
}
改进一下
package mainimport "os"func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if err := f.Close(); err != nil {// log etc}}()}// ..code...return nil
}func main() {do()
}
再改进一下
通过命名的返回变量来返回 defer 内的错误。
package mainimport "os"func do() (err error) {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if ferr := f.Close(); ferr != nil {err = ferr}}()}// ..code...return nil
}func main() {do()
}
释放相同的资源
如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。
package mainimport ("fmt""os"
)func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func() {if err := f.Close(); err != nil {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
}func main() {do()
}
输出结果: defer close book.txt err close ./another-book.txt: file already closed
当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭
解决方案:
package mainimport ("fmt""io""os"
)func do() error {f, err := os.Open("book.txt")if err != nil {return err}if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close book.txt err %v\n", err)}}(f)}// ..code...f, err = os.Open("another-book.txt")if err != nil {return err}if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close another-book.txt err %v\n", err)}}(f)}return nil
}func main() {do()
}
异常处理
Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic:
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover:
1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error
注意:
- 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package mainfunc main() {test()
}func test() {defer func() {if err := recover(); err != nil {println(err.(string)) // 将 interface{} 转型为具体类型。}}()panic("panic error!")
}
输出结果:
panic error!
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
func panic(v interface{})func recover() interface{}
向已关闭的通道发送数据会引发panic
package mainimport ("fmt"
)func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}}()var ch chan int = make(chan int, 10)close(ch)ch <- 1
}
输出结果:
send on closed channel
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
package mainimport "fmt"func test() {defer func() {fmt.Println(recover())}()defer func() {panic("defer panic")}()panic("test panic")
}func main() {test()
}
输出:
defer panic
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
package mainimport "fmt"func test() {defer func() {fmt.Println(recover()) //有效}()defer recover() //无效!defer fmt.Println(recover()) //无效!defer func() {func() {println("defer inner")recover() //无效!}()}()panic("test panic")
}func main() {test()
}
输出:
defer inner<nil>test panic
使用延迟匿名函数或下面这样都是有效的。
package mainimport ("fmt"
)func except() {fmt.Println(recover())
}func test() {defer except()panic("test panic")
}func main() {test()
}
输出结果:
test panic
如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执 。
package mainimport "fmt"func test(x, y int) {var z intfunc() {defer func() {if recover() != nil {z = 0}}()panic("test panic")z = x / yreturn}()fmt.Printf("x / y = %d\n", z)
}func main() {test(2, 1)
}
输出结果:
x / y = 0
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
type error interface {Error() string
}
标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
package mainimport ("errors""fmt"
)var ErrDivByZero = errors.New("division by zero")func div(x, y int) (int, error) {if y == 0 {return 0, ErrDivByZero}return x / y, nil
}func main() {defer func() {fmt.Println(recover())}()switch z, err := div(10, 0); err {case nil:println(z)case ErrDivByZero:panic(err)}
}
输出结果:
division by zero
Go实现类似 try catch 的异常处理
package mainimport "fmt"func Try(fun func(), handler func(interface{})) {defer func() {if err := recover(); err != nil {handler(err)}}()fun()
}func main() {Try(func() {panic("test panic")}, func(err interface{}) {fmt.Println(err)})
}
输出结果:
test panic
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
Go基础:延迟调用defer、异常处理相关推荐
- Go基础编程:延迟调用defer
链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 本篇文章所讲的就是go编程中的延迟调用defer,希望对社区的成员有较多的帮助. 1 defer作用 关键字defer ...
- Go 延迟调用 defer 用法详解
引子 package counterimport ("log""sync" )type Counter struct {mu *sync.MutexValue ...
- Golang——延迟调用defer
defer用于向当前函数注册稍后执行的函数调用.这些调用被称作延迟调用,它们直到当前函数执行结束前才被执行,常用于资源释放.错误处理等操作 func main() {f, err := os.Open ...
- python延时执行函数_一日一技:在 Python 中实现延迟调用
一日一技:在 Python 中实现延迟调用 收录于话题 #你不知道的 Python 71个 摄影:产品经理 产品经理的生日餐 熟悉 Golang 的同学都知道,Golang 里面有一个关键词叫做def ...
- 带有Java Util日志记录的Java 8延迟调用
在博客文章"在Log4j2中更好地执行非日志记录器调用"中 ,我介绍了可以在Log4j 2中使用的方法,这些方法可以减少或避免在基于指定日志级别实际上根本未记录的日志语句中调用方法 ...
- js的间隔调用和延迟调用
计时事件 描述:通过使用js,可以做到在一个设定的时间间隔之后来执行代码,而不是在函数被调用后立即执行,称为计时事件 类型: setInterval--间隔指定的毫秒数不停地执行指定的代码(间隔调用) ...
- 记录 activity onStop、onDestroy 延迟调用问题解决过程
问题背景 在我的项目中,从其它页面回到首页后,其它页面的 onStop.onDestroy 都会延迟调用,大概 7s 左右吧. 思考方向 1.可能是 Home 页面的onStart onResume ...
- ios performSelector延迟调用及取消问题
iOS延迟调用NSObject提供的相关函数如下: /**************** Delayed perform ******************/@interface NSObject ( ...
- OC基础学习 调用方式
OC基础学习 调用方式 调用方法: C++里,送一个消息给对象(或者说调用一个方法)的语法如下: obj.method(argument); Objective-C则写成: [obj method: ...
- Unity中在Editor下的延迟调用
Unity中在Editor下的延迟调用 说到延迟调用, 大家肯定首先想到的是MonoBehavior的协程, 但是在Editor不太好用. 我们这里给出两个方案供大家选择. async 第一种比较简单 ...
最新文章
- 按摩师-总预约时间最长
- Tensorflow |(5)模型保存与恢复、自定义命令行参数
- c++实现ftp服务器_第三步,尝试用树莓派搭建你的云计算平台和服务器
- Codeforces D. Fair 多源BFS求最短路
- rails online api
- Java9 jar兼容_java9新特性-6-多版本兼容jar包
- 安装CUDA时出现黑屏的现象解决办法
- 宇宙第一 IDE 发布新版了
- 逻辑代码自动生成相关技术概述
- ST、SC、FC、LC光纤接头区别
- 正弦波叠加成方波--Python简易版
- 美团外卖离线数仓建设实践
- RRU原理详解以及eCPRI+Low-Phy(一篇文章让你搞懂RRU---呕心沥血之作)
- hdu 5514 2015 icpc 沈阳现场 F Frogs
- PHP发送邮件类库PHPMailer的简单使用 摘自 现代魔法研究协会
- phpmyadmin 修改记录(不断更新)
- c++类与对象(一)
- mysql基础架构(一条update语句如何执行)
- 哈尔滨小学计算机上课时间,哈市中小学各校新学期作息时间调整汇总,看看有没有你的学校!...
- 使用树莓派打造Apple Mac TimeCapsule
热门文章
- python修改图片尺寸
- 黑客入侵香港中文大学网 师生资料被盗
- Python基础+数据科学入门(四)程序控制结构
- The field file exceeds its maximum permitted size of 1048576 bytes.
- ps写php,ps怎么做立体效果文字
- PS 怎么去掉图片上的文字
- linux aria2 多线程,Mac/Linux 多线程下载解决方案(Aria2 YAAW是什么)
- 【学习笔记】吉司机线段树
- 程序员超实用网站,留着总有用的着的时候
- 修改后的取得汉字首字母的lazarus函数,可以自己增加疑难汉字,这个应该比较理想了