Golang有关defer延迟函数的讨论与研究

延迟函数defer是Golang的一个语法糖,逻辑上defer是与普通函数适配的。所以在使用defer时,应该对应到一个函数中,而不是一个for循环语句内或一个代码块中,因为这样做是危险和无意义的。

import (
	"fmt"
	"sync"
)

func main() {

	/* 问题一:defer nil函数 */
	func(){
		var run func() = nil
		defer run()

		fmt.Println("runs")
	}()
	// 匿名函数执行结束后,defer函数被执行,因为run()为nil,而出现panic

	/* 问题二 在循环中使用defer */
	func() {
		for {
			wg := sync.WaitGroup{}
			wg.Add(1)
			// do something

			defer wg.Done()
		}
	}()
	// defer延迟函数会在该匿名函数结束后运行,而不是每次的for循环结束后。
	// 于是这些延迟函数会在每次循环中不停地堆积到延迟调用栈,最终导致不可预知的问题

	/* 问题二的解决方案1 */
	func() {
		for {
			wg := sync.WaitGroup{}
			wg.Add(1)
			// do something
			wg.Done()
		}
	}()
	// 这里需要注意,不使用defer,直接在for循环的每一次的末尾进行结束

	/* 问题二的解决方案2 */
	func() {
		for {
			func() {
				wg := sync.WaitGroup{}
				wg.Add(1)
				// do something
				defer wg.Done()
			}()
		}
	}()
	// for循环中嵌套匿名函数,这样延迟函数defer会在每次内部的匿名函数结束后执行

	/* 问题三 延迟调用含有闭包的函数 */
	{
		db := &database{}
		defer db.connect()
		fmt.Println("query db...")
		/* Output:
			query db...
			connect
		 */
	} // connect()函数执行结束后,其执行域被保存起来,但内部的闭包不会执行

	/* 问题三的解决方案1 */
	func() {
		db := &database{}
		close := db.connect()
		defer close()

		fmt.Println("query db...")
	}()
	// 将db.database()返回了一个函数,再对该函数使用defer,就可以在匿名函数结束后实际执行

	/* 问题三的解决方案2 */
	func() {
		db := &database{}
		defer db.connect()()
	}()
	// 这里与之前的解决方案没有本质区别。第一个括号是执行connect函数内部的方法,
	// 第二个括号是执行返回的闭包

}

type database struct {}

func (db *database) connect() (disconnect func()) {
	fmt.Println("connect")

	return func() {
		fmt.Println("disconnect")
	}
}
	/* 问题四:在执行块中使用defer */
func main() {
	{
		defer func() {
			fmt.Println("block: defer runs.")
		}()

		fmt.Println("block: ends")
	}
	fmt.Println("main: ends")
}
/* Output:
block: ends
main: ends
block: defer runs
*/
// 延迟是相对于一个函数而非一个代码块
func main() {
	func() {
		defer func() {
			fmt.Println("func: defer runs")
		}()
		fmt.Println("func: ends")
	}()
	fmt.Println("main: edns")
}
/* Output:
func: ends
func: defer runs
main: edns
*/
// 延迟函数在匿名函数结束后便执行,而不是等到main函数结束defer才执行
	/* 问题五:延迟方法的坑 */
type Car struct {
	model string
}

func (c Car) PrintModel() {
	fmt.Println(c.model)
}
func (c *Car) PrintModel1() {
	fmt.Println(c.model)
}

func main() {
	c := Car{model: "DeLorean DMC-12"}

	defer c.PrintModel()
	//defer c.PrintModel1()

	c.model = "Chevrolet Impala"
}
/*
	当defer调用的是PrintModel()时,输出结果是:DeLorean DMC-12
	当defer调用的是PrintModel1()时,输出结果是:Chevrolet Impala

	当以值为接收者的方法被defer修饰时,接收者会在声明时被拷贝,之后的修改都不会影响到这次拷贝的值
	而当以指针对象作为接收者的方法被defer修饰时,生成的新的指针变量的指向的地址与原对象c的指向地址相同,因此任何修改都作用于同一个对象中。
*/

将defer置于for循环中,每一次循环的defer函数都会保存在Defers stack中,所以结果是反向打印出来的。

	/* 问题六 */
func main() {
	for i:=0;i<4;i++ {
		defer fmt.Println(i)
	}
}
/*Output:
3
2
1
0
*/

下面的例子中,输出的结果是Hello的原因是,defer函数虽然是在所属函数执行完后才会执行,但是m.print()却是按照代码顺序立即执行出结果了,所以defer保存的就是Hello,故最后输出的结果是Hello

	/* 问题七 */
type message struct {
	content string
}
func (p *message) set(c string) {
	p.content = c
}
func (p *message) print() string {
	return p.content
}

func main() {
	func() {
		m := &message{content: "Hello"}
		defer fmt.Print(m.print())
		m.set("World")
		// deferred func runs
	}()
}
/*Output:
Hello
*/

变量i被初始化之后其值0会保存在某一个地址内,而循环结束后defer才会执行完毕,而defer准备去执行的时候,变量i的值是3,所以结果是打印三次3

	/* 问题八 */
func main() {
	for i := 0; i < 3; i++ {
		defer func() {
			fmt.Println(i)
		}()
	}
}
/*Output:
3
3
3
*/

为了想实现打印出2 1 0,有如下解决方案。
如此,go runtime会创建不同的变量i(i已经作为一个实参传入,那么实际上是会被复制的),并且保存预期正确的值。

func main() {
	for i := 0; i < 3; i++ {
		defer func(i int) {
			fmt.Println(i)
		}(i)
	}
}
/*Output:
2
1
0
*/

下面的例子,道理类似。将i赋给一个新的同名变量i,所以在每次循环时,都会初始化一个同名i的变量,所以执行三次defer函数时,都不会使用同一个值。

func main() {
	for i := 0; i < 3; i++ {
		i := i
		defer func() {
			fmt.Println(i)
		}()
	}
}
/*Output:
2
1
0
*/

这个例子能得到预期的结果,原因与之前例子打印出Hello的原因类似,defer后直接使用了打印语句,其实结果已经得到,只是并没有执行打印到控制台。当main函数结束后,defer会把之前得到的结果(结果保存在Defers Stack中)逐一弹出。

func main() {
	for i := 0; i < 3; i++ {
		defer fmt.Println(i)
	}

}
/*Output:
2
1
0
*/

首先需要提及的一个知识点是named result values,它可以让你将需要返回的结果值当作声明的变量来使用,并且你可以仅仅返回一个赤裸裸的return
然后看一下下面的例子,release()函数返回的结果是nil,因为defer函数的return是不会影响到正常的返回值。而如果强行想改变,那么需要考虑正常返回的变量和defer函数中返回的变量是同一个地址的变量。那么应用到命名结果值,在release1()函数中,defer延迟函数里,err的值被赋值为“error”,所以在返回时如果err变量不为空(为空当然会返回nil),就会返回err的值,如此defer延迟函数里的赋值就生效了。

	/* 问题九 */
func release() error {
	defer func() error {
		return errors.New("error")
	}()
	return nil
}

func release1() (err error) {
	defer func() {
		err = errors.New("error")
	}()
	return nil
}

func main() {
	fmt.Println(release())
	fmt.Println(release1())
}
/*Output:
<nil>
error
*/

参考文章的示例,有所改动,并且内容并非完全翻译,都是笔者自己的话,所以不作为译文。

参考文章:
5 Gotchas of Defer in Go — Part I
5 More Gotchas of Defer in Go — Part II

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值