关于空结构体struct{}补充|青训营笔记

77 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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("青训营!")
}

代码会输出“第五届”两秒后再输出“青训营!”。

本文仅作为本人学习的笔记及细节补充,若有疏漏还请读者不吝赐教