空结构体是指一个 struct 里不包含任何字段
type A struct{}
它的宽度是0,占用 0 字节的内存
unsafe.Sizeof(s) // 0
由纯空结构体组成的对象也不会占用内存空间
type A struct {
B struct{}
C struct{}
} // size 0
var Array [100]struct{} // size 0
Array = make([]struct{}, 100) // size 12 slice 的宽度
Go
使用场景
作为名称空间分割行为,可以把一些非实例化的行为放在里面
type _A struct{}
var A *_A
func (_ *_A) MethodOne() {}
func (*_A) MethodOne() {}
Go
由于空结构体不占用额外内存,可以用在 chan 传递信号上
c := make(chan struct{}, 100)
Go
同理用在 Map 上判断 key 是否存不存在
m := make(map[string]struct{})
Go
作为不导出的字段放在 Struct 中阻止 struct 被匿名字段的方式初始化
type Class struct {
_ struct{} // to prevent unkeyed literals
X, Y float64
}
Class{1,1} // errors too few values in struct literal
Class{X: 1, Y: 1} // ok
Go
这里的原理是因为在 golang 中对于 struct 的匿名赋值必须对所有字段进行赋值。所以当我们有不可导出字段时,就不能进行匿名字段赋值了,会报编译错误。并且空 struct 不会消耗额外的内存空间,这里有个前提是不能作为最后一个字段。如果作为 struct 的最后一个字段会填充对齐前一个字段的大小,产生额外的内存消耗。如果这里不填充额外的内存,末尾的空结构体的指针就会指向父 struct 外面的地址,产生内存泄露问题。
type Class struct {
X float64
Y struct{}
}
var c Class
fmt.Println(unsafe.Sizeof(c)) // 16
type Class struct {
X float64
B bool
Y struct{}
}
fmt.Println(unsafe.Sizeof(c)) // 16
type Class struct {
Y struct{}
X float64
}
fmt.Println(unsafe.Sizeof(c)) // 8
Go
比较问题
在 golang 中相同类型的 struct 是可以比较的, 不同类型的 struct 比较会报编译错误 mismatched types ,如果 struct 中包含 slice、map、function 时也是不能比较的会报编译错误 struct containing x cannot be compared , 这个应该是 go 团队偷懒没有实现这部分的比较代码。
其次 struct 在比较的时候会比较字段的值是否相同,而不会考虑指针指向的对象的值是否相同。
type Class struct {
X float64
S *string
}
s1 := "hello"
s2 := "hello"
c1 := Class{1, &s1}
c2 := Class{1, &s2}
fmt.Println(c1==c2) // false
Go
对于空结构体的比较:
type Class struct {}
c1 := Class{}
c2 := Class{}
fmt.Println(c1 == c2) // true
c3 := &Class{}
c4 := &Class{}
fmt.Println(c3 == c4) // false
c5 := &Class{}
c6 := &Class{}
fmt.Println(c5, c6)
fmt.Println(c5 == c6) // true
Go
对于第一个比较 c1 和 c2 的比较是比较好理解的, 但是对于第二个和第三个仅仅在 println 之前增加了一个 fmt.Println() 就导致比较结果从 false 变成了 true
我们可以打印具体指针地址来进行检查
c3 := new(Class)
c4 := new(Class)
println(c3, c4) // 0xc00006ef37 0xc00006ef37
fmt.Println(c3 == c4) // false
c5 := &Class{}
c6 := &Class{}
fmt.Println(c5, c6)
println(c5, c6) // 0x1164fe0 0x1164fe0
fmt.Println(c5 == c6) // true
Go
发现在进行 println 以后空结构体的地址发生了变化
这是因为调用 fmt.Println() 传入的变量发生了逃逸,这个是逃逸应该是在代码分析阶段就做的所以只要有调用了 fmt.Println() 里面的变量就会发生逃逸,即使目前还没有调用 fmt.Println() 变量也会发生逃逸。
为什么逃逸以后 c5 和 c6 的地址是相等的,这是因为 go runtime 在优化的时候,把 zerobase 变量作为所有 0 字节内存分配的基地址,也就是所有在堆上的 0 字节大小的对象都会指向这个地址。因为逃逸以后地址是相同的所以 c5 和 c6 比较是相等的
增加运行参数来查看变量逃逸情况:
go run -gcflags="-m -l" main.go
# command-line-arguments
./emtpy_struct.go:12:11: new(Class) does not escape
./emtpy_struct.go:13:11: new(Class) does not escape
./emtpy_struct.go:15:13: ... argument does not escape
./emtpy_struct.go:15:17: c3 == c4 escapes to heap
./emtpy_struct.go:17:8: &Class{} escapes to heap
./emtpy_struct.go:18:8: &Class{} escapes to heap
./emtpy_struct.go:19:13: ... argument does not escape
./emtpy_struct.go:22:13: ... argument does not escape
./emtpy_struct.go:22:17: c5 == c6 escapes to heap
Shell
c3 和 c4 其实地址也是相同的但为什么比较不相等,这其实是一个 Go 设计的坑,对于分配在栈上的 0 字节比较会对其进行优化,直接返回 false 可以在运行时增加 gcflags="-N -l” 不使用这个优化。下面是关于这个优化的官方回复:
The spec says "may or may not be equal." Either option is permitted. 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.
Shell
看样子这个优化的意思是如果每个零字节的对象都要不同至少需要额外分配一个字节,为了节省空间所以堆里面的零字节对象直接让他们相等。对于栈里面的零字节空间,可以分配额外的栈空间让其做区分是不是同一个对象:
c3 := new(Class)
c4 := new(Class)
c5 := c3
c6 := c4
println(c3, c4) // 0xc00006ef37 0xc00006ef37
println(c3 == c3)
println(c3 == c5)
println(c5 == c4)
println(c5 == c6)
println(c5, c6) // 0xc00006ef37 0xc00006ef37
c6 = c3
println(c5 == c6)
输出:
0xc000042770 0xc000042770
true
true
false
false
0xc000042770 0xc000042770
true
Go
可以看到在栈中相同的 struct 比较是相同的,不同的 struct 比较是不同的,即使他们的地址可能是同一个比较也会返回 false
参考: