Go语言---为什么空结构体不占用任何的内存空间

Go语言的空结构体占用0字节,常被用作占位符。本文详细介绍了空结构体的特性,如在内存分配中的优化,以及在实现方法接收者、集合类型(如Set)和空通道中的应用。通过示例代码展示了如何利用空结构体优化内存使用和简化编程逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Go语言—空结构体

Go语言中宽度是描述一个类型实例所占用的存储空间的字节数。

宽度是一个类型的属性。在 Go 语言中的每个值都有一个类型,值的宽度由其类型定义,并且总是 8 bits 的倍数。

在Go语言中可以使用unsafe.Sizeof方法来获取一个类型所占用的字节数。

unsafe.Sizeof函数定义

// Sizeof takes an expression x of any type and returns the size in bytes
// of a hypothetical variable v as if v was declared via var v = x.
// The size does not include any memory possibly referenced by x.
// For instance, if x is a slice, Sizeof returns the size of the slice
// descriptor, not the size of the memory referenced by the slice.
// The return value of Sizeof is a Go constant.

// Sizeof接受任何类型的表达式x并返回字节大小
// 假设变量v的形式,就好像v是通过var v=x声明的一样。
// 该大小不包括x可能引用的任何内存。
// 例如,如果x是片,Sizeof返回片的大小
// 描述符,而不是片引用的内存大小。
// Sizeof的返回值是一个Go常量。
func Sizeof(x ArbitraryType) uintptr

常用数据类型所占用的字节数:

func main() {
	var a int
	var b string
	var c bool
	var d [3]int32
	var e []string
	var f map[string]bool
	fmt.Println(
		unsafe.Sizeof(a),
		unsafe.Sizeof(b),
		unsafe.Sizeof(c),
		unsafe.Sizeof(d),
		unsafe.Sizeof(e),
		unsafe.Sizeof(f),
	)
}

输出结果:
8 16 1 12 24 8

空结构体的特殊性

空结构体在各类系统中频繁出现的原因之一,就是需要一个占位符。而恰恰好,Go 空结构体的宽度是特殊的。

func main() {
	var s struct{}
	fmt.Println(unsafe.Sizeof(s))
}

输出结果:
0
type S struct {
	A struct{}
	B struct{}
}

func main() {
	var s S
	fmt.Println(unsafe.Sizeof(s))
}

输出结果:
0

其最终输出结果也是 0,完美切合人们对占位符的基本诉求,就是占着坑位,满足基本输入输出就好。

但这时候问题又出现了,为什么只有空结构会有这种特殊待遇,其他类型又不行?

这是 Go 编译器在内存分配时做的优化项

// base address for all 0-byte allocations
// 所有0字节分配的基址
var zerobase uintptr

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
 ...
 if size == 0 {
  return unsafe.Pointer(&zerobase)
 }
}

当发现 size 为 0 时,会直接返回变量 zerobase 的引用,该变量是所有 0 字节的基准地址,不占据任何宽度。

因此空结构体的广泛使用,是 Go 开发者们借助了这个小优化,达到了占位符的目的。

使用场景

了解清楚为什么空结构作为占位符使用的原因后,我们更进一步了解其真实的使用场景有哪些。

主要分为三块:

  • 实现方法接收者。
  • 实现集合类型。
  • 实现空通道。

实现方法接收者

type T string

func (s *T) Call()

又似乎有点不大友好,因为作为一个字符串类型,其本身会占据定的空间。

这种时候我们会采用空结构体的方式,这样也便于未来针对该类型进行公共字段等的增加。如下:

type T struct{}

func (s *T) Call() {
 fmt.Println("奔跑的小辣条")
}

func main() {
 var s T
 s.Call()
}

在该场景下,使用空结构体从多维度来考量是最合适的,易拓展,省空间,最结构化。

另外你会发现,其实你在日常开发中下意识就已经这么做了,你可以理解为设计模式和日常生活相结合的另类案例。

实现集合类型

在 Go 语言的标准库中并没有提供集合(Set)的相关实现,因此一般在代码中我们图方便,会直接用 map 来替代。

但有个问题,就是集合类型的使用,只需要用到 key(键),不需要 value(值)。

这就是空结构体大战身手的场景了:

type Set map[string]struct{}

func (s Set) Append(k string) {
 s[k] = struct{}{}
}

func (s Set) Remove(k string) {
 delete(s, k)
}

func (s Set) Exist(k string) bool {
 _, ok := s[k]
 return ok
}

func main() {
 set := Set{}
 set.Append("小辣条")
 set.Append("奔跑的小辣条")
 set.Append("第二杯半价")
 set.Remove("小辣条")

 fmt.Println(set.Exist("小辣条"))
}

空结构体作为占位符,不会额外增加不必要的内存开销,很方便的就是解决了。

实现空通道

在 Go channel 的使用场景中,常常会遇到通知型 channel,其不需要发送任何数据,只是用于协调 Goroutine 的运行,用于流转各类状态或是控制并发情况。

func main() {
	ch := make(chan struct{})
	go func() {
		time.Sleep(1 * time.Second)
		close(ch)
	}()

	fmt.Println("奔跑的...")
	<-ch
	fmt.Println("小辣条!")
}

输出结果:

奔跑的...
小辣条!

该程序会先输出 ”奔跑的…“ 后,再睡眠一段时间再输出 “小辣条!”,达到间断控制 channel 的效果。

由于该 channel 使用的是空结构体,因此也不会带来额外的内存开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值