Go中的空结构体/数组
在 Go 语言中,空结构体(struct{}) 和 空数组([0]type) 是非常特殊的类型,不包含任何字段或元素并且不占用任何内存空间
虽然听起来似乎没什么用,但在 Go 编程中有着广泛的应用
不占用内存空间
下面的代码片段用来验证是否占用内存空间
type EmptyS struct{} var s1 struct{} s2 := EmptyS{} s3 := struct{}{} type EmptyA [0]int var a1 [0]int a2 := EmptyA{} a3 := [0]int{} fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1)) fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2)) fmt.Printf("s3 addr: %p, size: %d\n", &s3, unsafe.Sizeof(s3)) fmt.Printf("a1 addr: %p, size: %d\n", &a1, unsafe.Sizeof(a1)) fmt.Printf("a2 addr: %p, size: %d\n", &a2, unsafe.Sizeof(a2)) fmt.Printf("a3 addr: %p, size: %d\n", &a3, unsafe.Sizeof(a3)) 执行输出结果: s1 addr: 0x92ca80 , size: 0 s2 addr: 0x92ca80 , size: 0 s3 addr: 0x92ca80 , size: 0 a1 addr: 0x92ca80 , size: 0 a2 addr: 0x92ca80 , size: 0 a3 addr: 0x92ca80 , size: 0
根据输出结果可知:
1、空结构体或空数组占用字节数为 0,即不占用内存空间 2、多个空结构体或空数组的内存地址相同
在Go官方的语言规范中对关于空结构体内存地址进行了说明:
如果一个结构体或数组类型不包含任何占用内存大小大于零的字段或元素,那么它的大小为零。 两个不同的零大小变量可能在内存中具有相同的地址
注意,官方用了可能这个词,因为这跟编译器实现有关,后续实现也可能会有变化
不仅对于空结构体大小为0,嵌套的空结构体表现与普通空结构体相同,大小也为0
type Empty struct{} type MultiEmpty struct { A Empty B struct{} } s1 := Empty{} s2 := MultiEmpty{} fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1)) fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2)) 执行输出结果: s1 addr: 0x49ba80, size: 0 s2 addr: 0x49ba80, size: 0
空结构体或空数组会影响结构体的内存对齐
空结构体或空数组也并不是什么时候都不会占用内存空间
当空结构体或空数组作为另一个结构体字段时,根据字段位置不同,可能因内存对齐原因,导致外层结构体大小不一样
type A struct { x int y string z struct{} } type B struct { x int z struct{} y string } type C struct { z struct{} x int y string } a := A{} b := B{} c := C{} fmt.Printf("struct a size: %d\n", unsafe.Sizeof(a)) fmt.Printf("struct b size: %d\n", unsafe.Sizeof(b)) fmt.Printf("struct c size: %d\n", unsafe.Sizeof(c)) 执行输出结果: struct a size: 32 struct b size: 24 struct c size: 24
上面示例中,当空结构体放在另一个结构体最后一个字段时,会触发内存对齐
此时外层结构体会占用更多的内存空间,所以如果程序对内存要求比较严格,在使用空结构体作为字段时需要考虑这一点
空结构体或空数组的用法
实现Set
最常用的就是用来实现 set(集合) 类型
type Set map[string]struct{} func (s Set) Add(element string) { s[element] = struct{}{} } func (s Set) Remove(element string) { delete(s, element) } // 检查 set 中是否包含指定元素 func (s Set) Contains(element string) bool { _, exists := s[element] return exists } func (s Set) Size() int { return len(s) } // 实现fmt.Stringer接口 func (s Set) String() string { format := "(" for element := range s { format += element + " " } format = strings.TrimRight(format, " ") + ")" return format } s := make(Set) s.Add("one") s.Add("two") fmt.Printf("set: %s\n", s) fmt.Printf("set size: %d\n", s.Size()) s.Remove("one")
map 的 key 实际上与 set 不重复的特性刚好一致,一个不需要关心 value 的 map 即为 set
正因如此,空结构体类型最适合作为这个不需要关心的 value 的 map 了,因为它不占空间,没有语义
申请超大容量Array
var a [1000000]string var b [1000000]struct{} fmt.Printf("array a size: %d\n", unsafe.Sizeof(a)) fmt.Printf("array b size: %d\n", unsafe.Sizeof(b)) 执行输出结果: array a size: 16000000 array b size: 0
信号通知
与channel结合当作信号来使用
done := make(chan struct{}) go func() { time.Sleep(1 * time.Second) // 执行一些操作... fmt.Printf("goroutine done\n") done <- struct{}{} // 发送完成信号 }() <-done // 等待完成 fmt.Printf("main exit\n")
由于struct{}并不占用内存,所以实际上 channel 内部只需要将计数器加一即可,不涉及数据传输,所以没有额外内存开销
无操作的方法接收器
有时候,需要“组合”一些方法,这些方法内部并不会用到方法接收器,这时就可以使用 struct{} 作为方法接收器。
type NoOp struct{} func (n NoOp) Perform() { // 一些不涉及操作n的操作 } 方法体中并没有引用 n,如果换成其他类型则会占用内存空间
禁止意外复制标识符
在go的1.7版本时引入了一个静态检查机制,当一个结构体中存在noCopy字段时,表示该结构体不希望在程序运行时被意外的复制
当用户在代码中显示的使用copy()等方法来复制时会成功复制,该标识符仅用于阻止在程序运行时被程序自动的复制行为
noCopy的定义: type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {} 禁止被意外复制的结构体: type Demo struct { noCopy noCopy // noCopy首字母为小写,不会导出 ... }
该文原文地址:https://juejin.cn/post/7376177615441068086,尊重原创
- Go语言中的bytes包
- 没有了