当前位置: 首页 > 科技观察

Gostruct不会犯的低级错误!

时间:2023-03-17 12:39:51 科技观察

大家好,我是炸鱼。前段时间分享了《手撕 Go 面试官:Go 结构体是否可以比较,为什么?》的文章,研究了Go基本结构体比较的基础。这不,最近有个读者遇到了一个关于struct的新问题,踩到了雷区。无法理解。一起来看看吧。建议大家看到代码示例后再想答案,再往下看。独立思考非常重要。疑惑例子给出的例子如下:typePeoplestruct{}funcmain(){a:=&People{}b:=&People{}fmt.Println(a==b)}你觉得输出结果是什么?输出是:假的。稍微修改一下,第二个例子如下:typePeoplestruct{}funcmain(){a:=&People{}b:=&People{}fmt.Printf("%p\n",a)fmt.Printf("%p\n",b)fmt.Println(a==b)}输出结果为:true。他的问题是“为什么第一个返回false,第二个返回true,是什么原因?建宇把这个例子进一步简化,得到最小的例子:funcmain(){a:=new(struct{})b:=new(struct{})println(a,b,a==b)c:=new(struct{})d:=new(struct{})fmt.Println(c,d)println(c,d,c==d)}输出结果://a,b;a==b0xc00005cf570xc00005cf57false//c,d&{}&{}//c,d,c==d0x118c3700x118c370true第一段代码的结果为假,第二段section结果为真,可以看出内存地址指向完全一致,也就是排除输出后变量内存点变化的原因,再往下看,好像是导致的通过fmt.Print方法,但是在一个标准库中输出的方法会不会导致这样奇怪的问题?是逃逸分析的结果。我们对例子进行逃逸分析://sourcecodestructure$cat-nmain.go5funcmain(){6a:=new(struct{})7b:=new(struct{})8println(a,b,a==b)910c:=new(struct{})11d:=new(struct{})12fmt.Println(c,d)13println(c,d,c==d)14}//逃逸分析$gorun-gcflags="-m-l"main.go#command-line-arguments./main.go:6:10:adoesnotescape./main.go:7:10:bdoesnotescape./main.go:10:10:cescapestoheap./main.go:11:10:descapestoheap./main.go:12:13:...argumentdoesnotescape通过分析可知变量a和b分配在栈上,而变量c和d分配在堆上。关键原因是调用了fmt.Println方法,涉及到大量反射相关的方法调用,会造成逃逸行为,即分配到堆上。为什么转义后相等?关注第一个细节,即“为什么两个空结构在转义后相等?”。这主要与Go运行时的一个优化细节有关,如下://runtime/malloc.govarzerobaseuintptr变量zerobase是所有0字节分配的基地址。此外,执行转义分析后,空(0字节)将指向零基地址。所以空struct转义后本质上指向zerobase,两者比较相等,返回true。为什么他们不逃避就不平等?关注第二个细节,即“为什么两个空结构在转义前不相等?”。Gospec从Gospec的角度来看,这是Go团队刻意设计的,我们不希望大家以此作为判断的依据。如下:这是一种有意的语言选择,可以让实现在如何处理指向零大小对象的指针方面具有灵活性。如果每个指向零大小对象的指针都需要不同,那么零大小对象的每次分配都必须分配至少一个字节。如果要求每个指向零大小对象的指针都相同,那么处理在较大结构中获取零大小字段的地址将是不同的。我也说了一句很经典的,Details:Pointerstodistinctzero-sizevariablesmayormaynotequal。另外,实际使用中空struct场景比较少。常见的有:设置上下文,传递的时候作为key。设置一个空结构体,供业务场景临时使用。但是,在业务场景的情况下,大部分都会随着业务的发展而不断变化。假设有一段古老的Go代码依赖于一个空结构的直接判断。不会是意外吧?不能直接依赖,所以围棋队的操作,和围棋地图的随机性如出一辙,值得思考,避免人们对这种逻辑的直接依赖。而在没有转义的场景下,两个空struct的比较动作,你觉得真的是在比较。其实在代码优化阶段已经直接优化了,转为false。因此,虽然在代码中看起来==是在做比较,但实际上当结果为a==b时,会直接变为false,不需要比较。你不觉得很棒吗?没有办法使它相等??。既然我们在代码优化阶段就知道它被优化了,那么相对地,知道了原理,我们也可以在go编译和运行时使用gcflags指令来阻止它被优化。运行前面的示例时,执行-gcflags="-N-l"命令:$gorun-gcflags="-N-l"main.go0xc000092f060xc000092f06true&{}&{}0x118c3700x118c370true你看,两次比较的结果都是真的。小结在今天的文章中,我们进一步完成了Go语言空结构的对比场景。经过这两篇文章的洗礼,你会更好地理解为什么围棋结构被称为既可比又不可比。空结构之所以神奇,是因为:如果它逃逸到堆上,空结构会默认分配runtime.zerobase变量,这是专门用来分配到堆上的0字节基地址。所以,这两个空结构都是runtime.zerobase,一对比当然是对的。如果没有发生逃逸,则将其分配到堆栈上。在Go编译器的代码优化阶段,会进行优化,直接返回false。不是传统意义上的,真的去比较。没有人会用它来出面试题,没办法,围棋结构为什么说可比却不可比呢?