β

对一段Go语言代码输出结果的简要分析

Tony Bai 75 阅读

年后事情实在是多,各种被催进度,于是好长一段时间未更博客了,自责中….。今天蹦出来热热身^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

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2018, bigwhite . 版权所有.

作者:Tony Bai
一个程序员的心路历程
原文地址:对一段Go语言代码输出结果的简要分析, 感谢原作者分享。

发表评论