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

LinkerLin · · 1804 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

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

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")
}

输出:

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的输出是符合预期的。

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

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")
}

输出:

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。

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

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")
}

输出:

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小的输出。

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

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")
}

输出:

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的主函数入口一一指定。那么我们推荐的写法应该是什么样子的呢?

最后,来看第五段代码:

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")
}

输出:

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的参数用传值的方法传给这个外包的函数。参数名保持同名。

 

 

 

 


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:LinkerLin

查看原文:从一个WaitGroup的例子看Go语言的Upvalue的传递

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1804 次点击  
加入收藏 微博
2 回复  |  直到 1999-12-31 16:00:00
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传