Go 语言中空 `struct{}` 占多少空间?

Evan
Evan
发布时间:2026-08-25 | 96人浏览过
原创

前言

在 Go 里,`struct{}` 是一个很容易见到、却经常被一句“占用 0 字节”带过的类型。真正写到工程代码时,很多人更关心的是:它为什么能这么设计、哪些场景值得优先用、遇到结构体对齐这类细节时会不会踩坑。下面就按“先验证,再解释,最后落到实战”的顺序,把空结构体的行为和用法讲清楚。

在 Go 里,struct{} 是一个很容易见到、却经常被一句“占用 0 字节”带过的类型。真正写到工程代码时,很多人更关心的是:它为什么能这么设计、哪些场景值得优先用、遇到结构体对齐这类细节时会不会踩坑。下面就按“先验证,再解释,最后落到实战”的顺序,把空结构体的行为和用法讲清楚。

什么是空结构体

空结构体就是没有任何字段的结构体,既可以匿名定义,也可以先声明类型再使用:

// 匿名空结构体
var a struct{}

// 命名空结构体
type EmptyStruct struct{}
var b EmptyStruct

它最显眼的特点,是值本身不承载任何数据,因此在很多场景里都能作为“只表达存在、不表达内容”的类型来使用。这也是它在 Go 中频繁出现在集合、信号通知和无状态对象里的原因。

为什么它看起来占用 0 字节

先看一个最直接的验证方式,用 unsafe.Sizeof 比较普通类型和空结构体:

用纸张卡片和箭头说明 Go 空结构体为何显示为 0 字节,以及零大小对象与地址复用的关系
空结构体为什么是 0 字节用专栏式卡片把“Sizeof 为 0”“zerobase”“地址示例”“不要依赖地址语义”放在一。
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a int
    var b string
    var e struct{}

    fmt.Println(unsafe.Sizeof(a)) // 8字节
    fmt.Println(unsafe.Sizeof(b)) // 16字节  
    fmt.Println(unsafe.Sizeof(e)) // 0字节
}

从输出可以直接看到,struct{} 的大小是 0。这也是“空结构体不占内存”这句话的来源。

底层机制:编译器如何处理空结构体

空结构体之所以能表现为零大小,关键在 Go 运行时和编译器对零大小对象的处理。文中提到的核心点是 zerobase 机制:当编译器识别到 struct{} 这类零大小类型的分配时,会复用一个统一的基础地址表示它们。

用暖色便签和卡片概括 Go 空结构体的典型工程用法,包括 Set、信号通道、无状态接收器和事件订阅
哪些场景最适合用空结构体这一张不重复讲原理,改用四格专栏卡片整理工程里最常见的落地写法。

因此,示例里常会看到多个空结构体变量打印出相同地址:

package main

import "fmt"

func main() {
    a := struct{}{}
    b := struct{}{}

    fmt.Printf("%pn", &a) // 0x58e360
    fmt.Printf("%pn", &b) // 0x58e360
    fmt.Println(&a == &b)   // true
}

这说明零大小对象在实现上可以共享表示方式,但这里有个边界不能忽略:不要把“示例里地址相同”理解成语言层面的绝对保证。更稳妥的理解是,空结构体体现了 Go 对零内存抽象的优化设计,而不是鼓励你在业务逻辑里依赖它的地址行为。

空结构体最常见的四种用法

空结构体真正有价值的地方,不是“概念上占 0 字节”,而是它能把“这个值没有负载,只是一个标记”表达得非常清楚。下面几种场景最典型。

用 map 模拟 Set

Go 没有内置 Set,工程里最常见的做法之一就是用 map[T]struct{} 表示集合成员是否存在。相比 map[string]bool,这种写法不需要为值部分再存一个布尔语义,意图也更直接。

type Set map[string]struct{}

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

func (s Set) Add(key string) {
    s[key] = struct{}{}
}

func (s Set) Delete(key string) {
    delete(s, key)
}

func (s Set) Intersection(other Set) Set {
    result := make(Set)
    for key := range s {
        if other.Has(key) {
            result.Add(key)
        }
    }
    return result
}

这里的重点不是炫技,而是告诉读代码的人:map 的值没有业务含义,只关心 key 在不在。数据量一大,这种写法在内存上也通常比 map[string]bool 更省。

作为 channel 的完成信号

如果通道只负责“通知某件事发生了”,而不是传业务数据,那么 chan struct{} 往往就是最贴切的选择。

func worker(done chan struct{}) {
    fmt.Println("Working...")
    time.Sleep(2 * time.Second)
    fmt.Println("Work completed!")
    done <- struct{}{} // 发送完成信号
}

func main() {
    done := make(chan struct{})
    go worker(done)

    <-done // 等待任务完成
    fmt.Println("Main: Done!")
}

这种模式在并发代码里非常常见,比如任务完成通知、协程退出同步、广播式关闭信号等。和 chan bool 相比,chan struct{} 少了一层“值到底该是 true 还是 false”的歧义,语义更干净:收到消息,就表示事件发生了。

无状态类型的接收器

有些类型存在的目的,只是为了挂载方法、满足接口,并不需要任何字段。这时直接用空结构体做接收器会很自然。

type Logger interface {
    Debug(msg string)
    Info(msg string) 
    Warn(msg string)
    Error(msg string)
}

// ConsoleLogger 控制台日志实现
type ConsoleLogger struct{} // 空结构体作为接收器

func (l ConsoleLogger) Debug(msg string) {
    fmt.Printf("[DEBUG] %sn", msg)
}

func (l ConsoleLogger) Info(msg string) {
    fmt.Printf("[INFO] %sn", msg)
}

这种写法常见于日志器、策略对象、占位实现、默认实现等场景。它的价值在于:类型语义存在,但实例本身没有状态负担,也不会为了“必须有个对象”而强行塞入无意义字段。

事件订阅与消息通知

当系统里存在大量“订阅者是否存在”“某个通知是否触发”的判断时,空结构体也很适合拿来做事件总线里的标记值。

type EventBus struct {
    subscribers map[string]map[chan struct{}]struct{}
    mu sync.RWMutex
}

func (eb *EventBus) Subscribe(event string) chan struct{} {
    eb.mu.Lock()
    defer eb.mu.Unlock()

    ch := make(chan struct{}, 1)
    if _, exists := eb.subscribers[event]; !exists {
        eb.subscribers[event] = make(map[chan struct{}]struct{})
    }
    eb.subscribers[event][ch] = struct{}{}
    return ch
}

这里的两层空结构体都很有代表性:map[chan struct{}]struct{} 用来表示“这个订阅通道是否存在”,而 chan struct{} 用来表达“只发信号、不带消息体”。在大量通知场景下,这种写法能把结构压得比较轻。

使用时要注意的边界

空结构体适合用,但不等于任何地方都该机械套用。真正容易被忽略的,通常是结构体布局和可读性问题。

用纸面标注说明空结构体作为结构体字段时的对齐问题,以及如何兼顾节省空间和代码可读性
空结构体并非任何位置都零成本最后一张图聚焦容易被误解的部分:单独是 0 字节,不代表放进结构体后永远没有代价。

最后一个字段时,可能出现额外填充

单独的 struct{} 大小是 0,但把它放进别的结构体里,就还要考虑对齐规则。特别是当它作为最后一个字段时,编译器可能做额外填充:

type Example struct {
    a int32
    b struct{} // 空结构体作为最后一个字段
}

fmt.Println(unsafe.Sizeof(Example{})) // 输出可能是8而不是4

这也是为什么“空结构体不占空间”不能被简单理解成“放到任何结构里都绝对没有成本”。如果你非常在意布局,应该结合 unsafe.Sizeof 实测,必要时调整字段顺序,比如把空结构体放在前面,而不是只凭直觉判断。

能省内存,也要让语义清楚

空结构体最大的优点之一,就是它能表达“这里没有数据,只有信号或标记”。但前提是命名和注释要跟上,否则读者只会觉得写法过于技巧化。

// 好的实践:使用有意义的变量名
var (
    DoneSignal  = struct{}{}
    StartEvent  = struct{}{}
    StopEvent   = struct{}{}
)

// 在通道使用时添加注释说明
ch := make(chan struct{}) // 用于任务调度的信号通道

换句话说,空结构体适合表达“无数据负载”的事实,但不适合拿来制造阅读门槛。尤其是在团队协作代码里,越是轻量的类型,越要把意图说清楚。

怎么判断该不该用空结构体

把全文收束成一个简单判断标准,通常会更实用:

  • 只关心存在性:例如 Set、订阅关系、标记位容器,适合用 map[T]struct{}。
  • 只传递事件,不传数据:例如完成通知、退出信号,适合用 chan struct{}。
  • 类型需要方法,但不需要状态:例如某些接口实现、默认实现,可以考虑空结构体接收器。
  • 对内存布局敏感:不要想当然,尤其是作为结构体字段时,先测大小再决定。

struct{} 看起来很小,但它体现的是 Go 里一种很典型的设计思路:当一个值不需要携带数据时,就明确告诉编译器和读代码的人,这里只保留语义,不保留负担。把这点用对了,代码通常会同时得到更清晰的表达和更克制的资源开销。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表