写点什么

Go 空结构体:零内存的魔力

作者:陈明勇
  • 2023-06-08
    广东
  • 本文字数:2371 字

    阅读完需:约 8 分钟

Go 空结构体:零内存的魔力

作者:陈明勇

个人网站:https://chenmingyong.cn

文章持续更新,如果本文能让您有所收获,欢迎关注本号。

微信阅读可搜《Go 技术干货》。这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog,欢迎大家 Star 催更并持续关注。


Go Version → 1.20.4

前言

Go 语言中,有一种特殊的用法可能让许多人感到困惑,那就是空结构体 struct{}。在本文中,我将对 Go 空结构体进行详解,准备好了吗?准备一杯你最喜欢的饮料或茶,随着本文一探究竟吧。

什么是空结构体

不包含任何字段的结构体,就是空结构体。它有以下两种定义方式:


  • 匿名空结构体


    var e sruct{}
复制代码


  • 命名空结构体


    type EmptyStruct struct{}    var e EmptyStruct
复制代码

空结构体的特点

空结构体主要有以下几个特点:


  • 零内存占用

  • 地址相同

  • 无状态

零内存占用

空结构体不占用任何内存空间,这使得空结构体在内存优化方面非常有用,我们来通过例子看看是否真的是零内存占用:


package main
import ( "fmt" "unsafe")
func main() { var a int var b string var e struct{} fmt.Println(unsafe.Sizeof(a)) // 4 fmt.Println(unsafe.Sizeof(b)) // 8 fmt.Println(unsafe.Sizeof(e)) // 0}
复制代码


通过打印结果对比可知,空结构体内存占用为 0

地址相同

无论创建多少个空结构体,它们所指向的地址都相同的。


package main
import ( "fmt")
func main() { var e struct{} var e2 struct{} fmt.Printf("%p\n", &e) // 0x90b418 fmt.Printf("%p\n", &e2) // 0x90b418 fmt.Println(&e == &e2) // true}
复制代码

无状态

由于空结构体不包含任何字段,因此它不能有状态。这使得空结构体在表示无状态的对象或情况时非常有用。

为什么是零内存和地址相同

要理解为什么空结构体在内存上是零大小(零内存)并且多个空结构体的地址是相同的,需要深入研究 Go 的源码。


/go/src/runtime/malloc.go


// base address for all 0-byte allocationsvar zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ······
if size == 0 { return unsafe.Pointer(&zerobase) } ······
复制代码


根据 malloc.go 源码的部分内容,当要分配的对象大小 size0 时,会返回指向 zerobase 的指针。zerobase 是一个用于分配零字节对象的基准地址,它不占用任何实际的内存空间。

空结构体的使用场景

空结构体主要有以下三种使用场景:


  • 实现 Set 集合类型

  • 用于通道信号

  • 作为方法接收器

实现 Set 集合类型

Go 语言中,虽然没有内置 Set 集合类型,但是我们可以利用 map 类型来实现一个 Set 集合。由于 mapkey 具有唯一性,我们可以将元素存储为 key,而 value 没有实际作用,为了节省内存,我们可以使用空结构体作为 value 的值。


package main
import "fmt"
type Set[K comparable] map[K]struct{}
func (s Set[K]) Add(val K) { s[val] = struct{}{}}func (s Set[K]) Remove(val K) { delete(s, val)}
func (s Set[K]) Contains(val K) bool { _, ok := s[val] return ok}
func main() { set := Set[string]{} set.Add("陈明勇") fmt.Println(set.Contains("陈明勇")) // true set.Remove("陈明勇") fmt.Println(set.Contains("陈明勇")) // false}
复制代码

用于通道信号

空结构体常用于 Goroutine 之间的信号传递,尤其是不关心通道中传递的具体数据,只需要一个触发信号时。例如,我们可以使用空结构体通道来通知一个 Goroutine 停止工作:


package main    import (     "fmt"     "time"  )    func main() {     quit := make(chan struct{})     go func() {        // 模拟工作        fmt.Println("工作中...")        time.Sleep(3 * time.Second)        // 关闭退出信号        close(quit)   }()       // 阻塞,等待退出信号被关闭     <-quit     fmt.Println("已收到退出信号,退出中...")  }
复制代码


在这个例子中,创建了一个通道 quit,并在一个单独的 Goroutine 中模拟执行工作。在完成工作后,关闭了 quit 通道,表示退出信号。主函数在 <-quit 处阻塞,直到收到退出信号,然后打印一条消息并退出程序。


由于通道使用的类型是空结构体,因此不会带来额外的内存开销。


Go 标准库中,context 包中的 Context 接口的 Done() 方法返回一个通道信号,用于通知相关操作的完成状态。这个通道信号的返回值就是使用了空结构体。


type Context interface {    Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any}
复制代码

作为方法接收器

有时候我们需要创建一组方法集的实现(一般来说是实现一个接口),但并不需要在这个实现中存储任何数据,这种情况下,我们可以使用空结构体来实现:


type Person interface {   SayHello()   Sleep()}
type CMY struct{}
func (c CMY) SayHello() { fmt.Println("你好,我叫陈明勇。")}
func (c CMY) Sleep() { fmt.Println("陈明勇睡觉中...")}
复制代码


这个例子定义了一个接口 Person 和一个结构体 CMY ,并为 CMY 实现了 Person 接口,定义了一组方法(SayHelloSleep)。


由于 CMY 结构体为空结构体,因此不会带来额外的内存开销。

小结

在本文中,首先介绍了 Go 语言 空结构体 的概念和定义方式,它有两种定义方式;


随后对 空结构体 的特点进行介绍,包括其零内存和多个变量地址相同的特性;


接着进一步深入源码,探究了为什么空结构体在 Go 语言中是零内存且多变量地址相同,原因是当要分配的对象大小 size 为 0 时,会返回指向 zerobase 的指针;


最后列举了空结构体的三个使用场景,通过这些代码示例,展示了空结构体在实际应用中的一些常见用途。


你还知道 空结构体 的其他使用场景吗?欢迎评论区留言探讨。

发布于: 刚刚阅读数: 7
用户头像

陈明勇

关注

一个热爱技术,喜欢专研技术的程序员。 2021-10-20 加入

公众号:Go技术干货

评论

发布
暂无评论
Go 空结构体:零内存的魔力_Go_陈明勇_InfoQ写作社区