在 Go 项目里,struct{} 经常出现在 map、chan 和一些很小的工具类型中。它看起来不起眼,但背后涉及零大小类型、地址复用、内存对齐和可读性取舍几个关键点。本文从“它到底占不占内存”讲起,再拆到去重、Set、并发信号和无状态接收器这些典型场景,帮助你判断什么时候该用,什么时候应当克制。
先弄清楚:Go 的空结构体到底是什么
Go 中的空结构体就是 struct{}。它不包含任何字段,因此属于一种特殊的零大小类型。很多文章会直接把它概括成“零内存占用”,这个说法大体没错,但更准确的理解应该是:它的实例大小为 0,因此不会像普通结构体那样为字段内容分配实际存储空间。
先看一个直接验证大小和地址的例子:
package main
import (
"fmt"
"unsafe"
)
type Empty struct{}
func main() {
var e Empty
var s struct{}
fmt.Printf("e addr: %p, size: %dn", &e, unsafe.Sizeof(e)) // output: e addr: 0x5800a0, size: 0
fmt.Printf("s addr: %p, size: %dn", &s, unsafe.Sizeof(s)) // output: s addr: 0x5800a0, size: 0
}
从输出可以看到两件事:
- 空结构体的大小是
0。 - 示例里的两个空结构体变量地址相同。
这也是很多人第一次接触 struct{} 时觉得反直觉的地方:既然地址都能一样,那它到底算不算一个“正常对象”?答案是算,但它是零大小对象,Go 运行时对这类对象会做特殊处理。
“地址相同”能不能当成固定结论
不能。更稳妥的说法是:两个不同的零大小变量可能拥有相同地址,但这不是必须成立的行为。

Go 语言规范里有一句很关键的话:
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
这段话的重点有两个:
- 如果结构体或数组中没有任何大小大于零的字段或元素,那么它的大小就是零。
- 两个不同的零大小变量在内存中可能有相同地址。
注意这里用的是 may,不是 must。也就是说,地址相同只是可能出现的实现结果,不应该把它当成业务逻辑依赖条件。原文里也提到一个常见例外:如果发生了内存逃逸,这些空结构体变量的地址就不一定相同。
因此,在工程里对空结构体的正确预期应该是:
- 可以利用它“大小为 0”这一事实。
- 不要利用“不同实例地址一样”来设计逻辑判断。
最常见的几个实用场景
1. 用作标记、去重和占位
空结构体最典型的用途,就是作为 map 的值来表示“某个 key 存在”。这类场景里你并不关心值本身,只需要一个占位符,所以用 struct{} 比用 bool 更贴近意图。

func ReDupUint64(ui64s []uint64) []uint64 {
var rets []uint64
m := make(map[uint64]struct{})
for _, ui64 := range ui64s {
if _, ok := m[ui64]; ok {
continue
}
rets = append(rets, ui64)
m[ui64] = struct{}{}
}
return rets
}
这段代码的核心思路很直接:遍历切片时,把已经出现过的元素记录到 map[uint64]struct{} 中;后续再遇到同一个值,就跳过。这里的 struct{}{} 只是占位,不承担业务数据含义。
这类写法适合去重、存在性判断、访问过节点标记、白名单命中记录等场景。它的优势不只是节省空间,更重要的是表达清晰:值没有意义,只看 key 是否存在。
2. 自定义轻量级 Set
Go 没有内建的 Set 类型,所以项目里常见做法是用 map 自己封装一个集合。既然集合只关心元素是否存在,那么把 value 定义为空结构体就很自然。
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{}{}
}
相比 map[string]bool,map[string]struct{} 的语义更准确:这里不是要记录真假,而是在维护一个唯一元素集合。对读代码的人来说,这种类型定义几乎能直接说明设计意图。
如果集合操作比较多,还可以在这个基础上继续补 Delete、Len、List 等方法;但底层思路并不会变,仍然是“key 承载数据,value 只负责占位”。
3. 在 channel 中只传递信号
并发场景下,如果 channel 的作用只是“通知对方某件事发生了”,而不是传输具体数据,那么 chan struct{} 是非常常见的写法。
done := make(chan struct{})
go func() {
// 执行任务
done <- struct{}{} // 发送完成信号
}()
<-done // 阻塞等待信号
这里发送方和接收方都不关心消息内容,只关心“信号到了没有”。因此使用空结构体很合适:语义明确,而且不会为了传一个无意义的值额外引入数据负担。
在实际项目里,这类写法常见于任务完成通知、协程退出同步、一次性触发器等场景。如果只是为了表示事件发生,struct{}{} 比传 1、true 或空字符串更不容易引起误解。
另外两个容易被忽略的用法
无状态方法接收器
有些类型只是为了挂载方法,实例本身不需要保存任何字段状态。这时候定义一个空结构体作为接收器,可以把“这是一个无状态对象”表达得很明确。
type Logger struct{} // 空结构体
func (l Logger) Log(msg string) {
fmt.Println(msg) // 不依赖实例状态
}
// 调用时零内存开销
logger := Logger{}
logger.Log("info")
这种方式常见于工具类实现、简单策略对象、接口适配器等场景。它的价值不在“省下了多少内存”,而在于类型设计足够直白:这个对象存在,是为了方法集合,而不是为了保存内部状态。
优化结构体内存布局时要注意位置
空结构体也可能出现在另一个结构体的字段中,但这里有一个容易踩的点:如果把它放在末尾,可能会引入额外的内存对齐填充。原文给出的建议是,把这类字段前置。
type A struct {
_ struct{} // 零大小
n int64 // 偏移量0
}
这段代码说明的是布局意识,而不是鼓励滥用空字段。只有当你确实在关注结构体尺寸、对齐和批量实例内存成本时,这种细节才值得优化。普通业务结构体如果为了“理论上的零占用”强行插入空字段,通常会让代码更难读。
什么时候值得用,什么时候该克制
空结构体在 Go 里确实很有用,但它并不是“看到能省一点空间就应该上”的通用答案。更实际的判断标准有三条:
- 你的 value 是否真的没有业务含义,只是占位或信号。
- 使用
struct{}后,代码语义是否比bool、int、普通结构体更清晰。 - 团队成员是否能快速读懂这段写法,而不是被“零大小技巧”干扰理解。
把它用在去重、Set、信号通知这些约定俗成的位置,收益通常很稳。相反,如果只是为了展示技巧而过度设计,空结构体就会从“简洁”变成“晦涩”。
归根结底,struct{} 最值得利用的是它清晰表达“这里没有数据,只有存在性或行为”的能力。至于零大小带来的节省,是加分项,但不该喧宾夺主。













