对一段Go语言代码输出结果的简要分析
年后事情实在是多,各种被催进度,于是好长一段时间未更博客了,自责中….。今天蹦出来热热身^0^!
中午在微博私信中看到一封来自某Gopher的咨询,他贴了一段代码,并表示对代码的输出结果的不解,希望我能帮他分析一下。他的代码如下:
//testslicerange.go
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
在go playground上,其输出结果为(在我的多核mac,Go 1.10上面程序与此稍有不同,输出的item相同,只是前后顺序有不同):
one
two
three
six
six
six
虽然这位Gopher并没有明确说明他的疑惑究竟是什么?但从上述的输出结果来看,他一定是想问:为什么对data2的迭代输出的是三个”six”,而不是four、five、six?
好了,我来分析一下。首先,我要对这个程序做个等价变换,变换后的程序源码如下:
//testslicerange-transform.go
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func print(p *field) {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go print(v)
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go print(&v)
}
time.Sleep(3 * time.Second)
}
这里我把field结构体的method:print,换成了普通的以field指针作为第一个参数的函数print,这个变换是等价的,因为go中的method本质上就是以method的receiver作为第一个参数的普通function,即:
instance.method(x,y) <=> function(instance, x,y)
因此,执行上述的变换后的testslicerange-transform.go,得到的结果与testslicerange.go是一致的:
one
two
three
six
six
six
这样变换以后,问题是不是豁然开朗了,你可以很清楚地看到使用go关键字启动一个新goroutine时是如何绑定参数的:
- 迭代data1时,由于data1中的元素类型是field指针,因此赋值后v就是元素地址, 每次调用print时传入的参数(v)实际上也是各个field元素的地址;
- 迭代data2时,由于data2中的元素类型是field(非指针),因此赋值后v是元素的copy,每次传入的&v实际上是v的地址,而不是被copy的元素的地址;
剩下的就是for range常见的那个”坑”的问题(在我的《关于Go,你可能不注意的7件事》一文中有详尽说明),那就是v在整个for range过程只有一个,data2迭代完成之后,v是元素”six”的copy。
这样,一旦启动的各个child goroutine在main goroutine执行到Sleep时才被调度执行,那么最后的三个goroutine在打印&v时,打印的也就都v
中存放的值”six”了。而前三个child goroutine各自传入的是元素(“one”、”two”、”three”)的地址,打印的就是”one”、”two”、”three”。
那么原程序如何修改一下才能让其按期望输出(“one”、”two”、”three”, “four”, “five”, “six”)呢?我们来改一下:只需将field method的receiver type由*field改为field即可。
// testslicerange1.go
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
上述程序在go playground上的输出为:
one
two
three
four
five
six
至于为什么,可以参考我的分析思路,自行分析一下。
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
我的联系方式:
微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite
微信赞赏:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
评论