前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从一个WaitGroup的例子看Go语言的Upvalue的传递

从一个WaitGroup的例子看Go语言的Upvalue的传递

作者头像
Linker
发布2018-04-13 16:04:18
8900
发布2018-04-13 16:04:18
举报

Go语言的闭包捕获的外部变量,我还是习惯以Lua的叫法,称之为Upvalue,毕竟Go借鉴了很多Lua的特性。

让我们首先看五个几乎一样的代码片段。

代码语言:javascript
复制
package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(wg sync.WaitGroup, i int) {
			log.Printf("i:%d", i)
			wg.Done()
		}(wg, i)
	}
	wg.Wait()
	log.Println("exit")
}

输出:

代码语言:javascript
复制
go run wgtest1.go 
2017/01/01 23:43:08 i:4
2017/01/01 23:43:08 i:2
2017/01/01 23:43:08 i:3
2017/01/01 23:43:08 i:1
2017/01/01 23:43:08 i:0
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc42000a2ac)
	/usr/local/Cellar/go/1.7.4_1/libexec/src/runtime/sema.go:47 +0x30
sync.(*WaitGroup).Wait(0xc42000a2a0)
	/usr/local/Cellar/go/1.7.4_1/libexec/src/sync/waitgroup.go:131 +0x97
main.main()
	/Users/linkerlin/gos/wgtest1.go:17 +0xba
exit status 2

这是因为Go语言中WaitGroup是一个不可以在第一次使用后复制的对象。而goroutine的主函数其实是传值的方法传递了WaitGroup。这里可以特别注意下i的输出是符合预期的。

好,让我们接下来看第二段代码:

代码语言:javascript
复制
package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			log.Printf("i:%d", i)
			wg.Done()
		}()
	}
	wg.Wait()
	log.Println("exit")
}

输出:

代码语言:javascript
复制
go run wgtest2.go 
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 exit

没有死锁,但是i值的输出是错误的。因为,Go语言里面upvalue是引用的。Goroutine多次捕获的是同一个i。

再来,我们看第三段代码:

代码语言:javascript
复制
package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			log.Printf("i:%d", i)
			wg.Done()
		}()
	}
	wg.Wait()
	log.Println("exit")
}

输出:

代码语言:javascript
复制
go run wgtest3.go 
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:4
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 exit

没死锁,i的数值还是不对。因为upvaule的i是byRef传递。注意,这里出现了4个5和一个4,最终输出什么其实是随机,取决于操作系统和硬件。goroutine调度的越快,就越可能出现比5小的输出。

再来,我们看第四段代码:

代码语言:javascript
复制
package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup, i int) {
			log.Printf("i:%d", i)
			wg.Done()
		}(&wg, i)
	}
	wg.Wait()
	log.Println("exit")
}

输出:

代码语言:javascript
复制
go run wgtest4.go 
2017/01/01 23:56:51 i:1
2017/01/01 23:56:51 i:0
2017/01/01 23:56:51 i:4
2017/01/01 23:56:51 i:2
2017/01/01 23:56:51 i:3
2017/01/01 23:56:51 exit

一切正常,符合预期。但是,这种写法却比较累赘。首先,没有利用闭包的upvalue来构建一个高阶函数,而是恢复到传统的传值,同时这种写法对写代码的人的心智负担太重了,传值和传引用要手动指定,而且还要在goroutine的主函数入口一一指定。那么我们推荐的写法应该是什么样子的呢?

最后,来看第五段代码:

代码语言:javascript
复制
package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		func(i int) {
			wg.Add(1)
			go func() {
				log.Printf("i:%d", i)
				wg.Done()
			}()
		}(i)
	}
	wg.Wait()
	log.Println("exit")
}

输出:

代码语言:javascript
复制
go run wgtest5.go 
2017/01/02 00:03:32 i:4
2017/01/02 00:03:32 i:0
2017/01/02 00:03:32 i:1
2017/01/02 00:03:32 i:2
2017/01/02 00:03:32 i:3
2017/01/02 00:03:32 exit

一样的一切正常。但是在第五段代码中,Goroutine的主函数是没有参数的。传引用的情况利用了upvalue,而需要传值的i变量用了一个外包函数的参数来复制。因为每次循环都会调用这个外包函数,从而复制了一次i的数值,虽然里层的Goroutine主函数还是 通过 upvalue来捕获i,不过每次捕获的都是外包函数的i副本而已。

综上所述,处于降低开发人员心智负担的考虑,我建议:

    1. Go语言里面的goroutine的入口函数不要传递参数。

    2. 所有的传ref参数都通过upvalue来捕获。  

    3. 如果要传值,可以在goroutine外面包一个函数,把要传value的参数用传值的方法传给这个外包的函数。参数名保持同名。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档