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

3 回复

我很高兴发现,我想了解的内容在规范中写得非常清楚! 根据这段文字,这种行为似乎没有被定义。

从现在开始,我会确保亲自查阅语言规范。 谢谢!

更多关于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 逃逸到堆,但变量 ab 未逃逸(未打印地址)。
  • 行为:编译器将 ab 分配在栈上的不同地址(即使地址不指向实际内存),因此 &a == &bfalse

第二段代码:&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)
}
  • 逃逸分析ab 均逃逸到堆(moved to heap: amoved to heap: b)。
  • 行为:编译器将逃逸的零宽度变量分配到堆上的同一地址(因为无需内存占用),因此 &a == &btrue,且 %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版本或编译器优化而变化,但核心逻辑一致。

此行为在并发或依赖指针相等性的场景中需注意,但对于零宽度类型,通常应避免依赖指针地址。

回到顶部