Golang中为什么emptyCtx使用int类型而非struct{}
Golang中为什么emptyCtx使用int类型而非struct{}
在 $SDKPath\go1.19.2\src\context\context.go 中,emptyCtx 的定义如下:
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
我不理解注释“它不是 struct{},因为此类型的变量必须具有不同的地址”的含义。为什么这里说需要不同的地址?相同的地址似乎不会引起任何问题。如果这里可以使用 struct{},应该能节省大量内存。 例如:
type emptyCtx struct{}
更多关于Golang中为什么emptyCtx使用int类型而非struct{}的实战教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢您的回复。您提供的示例清晰地展示了emptyCtx设计的合理性。
更多关于Golang中为什么emptyCtx使用int类型而非struct{}的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你好 @polarbear122,欢迎来到论坛。
polarbear122:
如果在这里可以使用 struct{},应该能节省大量内存。
真诚提问:一个普通的 Go 应用程序在其整个生命周期中会实例化多少个空的上下文(Background() 或 TODO())?我敢说这个数字很少会高到需要担心内存消耗的程度。
编辑补充:这应该是对原帖的回复
为了进一步阐述 @falco467 的回答,如果 emptyCtx 是一个 struct{},它的 String() 方法可能会返回错误的上下文名称,因为第一个 case 将总是匹配:
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
输出:
context.Background
context.Background
由于空结构体的宽度为0,它不需要占用任何空间。但一个不占用空间的东西无法在内存中拥有自己的地址。否则,它至少需要占用一个字节的内存,这样其他数据就无法使用相同的地址。
因此,空结构体共享相同的地址:
package main
import "fmt"
type emptyStruct struct{}
func main() {
var a emptyStruct
var b emptyStruct
fmt.Printf("a= %p b= %p", &a, &b) // 将为 a 和 b 打印相同的地址
}
如果你想检查一个上下文是否是“空上下文”,你需要一个唯一的地址。否则,其他所有作为空结构体的上下文对于 ctx == emptyContext 的检查也会返回 true,而这并不是我们想要的结果。
在Go语言中,struct{}类型的零值变量确实共享相同的内存地址,这是由编译器的优化行为导致的。emptyCtx需要不同的地址是因为它在context包中被用作单例模式的键值,地址的唯一性是正确工作的关键。
让我们通过代码示例来说明这个问题:
package main
import (
"fmt"
"unsafe"
)
// 使用struct{}的情况
type emptyCtxStruct struct{}
// 使用int的情况
type emptyCtxInt int
func main() {
// 测试struct{}类型的地址行为
var s1, s2 emptyCtxStruct
fmt.Printf("struct{} 地址比较:\n")
fmt.Printf("s1地址: %p\n", &s1)
fmt.Printf("s2地址: %p\n", &s2)
fmt.Printf("s1 == s2: %v\n", &s1 == &s2)
fmt.Printf("大小: %d字节\n\n", unsafe.Sizeof(s1))
// 测试int类型的地址行为
var i1, i2 emptyCtxInt
fmt.Printf("int 地址比较:\n")
fmt.Printf("i1地址: %p\n", &i1)
fmt.Printf("i2地址: %p\n", &i2)
fmt.Printf("i1 == i2: %v\n", &i1 == &i2)
fmt.Printf("大小: %d字节\n\n", unsafe.Sizeof(i1))
// 演示context包中的实际使用场景
demoContextUsage()
}
func demoContextUsage() {
// 模拟context包中的使用方式
type key struct{}
// 如果使用struct{}作为键类型
var backgroundKey struct{}
var todoKey struct{}
// 这两个键会有相同的地址
fmt.Println("struct{}作为键:")
fmt.Printf("backgroundKey地址: %p\n", &backgroundKey)
fmt.Printf("todoKey地址: %p\n", &todoKey)
// 在map中使用时会产生冲突
m := make(map[interface{}]string)
m[&backgroundKey] = "background"
m[&todoKey] = "todo" // 这会覆盖前一个值
fmt.Printf("map内容: %v\n", m)
fmt.Printf("map长度: %d\n", len(m))
}
在context包的实际实现中,background和todo这两个空context需要是不同的实例:
// context包中的实际实现
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// 如果emptyCtx是struct{},那么:
// &background == &todo // 这会导致问题
// 因为new(struct{})返回的指针指向相同的零值地址
// 而使用emptyCtx int时:
// &background != &todo // 正确,每个new()调用分配新内存
emptyCtx需要实现context.Context接口,包括String()方法。如果使用struct{},所有实例的String()方法调用会混淆,因为基于地址的字符串表示会相同。
内存节省的考虑在这里是次要的,因为emptyCtx实例在程序中只有两个(background和todo),使用int类型(8字节)相比struct{}(0字节)多消耗的内存可以忽略不计,而正确性是最重要的。

