当前位置: 首页 > 后端技术 > PHP

Gostruct不会犯的低级错误!

时间:2023-03-29 19:31:32 PHP

微信搜索【脑补炸鱼】关注这条炸肝炸鱼。本文GitHubgithub.com/eddycjy/blog已收录,附有我的系列文章、资料和开源Go书籍。大家好,我是炸鱼。前段时间分享了《手撕 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第一段代码的结果为假,第二段的结果为真,可以看出内存地址指向完全一致,即排除输出后变量内存点变化的原因。再往下看,好像是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:a不逃逸./main.go:7:10:b不逃逸。/main.go:10:10:c逃逸到堆。/main.go:11:10:d逃逸到堆。/main.go:12:13:...参数不escape通过分析可以知道,变量a和b分配在栈中,而变量c和d分配在堆中。关键原因是调用了fmt.Println方法,涉及到大量反射相关的方法调用,会造成逃逸行为,即分配到堆上。为什么转义后相等?关注第一个细节,即“为什么两个空结构在转义后相等?”。这主要与Go运行时的一个优化细节有关,如下://runtime/malloc.govarzerobaseuintptr变量zerobase是所有0字节分配的基地址。此外,执行转义分析后,空(0字节)将指向零基地址。所以空struct转义后本质上指向zerobase,两者比较相等,返回true。为什么他们不逃避就不平等?关注第二个细节,即“为什么两个空结构在转义前不相等?”。从Gospec来看,这是Go团队刻意设计的,我们不希望大家以此作为判断依据。如下:这是一种有意的语言选择,可以让实现在如何处理指向零大小对象的指针方面具有灵活性。如果每个指向零大小对象的指针都需要不同,那么零大小对象的每次分配都必须分配至少一个字节。如果要求每个指向零大小对象的指针都相同,那么处理在较大结构中获取零大小字段的地址将是不同的。我也说了一句很经典的,Details:Pointerstodistinctzero-sizevariablesmayormaynotequal。另外,实际使用中空struct场景比较少。常见的有:设置上下文并在传递时将其用作键。设置一个空结构体,供业务场景临时使用。但是在业务场景的情况下,大部分会随着业务的发展不断变化。假设有一段古老的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。不是传统意义上的,真的去比较。没有人会用它来出面试题,没办法,为什么说围棋结构可比却不可比呢?如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,微信搜索【脑补炸鱼】即可阅读,回复【000】一线大厂面试算法方案和资料我都准备好了;本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。参考Oshen的微信交流关于曹大的一个空struct“坑”