惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
有赞技术团队
有赞技术团队
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
Vercel News
Vercel News
IT之家
IT之家
MyScale Blog
MyScale Blog
博客园_首页
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
罗磊的独立博客

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备
Go语言结构体内存对齐完全指南
zhengwenfeng · 2025-06-15 · via 郑文峰的博客

# 1. 简介

在Go语言开发中,结构体内存对齐是影响程序性能和内存效率的关键因素。本文将从底层原理出发,详细解析内存对齐的工作机制及其对程序性能的实际影响。

# 2. 使用结构体内存对齐效果是怎么样的?

创建两个结构体,其中的成员变量数量和类型完成一致,只有顺序摆放的不一样。

import (
	"fmt"
	"unsafe"
)

type PoorlyAligned struct {
	flag  bool
	count int64
	id    byte
}

type WellAligned struct {
	count int64
	flag  bool
	id    byte
}

func main() {
	fmt.Println(unsafe.Sizeof(PoorlyAligned{}))
	fmt.Println(unsafe.Sizeof(WellAligned{}))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行结果如下,可以看到 PoorlyAligned 的内存大小是24,而 WellAligned 的内存大小是 16

为什么改了变量的顺序,却在内存中的大小则不一样了呢?

# 3. 什么是结构体内存对齐?

CPU 访问内存时是根据字长来访问而不是字节。比如 32 位的 CPU 字长是 4 字节,64 位的 CPU 时 8 字节。这么设计的目的是减少 CPU 访问内存的次数,提高吞吐量。

这个时候我们看到结构体 PoorlyAligned,bool是 4 字节,int64 是 8 字节,byte是 4 字节,CPU 读取 bool 变量 flag 时,因为字长是 8 字节,必须得读 8 个字节,所以必须得补上空的 4 个字节。而 int64 的 count 变量本身是 8 字节的不需要补位,读byte 的 id 时和 flag 一样也需要补 4 个字节,最终是 24 个字节。

17499021159591749902115421.png

再看到结构体 WellAligned,顺序是 count、flag、id,count 可以一次取走,而 bool 可以和 id 在同一次一起取走,所以最终只需要在内存存储 16 个字节。

17499021329591749902132218.png

# 4. 针对结构体是否对齐的基准测试

func BenchmarkPoorlyAligned(b *testing.B) {
	for range b.N {
		var items = make([]PoorlyAligned, 10_000_000)
		for j := range items {
			items[j].count = int64(j)
		}
	}
}

func BenchmarkWellAligned(b *testing.B) {
	for range b.N {
		var items = make([]WellAligned, 10_000_000)
		for j := range items {
			items[j].count = int64(j)
		}
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果如下,内存对齐在速度上要快上约 35%,内存分配要少上约 50%

$ go test -bench=. -benchmem . 
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkPoorlyAligned-12            248           4575658 ns/op        240001038 B/op         1 allocs/op
BenchmarkWellAligned-12              356           3392792 ns/op        160006157 B/op         1 allocs/op
PASS
ok      main/demo       5.126s

1
2
3
4
5
6
7
8
9

# 5. 兼容硬件平台与架构

接下来看这么一种情况,f1 int8占用 1 个字节,f2 int16占用 2 个字节,f3 int8 也占用 1 个字节,但是输出的字节大小却是 6 而不是 4,因为这里也做了内存对齐。

type S1 struct { 
	f1 int8
	f2 int16
	f3 int8
}

func main() {
	s1 := S1{}
	fmt.Println(unsafe.Sizeof(s1))
}

1
2
3
4
5
6
7
8
9
10

运行输出:

问题来了,操作系统一个字长 8 个字节,那么无论是 4 还是 6 CPU 都能够一次拿到整体数据,效率是一样的呀。

这是因为,内存对齐不仅仅是为了保证 CPU 的读取效率,还有就是为了兼容硬件平台和架构做的限制,比如在 CPU 是 ARM架构 时强制要做内存对齐的,不做的话则会直接报错。

那么这里的对齐规则是怎么样的呢?

# 6. 内存对齐规则

# 6.1 对齐系数

字段类型不仅有类型大小,还有类型对齐系数, 其有以下两个规则

  • Go原始类型的对齐系数与类型长度相等。

可以使用unsafe.Alignof来查看类型的对齐系数,可以发现两者是一致的。

func main() {
	fmt.Println(unsafe.Sizeof(int64(1)))      // 8
	fmt.Println(unsafe.Sizeof(float32(32)))   // 4

	fmt.Println(unsafe.Alignof(int64(1)))     // 8
	fmt.Println(unsafe.Alignof(float32(32)))  //4
}

1
2
3
4
5
6
7

  • Go结构体类型的对齐系数是最长字段的对齐系数和系统对齐系数两者中的最小的那个。

可以看到结构体的对齐系数是 2,这是因为最长字段是 f2 其对齐系数是 2,操作系统是 64 位,其对齐系数是 8,取最小的也就是 2

type S1 struct { 
	f1 int8
	f2 int16
	f3 int8
}

func main() {
	s1 := S1{}
	fmt.Println(unsafe.Sizeof(s1))    // 6
	fmt.Println(unsafe.Alignof(s1))   // 2
}

1
2
3
4
5
6
7
8
9
10
11

# 6.2 字段偏移量

字段偏移量是指在结构体内存对齐后字段从起始位置开始的偏移量,也就是从起始位置开始计算的字节数,第一个字段的偏移量是 0。

# 6.3 struct 内存对齐规则

对齐规则如下:

  • 结构体中的每个字段所在的偏移量必须是其对齐系数的整数倍,如果不能保证则需要添加空的字节来保证内存对齐。
  • 结构体的长度一定要是其本身对齐系数的整数倍。

知道规则后,再回到之前的 S1 的例子:

  • f1 的偏移量为0,占用 1 个字节,默认是对齐的。
  • f2 的对齐系数是2,如果从偏移量 1 开始存储,则不是2的倍数,必须前面空出一个,从偏移量2开始,才是对齐系数的倍数,所以从偏移量从2开始存储,并存储2个字节
  • f3 的对齐系数是1,从偏移量 5 开始存储,存储 1 个字节,是对齐的。
  • 本结构体的对齐系数是 2,而大小需要时对齐系数的整数倍,而根据前面几步的长度是 5,不能对齐,所以还需要空出一个字节才能使结构体对齐,也就是长度为 6 字节
type S1 struct {   // 长度:6, 对齐系数: 2
	f1 int8        // 长度:1, 对齐系数: 1
	f2 int16       // 长度:2, 对齐系数: 2
	f3 int8        // 长度:1, 对齐系数: 1
}

1
2
3
4
5

17499021449591749902144432.png

# 6.4 struct{} 作为结构体最后一个字段

struct{}这样的空结构体的大小是为 0 的,如果其作为其他结构体字段的时候,是不占用内存的,也不需要考虑内存对齐的问题,但只有一种情况例外,就是struct{}作为结构体的最后一个字段时,是会进行内存对齐的,而其对齐规则是:会被填充对齐到前一个字段的大小。

这是因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)

具体案例如下,demo1、demo2、demo3中的struct{}的都会按照前一个字段的大小进行填充对齐。

type demo1 struct {
	c int8
	a struct{}
}

type demo2 struct {
	c int16
	a struct{}
}

type demo3 struct {
	c int32
	a struct{}
}

type demo4 struct {
	a struct{}
	c int32
}

func main() {
	fmt.Println(unsafe.Sizeof(demo1{})) // 2
	fmt.Println(unsafe.Sizeof(demo2{})) // 4
	fmt.Println(unsafe.Sizeof(demo3{})) // 8
	fmt.Println(unsafe.Sizeof(demo4{})) // 4
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 7. fieldalignment linter

通过使用fieldalignment linter工具可以提前发现内存对齐问题,可以帮助我们来对结构体中的字段顺序进行正确的排列,减少内存的占用,提高运行的效率。

该结构体通过上面的分析,长度为 6,为了内存对齐是补了 2 个字节的。

type S1 struct {
	f1 int8
	f2 int16
	f3 int8
}

1
2
3
4
5

在项目中新增文件.golangci.yaml,内容如下,主要是将 fieldalignmeng 功能开启

version: 2
linters:
  default: all
  enable:
    - govet
  settings:
    govet:
      enable:
        - fieldalignment

1
2
3
4
5
6
7
8
9

然后运行golangci-lint会有提示,结构体的大小可以被优化,从 6 到 4

$ golangci-lint run
...
main.go:9:9: fieldalignment: struct of size 6 could be 4 (govet)
type S1 struct {
...

1
2
3
4
5

修改结构体中字段顺序为不需要补齐内存的方式,再次运行上面的命令后,上面的提示错误信息就没有了。

type S1 struct {
	f1 int8
	f3 int8
	f2 int16
}

1
2
3
4
5

# 8. 最佳实践

  • 通过对字段的排序来减少内部无意义的填充。
  • 尽可能将类型大小相同的字段放在一起,避免大小交替。
  • 使用 fieldalignment linter 对代码进行校验,通过工具自动捕获