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

简介: 本文将会对 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 allocations
var 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 语言中是零内存且多变量地址相同,原因是当要分配的对象大小 size0 时,会返回指向 zerobase 的指针;

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

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

推荐内容

一文掌握 Go 并发模式 Context 上下文

Go 方法接收器:选择值接收器还是指针接收器?

Go 语言中没有枚举类型,但是我们可以这样做

目录
相关文章
|
28天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
100 71
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
148 13
|
2月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
69 11
|
2月前
|
存储 Rust Go
Go nil 空结构体 空接口有什么区别?
本文介绍了Go语言中的`nil`、空结构体和空接口的区别。`nil`是预定义的零值变量,适用于指针、管道等类型;空结构体大小为0,多个空结构体实例指向同一地址;空接口由`_type`和`data`字段组成,仅当两者均为`nil`时,空接口才为`nil`。
Go nil 空结构体 空接口有什么区别?
|
2月前
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
43 3
|
2月前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
32 3
|
2月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
2月前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
2月前
|
Java 编译器 测试技术
go语言避免不必要的内存分配
【10月更文挑战第18天】
53 1
|
2月前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
35 2