golang中的空结构体不能比较

什么是逃逸

垃圾回收是Go的一个很方便的特性–其自动的内存管理使代码更整洁,同时减少内存泄漏的可能性。但是,由于垃圾回收需要周期性的停止程序从而去收集不用的对象,不可避免的会增加额外开销。Go编译器是智能的,它会自动决定一个变量是应该分配在堆上从而在将来便于回收,还是直接分配到函数的栈空间。对于分配到栈上的变量,其与分配到堆上的变量不同之处在于:随着函数的返回,栈空间会被销毁,从而栈上的变量被直接销毁,不需要额外的垃圾回收开销。

但是在有些情况下,本应该分配到栈上的变量,被编译器智能的分配到了堆上,变量从栈逃逸到堆上就是逃逸现象。

逃逸分析
package main

import "fmt"

type People struct{}

func main() {
	f1()
	f2()
}

func f1() {
	a := &People{}
	b := &People{}
	fmt.Println(a == b)
}

func f2() {
	c := &People{}
	d := &People{}
	fmt.Printf("%p\n", c)
	fmt.Printf("%p\n", d)
	fmt.Println(c == d)
}

正常执行

> go run teststruct.go

false
0x5a6da8
0x5a6da8
true

逃逸分析

> go run -gcflags="-m -l" teststruct.go

# command-line-arguments
.\teststruct.go:13:7: &People literal does not escape
.\teststruct.go:14:7: &People literal does not escape
.\teststruct.go:15:13: ... argument does not escape
.\teststruct.go:15:16: a == b escapes to heap
.\teststruct.go:19:7: &People literal escapes to heap
.\teststruct.go:20:7: &People literal escapes to heap
.\teststruct.go:21:12: ... argument does not escape
.\teststruct.go:22:12: ... argument does not escape
.\teststruct.go:23:13: ... argument does not escape
.\teststruct.go:23:16: c == d escapes to heap

0x5a6da8
0x5a6da8
true

escapes to heap就说明发生了逃逸现象。通过分析可得知变量 a, b 均是分配在栈中,而变量 c, d 分配在堆中。关键原因是 fmt 中的打印函数很多用到了反射,会造成逃逸的行为。

具体分析问题

现在再来看上面的例子,是不是觉得很奇怪,即使发生了逃逸,a == b难道不应该是false吗?

从打印信息看,变量a和b存储的值都是0x5a6da8,也就是,他们指向的是同一个地址。

这里主要与 Go runtime 的一个优化细节有关,如下:

// runtime/malloc.go
var zerobase uintptr

变量 zerobase 是所有 0 字节分配的基础地址。更进一步来讲,就是空(0字节)的在进行了逃逸分析后,往堆分配的都会指向 zerobase 这一个地址。

所以空 struct 在逃逸后本质上指向了 zerobase,其两者比较就是相等的。

为什么没逃逸不相等?

从 Go spec 来看,这是 Go 团队刻意而为之的设计,不希望大家依赖这一个来做判断依据。如下:

This is an intentional language choice to give implementations flexibility in how they handle pointers to zero-sized objects. If every pointer to a zero-sized object were required to be different, then each allocation of a zero-sized object would have to allocate at least one byte. If every pointer to a zero-sized object were required to be the same, it would be different to handle taking the address of a zero-sized field within a larger struct.

还说了一句很经典的,细品:

Pointers to distinct zero-size variables may or may not be equal.

另外空 struct 在实际使用中的场景是比较少的,常见的是:

  • 设置 context,传递时作为 key 时用到。
  • 设置空 struct 业务场景中临时用到。

但业务场景的情况下,也大多数会随着业务发展而不断改变,假设有个远古时代的 Go 代码,依赖了空 struct 的直接判断,岂不是事故上身?

不可直接依赖
因此 Go 团队这番操作,与 Go map 的随机性如出一辙,避免大家对这类逻辑的直接依赖,是值得思考的。

而在没逃逸的场景下,两个空 struct 的比较动作,你以为是真的在比较。实际上已经在代码优化阶段被直接优化掉,转为了 false。

因此,虽然在代码上看上去是 == 在做比较,实际上结果是 a == b 时就直接转为了 false,比都不需要比了。

你说妙不?

没逃逸让他相等
既然我们知道了他是在代码优化阶段被优化的,那么相对的,知道了原理的我们也可以借助在 go 编译运行时的 gcflags 指令,让他不优化。

在运行前面的例子时,执行 -gcflags="-N -l" 指令:

go run -gcflags="-N -l" main.go

0xc000092f06 0xc000092f06 true

&{} &{}

0x118c370 0x118c370 true

你看,两个比较的结果都是 true 了。

总结
在今天这篇文章中,我们针对 Go 语言中的空结构体(struct)的比较场景进行了进一步的补全。经过这两篇文章的洗礼,你会更好的理解 Go 结构体为什么叫既可比较又不可比较了。

而空结构比较的奇妙,主要原因如下:

若逃逸到堆上,空结构体则默认分配的是 runtime.zerobase 变量,是专门用于分配到堆上的 0 字节基础地址。因此两个空结构体,都是 runtime.zerobase,一比较当然就是 true 了。
若没有发生逃逸,也就分配到栈上。在 Go 编译器的代码优化阶段,会对其进行优化,直接返回 false。并不是传统意义上的,真的去比较了。
不会有人拿来出面试题,不会吧,为什么 Go 结构体说可比较又不可比较?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值