面试官:请问 Golang 中空 struct{} 有什么用?

面试回答

“在 Go 语言中,空结构体 struct{} 最核心的特点是它不占用任何内存空间unsafe.Sizeof 返回 0)。基于这个特性,它主要被用于三种场景:

第一种是实现 Set 集合。Go 没有内置的 Set 类型,我们通常用 map[T]struct{} 来模拟。把元素作为 map 的键,值用空结构体,这样既能利用 map 的哈希查找特性,又不会为值分配额外的内存。

第二种是作为信号通道(Signal Channel)。在并发编程中,我们经常需要用 channel 来传递完成、退出等信号,而不需要传递具体的数据。这时候声明一个 chan struct{},不仅语义明确(只发信号),而且在通信时不会产生任何内存开销。

第三种是仅包含方法的结构体。有时候我们需要定义一个对象来实现某个接口,但这个对象本身不需要任何状态(字段)。这时候就可以定义一个基于空结构体的类型,把方法挂载在它上面,实现零内存开销的面向对象设计。”

系统讲解

空结构体 struct{} 是 Go 语言中一个非常特殊的类型,它的宽度(占用内存大小)为 0。

核心特性

  • 零内存占用unsafe.Sizeof(struct{}{}) 的结果永远是 0。
  • 地址相同:在 Go 的底层实现中,所有大小为 0 的变量都指向同一个特殊的内存地址 runtime.zerobase。因此,两个空结构体变量的地址是相同的。

典型应用场景

1. 实现 Set 集合

Go 语言标准库没有提供 Set 数据结构。由于 map 的 key 必须是唯一的,通常使用 map 来实现 Set。如果 map 的 value 使用 bool,每个键值对会额外占用 1 字节内存;如果使用 struct{},则 value 不占用任何内存。

type Set map[string]struct{}
 
func main() {
    s := make(Set)
    
    // 添加元素
    s["apple"] = struct{}{}
    
    // 检查元素是否存在
    if _, ok := s["apple"]; ok {
        fmt.Println("apple exists")
    }
}

2. 信号通道 (Signal Channel)

在协程之间同步状态或发送通知时,如果只需要知道“事件发生了”,而不需要传递任何具体的数据内容,使用 chan struct{} 是最优雅且高效的做法。

func worker(done chan struct{}) {
    fmt.Println("Working...")
    time.Sleep(time.Second)
    fmt.Println("Done")
    
    // 发送完成信号
    done <- struct{}{}
}
 
func main() {
    done := make(chan struct{})
    go worker(done)
    
    // 阻塞等待信号
    <-done
    fmt.Println("All workers finished")
}

对比:如果使用 chan boolchan int,每次发送信号都会有额外的内存拷贝和占用,而 chan struct{} 是真正的“零成本”通信。

3. 无状态的对象 (仅包含方法的结构体)

在某些场景下,我们需要一个类型来实现某个接口,但这个类型本身不需要保存任何数据字段,只需要它的方法逻辑。

// 定义一个接口
type Greeter interface {
    Greet()
}
 
// 定义一个空结构体
type EnglishGreeter struct{}
 
// 实现接口方法
func (e EnglishGreeter) Greet() {
    fmt.Println("Hello!")
}
 
func main() {
    var g Greeter = EnglishGreeter{}
    g.Greet()
}

亮点与深度

底层原理:zerobase

为什么所有空结构体的地址都一样?在 Go 的 runtime 源码中,有一个特殊的全局变量 zerobase。当程序尝试分配大小为 0 的内存时(例如 new(struct{})make([]struct{}, 100)),内存分配器 mallocgc 会直接返回 zerobase 的地址,从而避免了实际的内存分配开销。

a := new(struct{})
b := new(struct{})
fmt.Println(a == b) // 输出: true (它们都指向 runtime.zerobase)

常见追问

追问:空结构体切片 []struct{} 会占用内存吗?

切片本身(Slice Header)会占用 24 字节(在 64 位机器上,包含指针、长度、容量各 8 字节)。但是,切片底层的数组不会占用任何额外的内存,无论这个切片的容量有多大。它的底层数据指针也会指向 zerobase

s := make([]struct{}, 10000)
fmt.Println(unsafe.Sizeof(s)) // 输出: 24 (Slice Header 的大小)
// 底层数组不分配内存