本身是一个java开发,遇到一些需求需要修改一些go系统的bug。这个bug特别典型,在语言描述的本身没有问题,但是go里面的表达的含义确实和java是不一样的。导致了发现问题,在阅读代码的时候感觉逻辑没问题,在debug的时候发现情况确实和自己想象的不一样。

问题模拟

package main
import (
"fmt"
"time"
)
type Person struct{
ID int
}
func main() {
P1 := Person{1}
P2 := Person{2}
var a[2]Person
a[0]=P1
a[1]=P2
for _,p := range a{
go func(){
p.say()
}()
}
time.Sleep(time.Duration(2)*time.Second)
}
func (p *Person) say() {
fmt.Println(p.ID)
}

上面的代码比较简单。就是描述了一个Person的结构体,他里面只有一个id的字段。在下面的for循环里使用了异步的匿名函数,调用的就是循环体里的临时变量p,然后输出他的id。大家可以先想想,最后的输出是什么。

结果描述

最终结果是输出了两个2。并不是简单想的1,2。

如果换成java

其实这里如果换成java写。我们for循环里去提交一个匿名函数给线程池,在1.8之前,我们需要显示的加final字段,在1.8之后,会默认加final,不需要语法上声明,但是如果尝试修改,就会编译报错。在java中的模式变成了方法中的值传递,要不就是runnable有构造参数,直接在构造的时候就传递进去,要不就是final的不可变,让匿名内部类有了访问的权限。

为什么相似的表达结果会不同

这里的匿名函数里是有闭包的概念,但是java有类似的表达–内部类和final,但是并不会有语法书里介绍到java8里会有闭包的概念。

java里的值传递,是比较准确的,而且有final修饰,线程执行方法的时候会比较准确的获取到。

go里的闭包只是知道他获取的是p,真正执行异步的时候会去拿p,此时p已经换成新的对象了,也就是说这个临时变量并不是不可变的。

解决方法

既然已经知道了问题是变量的对象修改导致的,那么解决的方式就是让他不变

for _,p := range a{
go func(p *Person){
p.say()
}(p)
}

利用函数的值传递让对象不可变。我们写匿名函数的时候写成有参数的匿名函数,并且把值传递给他,等这个方法异步执行的时候,就会获取方法里的参数,这样值就是正确的了,这里就避免了闭包,通过闭包才能获取的对象,我们通过方法传递。

错误原因

其实这里出现的一个问题是我们用java的思路去想go的代码,这样的错误很难发现,谁知道go的闭包永远是看那个变量,变量的值变了,他也会跟着变了。

我们编写代码,其实思路和逻辑已经好了,只是找一种语言去实现自己的逻辑,这里就有语言的迁移性的问题了,如果学了java不利用java的一些只是去学习go,那么我们会困在基础语法很长时间出不来。正常工作,我们都是想依靠其他语言,来学习新的语言,以此达到更快的上手的效果。

这里迷惑到我们的也就是这个能访问但是值会变的闭包行为。

小结

在现在的开发模式中,技术栈会越来越丰富,可能是一堆java的项目,但是为了和k8s交互会加入一部分go,为了快速的开发数据分析的项目,会加入python模块,所以语言基础大家学习会非常的多,现在我就是一边开着基础文档和函数,一边在写代码,往往是思路到了但是语法不会写。我的一个感受是尽量不使用语言的特性来编程。这样的一个好处是多种语言不会有特别大的区别,都是基础的语法的描述,坏处是可能一个功能不是按照最佳实践编写的,例如在go的老手里看你的代码会很一般,在java里也是一样,java有丰富的功能,尤其是第三方提供的,可能java的老手看你在重复造轮子。好处就是没有太多的坑点,例如上面的例子,不使用闭包这个功能,其实也不会有这样思想感觉到了,但是运行结果不对的时候。