认识Go语言空结构体
结构体常识
在Go语言中,结构体(struct)是一种用户自定义的数据类型,用于封装一组相关的字段(field)。结构体可以包含不同类型的字段,并允许你创建具有特定属性和行为的自定义类型。
type 结构体名称 struct {
字段1 类型1
字段2 类型2
...
字段N 类型N
}
结构体名称是你为结构体定义的名称,字段是结构体的成员变量,类型是字段的数据类型。
golang 正常的 struct 就是普通的一个内存块,必定是占用一小块内存的,并且结构体的大小是要经过边界,长度的对齐的。
// 类型变量对齐到 8 字节;
type Tp struct {
a uint16
b uint32
}
按照内存对齐规则,这个结构体占用 8 个字节的内存。
空结构体
基本概念
空结构体(empty struct)是一个没有包含任何字段的结构体类型。它通常用于需要一个占位符类型,但又不需要存储任何实际数据的情况。空结构体通常写作 struct{}。
空结构体:
var s struct{}
// 变量 size 是 0 ;
fmt.Println(unsafe.Sizeof(s))
该空结构体的变量占用内存 0 字节。
空结构体有几个用途:
- 作为占位符:在需要一个类型,但不需要存储任何数据时,可以使用空结构体。
- 作为映射的键:当你想使用一个简单的、不占用太多内存的类型作为映射(map)的键时,空结构体是一个好选择。
- 作为信号量或标志:在某些同步原语中,可以使用通道(channel)来传递空结构体作为信号。
- 节省内存:当你需要一个类型,但内存使用是一个关键因素时,空结构体不占用任何额外的空间(除了可能的类型信息,这取决于运行时实现)。
本质上来讲,使用空结构体的初衷只有一个:节省内存,但是更多的情况,节省的内存其实很有限,这种情况使用空结构体的考量其实是:根本不关心结构体变量的值。
需要注意的是,虽然空结构体不占用任何额外的空间来存储字段,但在某些情况下(例如作为映射的键或通道的元素),它们仍然会占用一些内存来标识唯一的实例。这取决于Go运行时的具体实现。但相对于其他包含实际字段的结构体,空结构体在内存使用上是非常轻量级的。
特殊变量:zerobase
空结构体是没有内存大小的结构体。这句话是没有错的,但是更准确的来说,其实是有一个特殊起点的,那就是 zerobase 变量,这是一个 uintptr 全局变量,占用 8 个字节。当在任何地方定义无数个 struct {} 类型的变量,编译器都只是把这个 zerobase 变量的地址给出去。换句话说,在 golang 里面,涉及到所有内存 size 为 0 的内存分配,那么就是用的同一个地址 &zerobase 。
package main
import "fmt"
type emptyStruct struct {}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
}
dlv打印:
(dlv) p &a
(*struct {})(0x57bb60)
(dlv) p &b
(*struct {})(0x57bb60)
(dlv) p &c
(*main.emptyStruct)(0x57bb60)
(dlv) p &runtime.zerobase
(*uintptr)(0x57bb60)
空结构体的变量的内存地址都是一样的。
内存管理特殊处理
编译器在编译期间,识别到 struct {} 这种特殊类型的内存分配,会统统分配出 runtime.zerobase 的地址出去,这个代码逻辑是在 mallocgc 函数里面:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 分配 size 为 0 的结构体,把全局变量 zerobase 的地址给出去即可;
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ...
golang 使用 mallocgc 分配内存的时候,如果 size 为 0 的时候,统一返回的都是全局变量 zerobase 的地址。
参考:https://zhuanlan.zhihu.com/p/351176221