这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
在性能优化指南一节中,提到了使用空结构体以节省内存达到优化性能效果的技巧,不过没有对这个神秘的struct{}进行更详细一些的说明,因此在这里留些个人的细节补充。
解密struct{}的真身
从课程里,我们知道空结构体不占据内存空间,既节省资源,又本身就具备强语义,即可以在我们不关心值的时候仅作为占位符使用。
定义一个空结构体s,用unsafe.Sizeof(s)即可知道s占用的宽度为0。再定义一个空结构体t,输出t和s的地址,我们会发现,它们指向了同一个位置。
import "fmt"
func main() {
var s struct{}
var t struct{}
fmt.Printf("%p\n", &s)
fmt.Printf("%p\n", &t)
}
0xa90520
0xa90520
诶?为什么他们指向的内存地址是一样的呢?我们去翻一翻源码——
// base address for all 0-byte allocations
var zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
}
为什么struct{}这么特殊呢?实际上这是开发者们的一个巧妙的优化。
go编译器在分配内存时,如果检测到struct{}这一特殊的变量,就会返回变量zerobase的引用,全局变量zerobase的地址是所有零字节变量的基准地址,这就是0字节的真身。(你们都是我的小号?)
从这里我们就明白了struct{}的真身,然后我们就能愉快地开始了解这个占位符的各种用法了。
struct{}的各种玩法
从上文我们已经认识了struct{}——便宜又好用的零宽度占位符,那么接下来介绍一下这个“占位符”较为常见的用法。
作为receiver,实现包含方法的结构体
type T struct{}
func (s *T) method() {
//具体实现
}
func main() {
var a T
a.method()
}
我们并不关心a的值是什么,但需要通过其来调用函数,这时候就可以用到struct{}。
与map联合使用,实现集合set
在go中并没有set的相关实现,所以我们可以用map自己加工加工来实现。不过map是以键-值对形式储存内容,如果仅仅是用来当作set的话必然有一半的空间会浪费(value部分),这时候struct{}就完美地充当了占位符,占据值的位置。
type Set map[Typename]struct{}
作为通道的通知信号
在我们使用channel的时候,常常会需要一个只需要发送通知信号而不需要传递值的情况,这时候不管是用bool还是int都会占用额外的内存,因此可以使用struct{}。
func main() {
a := make(chan struct{})
go func() {
fmt.Println("第五届")
time.Sleep(2e9)
close(a)
}()
<-a
fmt.Println("青训营!")
}
代码会输出“第五届”两秒后再输出“青训营!”。
本文仅作为本人学习的笔记及细节补充,若有疏漏还请读者不吝赐教