首页 / Go 语言入门教程 / 结构体内存布局

Go 语言入门教程

结构体内存布局

本教程共 80 篇 · 第 40 篇 · 更新于 2026-07-27 · 约 7 分钟阅读

GoGo 入门教程结构体内存对齐字段排列struct{}内存布局

40. 结构体内存布局

本节目标:搞懂结构体占多少内存、为什么要内存对齐、字段顺序怎么排能省空间,以及空结构体 struct{} 有什么用。

结构体占多大内存

结构体的大小等于所有字段大小之和吗?不一定。来看个例子:

package main

import (
    "fmt"
    "unsafe"
)

type Bad struct {
    a bool   // 1 字节
    b int64  // 8 字节
    c bool   // 1 字节
}

type Good struct {
    b int64  // 8 字节
    a bool   // 1 字节
    c bool   // 1 字节
}

func main() {
    fmt.Println(unsafe.Sizeof(Bad{}))  // 24
    fmt.Println(unsafe.Sizeof(Good{})) // 16
}

同样的三个字段,换个顺序,大小从 24 变成 16。这就是内存对齐的影响。

Note

unsafe.Sizeof 返回类型占用的字节数。这个函数名虽然带 unsafe,但只是读取大小,不做危险操作,可以放心用。

内存对齐是什么

CPU 读取内存不是逐字节的,而是按块(比如 8 字节)来读。如果数据正好放在块边界上,一次就能读完。如果跨了边界,得读两次再拼接,慢很多。

所以编译器会在字段之间插入「填充字节」(padding),让每个字段都对齐到自己大小的整数倍地址上。

Bad 为例:

字段  大小  对齐要求  实际位置
a     1     1        [0]
      padding 7字节   [1-7]
b     8     8        [8-15]
c     1     1        [16]
      padding 7字节   [17-23]
总计: 24 字节

Good 的情况:

字段  大小  对齐要求  实际位置
b     8     8        [0-7]
a     1     1        [8]
c     1     1        [9]
      padding 6字节   [10-15]
总计: 16 字节

把大的字段放前面,小的放后面,减少了中间的 padding。

Tip

排列字段的技巧:把大字段(int64、float64)放前面,小字段(bool、int8)放后面。这样能最大化减少 padding,省内存。

对齐规则

Go 的内存对齐规则:

  1. 每个字段的对齐值 = 该类型的大小(但最大不超过平台字长,64 位系统是 8)
  2. 结构体的对齐值 = 所有字段对齐值中最大的那个
  3. 结构体大小 = 对齐值的整数倍

常见类型的大小和对齐值(64 位系统):

类型大小对齐值
bool11
int8 / uint811
int16 / uint1622
int32 / uint32 / float3244
int / int64 / uint64 / float6488
string168
指针88
切片248

字段排列实战

来看一个实际的优化案例:

// 优化前:浪费空间
type User1 struct {
    Active   bool       // 1 + 7 padding
    Created  int64      // 8
    Verified bool       // 1 + 7 padding
    Score    int64      // 8
}
// 总大小: 32 字节

// 优化后:紧凑排列
type User2 struct {
    Created  int64      // 8
    Score    int64      // 8
    Active   bool       // 1
    Verified bool       // 1 + 6 padding
}
// 总大小: 24 字节

省了 25% 的内存。在大量结构体(比如百万级的 slice)场景下,这个优化很可观。

Warning

内存对齐优化通常不是首要任务。如果结构体数量不多,字段顺序怎么舒服怎么来。只有在大量实例的场景下才值得优化。

空结构体 struct{}

struct{} 是没有任何字段的结构体,大小为 0:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s struct{}
    fmt.Println(unsafe.Sizeof(s)) // 0
}

大小为 0 的类型有什么用?

用作信号通道

chan struct{} 是 Go 里常见的「信号通道」模式,表示「只关心有没有信号,不关心信号内容」:

done := make(chan struct{})

go func() {
    // 干活...
    close(done) // 完成后关闭通道
}()

<-done // 阻塞等待完成信号

用作集合

map[string]struct{} 可以当作集合(Set)来用,value 不占内存:

set := make(map[string]struct{})
set["apple"] = struct{}{}
set["banana"] = struct{}{}

if _, ok := set["apple"]; ok {
    fmt.Println("apple 在集合里")
}
Tip

struct{} 做 map 的 value 比 bool 省 1 字节。虽然一个元素才省 1 字节,但百万级时差距明显。

小结

结构体内存布局的要点:

  • 字段之间有 padding,大小不等于字段大小之和
  • 大字段放前面,小字段放后面,能减少 padding
  • 空结构体 struct{} 大小为 0,适合做信号和集合
  • unsafe.Sizeof 查看实际大小

下一部分进入接口,Go 类型系统的灵魂。