Golang中空结构体指针的不一致行为问题

Golang中空结构体指针的不一致行为问题 我在研究空结构体 struct{} 时,发现比较指向空结构体的指针时有一些有趣的行为。虽然我在语言规范中没有找到相关说明,但编译器似乎将所有指向空结构体的指针都优化为指向同一个内存位置(无论它是局部变量还是全局变量)。

那么根据这个逻辑,比较指向两个空结构体的指针(无论它们是否代表不同的变量)应该总是返回 true?但编译器似乎也会对局部指针比较进行短路优化,如果它们代表不同的变量,则返回 false。

这导致了不一致的行为:当在局部和函数中比较相同的两个指针时,会得到不同的结果。这是一个 bug 吗?我在语言规范中没有找到任何能让我预料到这种行为的内容。

Go Playground 上的示例

var a struct{}

func main() {
	var b struct{}
	p1, p2 := &a, &b
	fmt.Printf("&a: %p, &b: %p \n", &a, &b)
	fmt.Printf("&a == &b: %v \n", &a == &b)
	fmt.Printf("p1 == p2: %v \n", p1 == p2)
	comp(p1, p2)
}

func comp(p1, p2 any) {
	fmt.Printf("p1 == p2: %v \n", p1 == p2)
}

这段代码为 &a&b 打印出相同的地址,但在主方法中进行比较时为 false,在函数中进行比较时却为 true。


更多关于Golang中空结构体指针的不一致行为问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

正如 @christophberger 所言,规范确实涵盖了这一点。你不能依赖比较指向零大小变量的指针所得到的布尔结果。但有趣的是,为什么在 main 函数和普通函数中会得到不同的结果。我认为这是由于 main() 中的优化导致的。

但我再次强调:你不应该编写依赖比较零大小变量地址的代码。

更多关于Golang中空结构体指针的不一致行为问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你说得对。虽然规范有些模糊:

指向不同零大小变量的指针可能相等,也可能不相等。 *

对我来说,多次比较相同的两个不同变量或在不同的调用点比较可能产生不同结果,这一点并不明显。但这是一种可能的解释,所以不算是一个错误。

*Go 规范:指针比较

你好 @falco467,

这确实是一个令人惊讶的行为,但这并不是一个错误。

这种行为的原因是,如果满足某些条件,变量可能会从调用栈中移除并分配到堆上。这样比较结果就可能不同。

完整解释:Golang 中比较空结构体的一个陷阱 - SoByte

另一篇有趣的文章:空结构体 | Dave Cheney

Go 语言规范中提到:“两个不同的零大小变量可能在内存中拥有相同的地址。”(强调是我加的。)

这是一个关于Go语言中空结构体指针比较的深入问题。根据Go语言规范,空结构体struct{}的大小为0,编译器确实会对空结构体的内存分配进行特殊优化。

问题分析

在你的示例中,关键点在于:

  1. 全局变量a和局部变量b都是空结构体
  2. 编译器优化使它们可能共享相同的内存地址
  3. 但Go的类型系统和比较规则导致了不一致的结果

代码示例分析

package main

import (
	"fmt"
)

var a struct{}

func main() {
	var b struct{}
	
	// 打印地址 - 由于优化,可能显示相同地址
	fmt.Printf("&a: %p, &b: %p \n", &a, &b)  // 可能输出相同地址
	
	// 直接比较地址 - 返回false,因为a和b是不同的变量
	fmt.Printf("&a == &b: %v \n", &a == &b)  // false
	
	p1, p2 := &a, &b
	fmt.Printf("p1 == p2: %v \n", p1 == p2)  // false
	
	comp(p1, p2)  // 在函数中比较,返回true
}

func comp(p1, p2 any) {
	// 当转换为interface{}时,比较的是底层值
	fmt.Printf("p1 == p2: %v \n", p1 == p2)  // true
}

根本原因

这种不一致行为是由于:

  1. 编译器优化:空结构体确实可能共享相同的内存地址
  2. 类型系统差异&a&b在编译时是不同类型的指针(*struct {}
  3. interface{}转换:当转换为any(即interface{})时,比较的是底层值而非指针地址

更详细的示例

package main

import (
	"fmt"
	"unsafe"
)

var globalEmpty struct{}

func main() {
	localEmpty := struct{}{}
	
	// 显示地址(可能相同)
	fmt.Printf("global: %p, local: %p\n", &globalEmpty, &localEmpty)
	
	// 直接指针比较
	fmt.Printf("&globalEmpty == &localEmpty: %v\n", 
		&globalEmpty == &localEmpty)  // false
	
	// 通过unsafe.Pointer比较
	fmt.Printf("unsafe compare: %v\n",
		unsafe.Pointer(&globalEmpty) == unsafe.Pointer(&localEmpty))  // 可能为true
	
	// 类型断言比较
	var i1, i2 any = &globalEmpty, &localEmpty
	fmt.Printf("interface compare: %v\n", i1 == i2)  // true
	
	// 多个局部空结构体
	var c, d struct{}
	fmt.Printf("&c == &d: %v\n", &c == &d)  // false
	fmt.Printf("interface(&c) == interface(&d): %v\n", 
		any(&c) == any(&d))  // true
}

规范说明

虽然Go语言规范没有明确说明空结构体指针的比较行为,但根据规范:

  1. 指针比较比较的是地址值
  2. 空结构体的大小为0,编译器可以自由优化其内存分配
  3. 当转换为interface{}时,比较遵循interface值的相等性规则

这不是一个bug,而是编译器优化和语言类型系统交互的结果。对于空结构体指针的比较,建议:

  • 避免依赖指针比较的结果
  • 如果需要比较,使用明确的逻辑而非依赖地址相等性
  • 理解interface{}比较与直接指针比较的差异
回到顶部