Golang中零宽度类型的指针相等行为探秘
Golang中零宽度类型的指针相等行为探秘 我想报告一个关于零宽度类型指针相等性的有趣行为。如果这是微不足道或已知问题,敬请谅解。
下面的两段代码看起来完全相同,但 &a == &b 的结果却不同。这似乎是可预测的:“变量在栈上占用不同的地址,除非需要指针”,而不是“当需要指针时,变量逃逸到堆上的同一地址”。
// go1.20.5 darwin/arm64
// go run input.go
func main() {
var a, b struct{}
fmt.Println(&a == &b) // false
}
// go build -gcflags="-m -N -l" -o output input.go
// # command-line-arguments
// ./input.go:9:13: ... argument does not escape
// ./input.go:9:17: &a == &b escapes to heap
// go1.20.5 darwin/arm64
// go run input.go
func main() {
var a, b struct{}
fmt.Println(&a == &b) // true
fmt.Printf("%p\n", &a) // 0x1172f60 *depends on env
fmt.Printf("%p\n", &b) // 0x1172f60 *depends on env
}
// go build -gcflags="-m -N -l" -o output input.go
// # command-line-arguments
// ./input.go:8:6: moved to heap: a
// ./input.go:8:9: moved to heap: b
// ./input.go:9:13: ... argument does not escape
// ./input.go:9:17: &a == &b escapes to heap
// ./input.go:10:12: ... argument does not escape
// ./input.go:11:12: ... argument does not escape
这是定义的行为、未定义的行为,还是一个错误? 请提供您可能有的任何见解或反馈。 谢谢。
更多关于Golang中零宽度类型的指针相等行为探秘的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我很高兴发现,我想了解的内容在规范中写得非常清楚! 根据这段文字,这种行为似乎没有被定义。
从现在开始,我会确保亲自查阅语言规范。 谢谢!
更多关于Golang中零宽度类型的指针相等行为探秘的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
语言规范的比较运算符部分对指针有如下说明:
指向不同零大小变量的指针可能相等,也可能不相等。
语言规范的最后一句写道:
两个不同的零大小变量在内存中可能具有相同的地址。
但这并不意味着它们必须如此。我不清楚是否有地方规定了这种情况发生的标准;我怀疑这是编译器的一个实现细节。
这是一个关于零宽度类型(如空结构体 struct{})指针相等性的深入观察,涉及Go编译器的逃逸分析和优化行为。您观察到的现象是定义的行为,由Go编译器对零宽度类型的特殊处理所导致。以下是对此行为的详细解释和示例代码分析。
核心机制
零宽度类型(如 struct{})在内存中不占用空间。当编译器遇到这类类型的变量时,可能会进行优化:
- 如果变量仅用于局部操作(如比较),编译器可能将其分配在栈上,每个变量有独立的地址(即使地址不指向实际内存)。
- 如果变量需要逃逸到堆(例如,通过
fmt.Printf打印指针地址),编译器可能将其分配到堆上的同一地址,因为零宽度类型无需实际内存分配。
代码分析
第一段代码:&a == &b 返回 false
func main() {
var a, b struct{}
fmt.Println(&a == &b) // false
}
- 逃逸分析:
&a == &b逃逸到堆,但变量a和b未逃逸(未打印地址)。 - 行为:编译器将
a和b分配在栈上的不同地址(即使地址不指向实际内存),因此&a == &b为false。
第二段代码:&a == &b 返回 true
func main() {
var a, b struct{}
fmt.Println(&a == &b) // true
fmt.Printf("%p\n", &a) // 与 &b 相同地址
fmt.Printf("%p\n", &b)
}
- 逃逸分析:
a和b均逃逸到堆(moved to heap: a和moved to heap: b)。 - 行为:编译器将逃逸的零宽度变量分配到堆上的同一地址(因为无需内存占用),因此
&a == &b为true,且%p打印相同地址。
示例代码验证
以下代码进一步演示了零宽度类型指针的行为:
package main
import (
"fmt"
"unsafe"
)
func main() {
// 情况1:局部变量,未逃逸
var x, y struct{}
fmt.Println("局部变量(未逃逸):")
fmt.Printf("&x == &y: %v\n", &x == &y) // 通常为 false
fmt.Printf("unsafe.Sizeof(x): %v\n", unsafe.Sizeof(x)) // 0
// 情况2:逃逸到堆
escape := func() (*struct{}, *struct{}) {
a, b := struct{}{}, struct{}{}
fmt.Printf("逃逸变量: &a == &b: %v\n", &a == &b) // 通常为 true
return &a, &b
}
p1, p2 := escape()
fmt.Printf("逃逸后: p1 == p2: %v\n", p1 == p2) // true
// 情况3:作为接口值传递
var iface1, iface2 interface{} = struct{}{}, struct{}{}
fmt.Printf("接口值: &iface1 == &iface2: %v\n", &iface1 == &iface2) // false,接口持有不同描述符
}
输出示例(具体地址可能因环境而异):
局部变量(未逃逸):
&x == &y: false
unsafe.Sizeof(x): 0
逃逸变量: &a == &b: true
逃逸后: p1 == p2: true
接口值: &iface1 == &iface2: false
结论
- 这是定义的行为:Go编译器对零宽度类型进行优化,当变量逃逸到堆时,可能分配同一地址以减少开销。
- 非错误:该行为符合Go语言规范,因为零宽度类型无需内存分配,编译器可自由优化地址分配。
- 依赖编译器实现:具体行为可能因Go版本或编译器优化而变化,但核心逻辑一致。
此行为在并发或依赖指针相等性的场景中需注意,但对于零宽度类型,通常应避免依赖指针地址。

