Go语言简单吗?很多人说go语言简单,我认为这是一个相对的问题。对于解决诸如多线程编程、网络编程、优雅的异常处理与资源释放等问题,go语言确实带来了更加简单的机制,但与其他语言一样,go语言本身也未必如你所想的那么简单,很多机制与语言用法仍需要我们仔细研究与验证。今天想与大家一同探讨对Go语言defer的机制与用法的理解。
假如你是有经验的go程序员,如果你推测下面程序的打印结果与我公布的结果一致,那么欢迎您帮我指正文章中可能出现的错误。否则,我建议您研究一下本文的内容并亲自试验,以更好地掌握defer机制。
func TestPrinter(t *testing.T) {
printer := func(message string) string {
println(message)
return "END" + message
}
defer printer(printer("TestPrinter"))
println(" handling in TestPrinter")
}
这个程序运行后打印如下信息:
TestPrinter
handling in TestPrinter
END TestPrinter
一、defer的语法、语义和实现机制
defer是go语言中推迟函数执行程序流程执行机制,defer机制与go routine可以看作go语言程序中一种特殊的控制流(control flow)。
defer语句的语法如下:
defer 函数调用表达式。
在计算机语言中,程序语句中出现的函数调用其实是一种表达式,与其他的运算或逻辑表达式一样可以被求值,“函数调用表达式”的求值就是执行函数,得到返回结果。
下面就是常见的defer 语句:
defer myFn(i)
defer myFn(otherFunc(i))
defer obj.MemberFn(i)
defer obj.MemberFn(i*2)
defer obj.MemberFn(otherFn(i))
GO语言中,一个完整GO函数生命周期包括正常执行与退出两个阶段,只有当两个阶段都执行完毕,函数才会返回到上级函数。
函数的退出阶段可由return语句、panic语句或runtime.Goexit()函数调用所引发。
defer语句的语义是:在外层函数正常阶段执行defer语句,而在外层函数的退出阶段执行“函数调用表达式”。外层函数执行defer语句的主要操作是把 “函数调用表达式”压入defer-call堆栈之中,在外层函数的退出阶段,如果defer-call堆栈为空,就什么都不做。否则,就会按照“后进先出”的顺序,逐个弹出”函数调用表达式“,予以执行。
如果在函数 f 退出阶段的“函数调用表达”执行时发生了panic,即使函数 f 没有(通过recover()函数)捕获并清除panic,也不会影响函数 f 在退出阶段对defer-call堆栈中尚没有弹出的“函数表达式”的弹出与执行。这是因为,一个GO函数最多只能关联一个panic,即使,多个相继发生多个panic,函数最后也只能关联最后发生的panic,在退出阶段完全结束,返回到上层函数时,才会检查函数是否关联了panic,所以,不用担心这一点。
总之,当外层函数执行遇到defer “函数调用表示式” 语句时,go运行时并不会立即执行“函数调用表达式”,而是把“函数调用表达式”所涉及的函数引用及调用该函数的参数都求值后压入defer-call堆栈中,在函数进入退出阶段时,按照后进先出的顺序执行每个“函数调用表达式”。切记的一点就是,defer-call堆栈中存储的是“可运行求值的函数调用表达式”。
二、使用defer 的6个注意事项
因此,defer 函数调用有以下6个特点:
1. 程序执行到defer语句时,defer语句中的“函数调用表达式”中涉及的变量,包括被推迟的函数自身的函数指针、函数输入参数、函数接收者参数会被立即求值,形成完整的“可运行求值的函数调用表达式”之后,再压入defer-call堆栈中。
(1)被推迟的“函数调用表达式”中如果用到了变量,那么在执行defer语句时,作为“可运行求值的函数调用表达式”组成部分的“变量值”会被立即求值。
(2)被推迟的“函数调用表达式”中如果有其他的表达式作为被推迟调用的函数的参数,在defer语句被执行时,这些表达式会被立即求值。尤其是这些表达式中有另外一个“函数调用表达式(假定为函数otherFn的调用表达式)”作为被推迟函数的参数时,函数otherFn会被立执行,其返回值会作为“可运行求值的函数调用表达式”组成部分而被压入defer-call堆栈中。
2.若被推迟执行的是一个闭包函数,若”闭包函数体“的代码中使用了外层函数或别处定义的外部变量,由于不是 “参数调用表达式”表达式的组成部分,所以,在defer语句执行时,并不会对其求值。 但是需要注意的是,在退出阶段,执行被推迟的闭包函数时,被闭包函数所捕获的外部变量的值是最后一次更新的值。这是容易引入bug的一点,需要谨慎对待。
3.defer函数调用能够改变外层函数的返回结果。因为defer 函数执行发生在外层函数的退出阶段,这意味着外层函数仍在其生命周期之中,如果外层函数的返回结果被命名,那这个命名的返回结果其实只是外层函数生命周期中的一个局部变量,这就可以在外层函数的退出阶段使用defer函数来对其进行更改。这种方式需要谨慎对待,否则容易出现bug。
4.defer函数最终一定会被调用。无论外部函数还是其他被defer的函数是否抛出了panic。
5.有些系统函数不能被defer。 被defer的函数在外层函数退出阶段被自动调用,因此被defer的函数即使有返回结果也无法被外层函数的逻辑所用。 因此,go会抛弃被推迟调用的函数的返回结果,因此,可以被推迟的函数的返回结果一定允许被抛弃。值得注意的是,系统内置函数(buidin包与unsafe包)中,除了copy与recover函数外,其他存在返回值的系统内置函数,由于返回值不允许被抛弃,所以,这些系统函数不能被defer。
6.如果被defer的函数是nil,尽管外层函数在正常执行阶段执行defer语句时不会抛出panic, 但是,在外层函数在退出阶段执行该空函数的调用表达式时会抛出panic。
三、defer机制的主要用途
1. defer函数可以用于优雅地释放被外层函数所申请的系统资源(比如,file,socket,lock等)。也就是在申请到资源后,立即defer 资源释放函数,这样,函数正常执行完毕后,一定会释放资源。此场景下 ,defer相当于Java中的finally,但打开多个资源的时候,go defer机制要比使用java finally机制更加优雅。
2.defer函数与recover函数配合,可以用于恢复(recover)外部函数中收到的panic异常。 如果一个函数抛出了异常,就会导致该函数进入自身的退出阶段,在自身的退出阶段通过recover函数 来截获发生的异常,如果函数自身退出阶段没有处理panic异常,该panic异常就会成为上级的外层函数的panic, 导致上级的外层函数进入退出阶段,在上级的外层函数退出阶段(的defer函数中)仍有机会使用recover函数截获 panic异常,并进行处理,如果所有外层函数在退出阶段都没有处理,就会导致goroutine的入口函数抛出该panic, 并使goroutine入口函数进入退出阶段,如果线程入口函数在退出阶段没有捕获并recover该panic,就会导致该goroutine因存在panic而崩溃,go运行时就会由于任何一个goroutine的崩溃,而让整个程序崩溃。 关于panic及其恢复机制以后再分享。
3.利用defer 机制可以对递归嵌套调用的函数进行调用层次的追踪与分析。
四、用途3的代码分享:
对于前两个用途,很多朋友可以轻松掌握,最后一个用途更加有趣。代码如下,由于头条排版功能限制,不方便之处请谅解。
func TestTracing(t *testing.T) {
tracer := NewTracer("")
parentFn := func() {
/**
注意,tracer.Trace("parentFn")会在parentFn函数正常执行阶段被执行求值,其执行 结果作为tracer.Untrace调用表达式的参数在parentFn函数的退出阶段执行。在parentFn数 退出所执行的函数调用表达式形如:tracer.Untrace(tracer.Trace执行并返回的结果)
**/
defer tracer.Untrace(tracer.Trace("parentFn"))
childFn()
}
childFn := func() {
/**
注意,tracer.Trace("childFn")会在childFn函数正常执行阶段被执行求值,其执行结果 作为tracer.Untrace调用表达式的参数在childFn函数的退出阶段执行。在childFn函数退 出所执行的函数调用表达式形如:tracer.Untrace(tracer.Trace执行并返回的结果)
**/
defer tracer.Untrace(tracer.Trace("childFn"))
}
/**
注意,tracer.Trace("TestTracing")会在TestTracing函数正常执行阶段被执行求值,其执行结果作为tracer.Untrace调用表达式的参数在TestTracing函数的退出阶段执行。在TestTracing函数退出所执行的函数调用表达式形如:tracer.Untrace(”tracer.Trace执行结果")
**/
defer tracer.Untrace(tracer.Trace("TestTracing"))
parentFn()
}
//------------------------
type Tracer struct {
traceLevel int //追踪的层级
traceIdentPlaceholder string //定义文本缩进的占位符,缺省为是制表符" ""
}
func NewTracer(traceIdentPlaceholder string) *Tracer {
if traceIdentPlaceholder == "" {
return &Tracer{traceIdentPlaceholder: " "\}
} else {
return &Tracer{traceIdentPlaceholder: traceIdentPlaceholder}
}
}
//根据缩进级别,生成缩进占位符所组成的缩进字符串
func (t *Tracer) identLevel() string {
return strings.Repeat(t.traceIdentPlaceholder, t.traceLevel-1)
}
//打印追踪信息
func (t *Tracer) tracePrint(fs string) {
fmt.Printf("%s%s ", t.identLevel(), fs)
}
//缩进级别加1
func (t *Tracer) incIdent() { t.traceLevel = t.traceLevel + 1 }
//缩进级别减1
func (t *Tracer) decIdent() { t.traceLevel = t.traceLevel - 1 }
//追踪启动
func (t *Tracer) Trace(msg string) string {
t.incIdent()//增加缩进级别
t.tracePrint("BEGIN " + msg) return msg
}
//追踪终结
func (t *Tracer) Untrace(msg string) {
t.tracePrint("END " + msg)
t.decIdent() //减少缩进级别
}
| 留言与评论(共有 0 条评论) “” |