1. 零内存占位符:struct{}{}
- 原理:
struct{}是空结构体,Go 编译器对其做了特殊处理,它在内存中不占任何空间(大小为 0 字节)。 - 场景 A:实现集合 (Set)
map[string]struct{}。比起map[string]bool,每个键值对能省下 1 字节。在千万级数据下,这就是 10MB+ 的纯内存节省。
- 场景 B:信号通知
done := make(chan struct{})。在协程同步时,只通知"到了",不需要传具体值,用它最省。
2. 切片预分配:make([]T, len, cap)
- 原理:如果不指定
cap(容量),切片在append过程中会频繁触发动态扩容(申请新数组 -> 拷贝旧数据 -> 销毁旧数组)。 - 骚操作:
- 如果你知道数据量(比如你的 300 个内容),永远使用
make([]string, 0, 300)。 - 效果:全程只申请一次内存,速度提升数倍,且减少了产生内存碎片的几率。
- 如果你知道数据量(比如你的 300 个内容),永远使用
3. 结构体字段对齐(Struct Alignment)
-
原理:CPU 访问内存是按"字长"(64位系统是 8 字节)对齐的。如果字段顺序乱放,编译器会为了对齐而填充空白字节(Padding)。
-
对比:
// ❌ 糟糕:占 24 字节 type Bad struct { A bool // 1 字节 // (填充 7 字节) B int64 // 8 字节 C bool // 1 字节 // (填充 7 字节) } // ✅ 骚操作:只占 16 字节 type Good struct { B int64 // 8 字节 A bool // 1 字节 C bool // 1 字节 // (最后填充 6 字节) } -
口诀:从大到小,依次排列(int64 -> int32 -> bool)。
4. 引用类型"传值":Map / Slice / Channel
- 原理:这三种类型底层都是 Header 结构(包含指针)。
- 骚操作:传参时直接传变量名,不要加
*号。 - 效果:避免了语法上的复杂性,同时保持了纳秒级的传参速度(只复制了几十个字节的 Header)。
5. 避免 Map 的 Value 中包含指针(针对千万级大 Map)
- 原理:Go 的垃圾回收器(GC)扫描 Map 时,如果发现 Key 或 Value 里有指针,就会进去扫描。千万级的 Map 如果存的是指针(比如
map[string]*User),GC 会压力山大,导致程序卡顿(STW)。 - 骚操作:
- 尽量存非指针类型(比如
map[int]int或map[string]MyStruct)。 - 如果数据很大,可以把对象存入切片,Map 只存切片的下标(int)。GC 发现 Map 里没指针,就会直接跳过扫描,性能起飞。
- 尽量存非指针类型(比如
6. 字符串与字节切片的"零拷贝"转换
- 原理:通常
string([]byte)会发生内存拷贝。 - 骚操作(Go 1.20+ 标准库已优化):
- 使用
unsafe包可以直接让string共享[]byte的底层数组。 - 虽然现在
unsafe用得少了,但在处理超大数据流时,这是省掉内存翻倍的关键。
- 使用
7. 这种 Map 其实能更小:int 代替 string
- 原理:
string在 Go 里占 16 字节(一个指针加一个长度)。如果你的千万级 Map Key 是可以转成数字的(比如 ID),用int做 Key。 - 性能:
map[int]any比map[string]any查找速度快 30%~50%,且内存占用更小。计算数字的 Hash 比计算字符串的 Hash 快得多。
8. 内存池化:sync.Pool
-
原理:如果你需要频繁创建和销毁临时对象(比如每次请求都要创建一个 300 长度的切片或临时结构体),频繁的堆内存分配会消耗 CPU 并增加 GC 压力。
-
骚操作:
var slicePool = sync.Pool{ New: func() any { return make([]string, 0, 300) }, } // 拿来用 tmp := slicePool.Get().([]string) // 用完还回去 slicePool.Put(tmp[:0]) -
效果:对象循环利用,几乎实现"零内存分配"运行。
9. 指针压缩:用切片下标代替指针
- 原理:在 64 位系统上,一个指针占 8 字节。如果你有千万个对象互相引用,光指针就占掉 80MB。
- 骚操作:把所有对象存在一个大的
[]User切片里,引用时记录int32类型的索引下标。 - 效果:
int32只占 4 字节,内存直接省一半,而且对 CPU 缓存(Cache)非常友好。
10. 位运算标记(Bitmask)
- 原理:如果你有几十个布尔状态(Switch),不要开几十个
bool字段。 - 骚操作:用一个
uint64变量,每一位(bit)代表一个开关。 - 效果:1 个字节能存 8 个状态。在千万级数据里,这种对齐后的节省非常惊人。
11. 数组访问的"边界检查消除"(BCE)
-
原理:Go 每次访问
a[i]都会检查i是否越界。 -
骚操作:
// 骚写法 _ = a[2] // 先访问最大的下标 // 后面的访问编译器就知道肯定不会越界了,不再检查 val1 := a[0] val2 := a[1] -
效果:在超大规模循环中,减少判断逻辑。
12. 字符串拼接:strings.Builder
- 原理:用
+拼接字符串,每次都会产生新字符串并拷贝旧数据。 - 骚操作:永远用
strings.Builder,并配合Grow(len)提前分配内存。 - 效果:在大循环里拼接字符串,效率比
+高出几个数量级。
优化工具:pprof 和 GODEBUG
- 看内存长在哪了:
在代码里加一句import _ "net/http/pprof"。运行后访问http://localhost:6060/debug/pprof/heap,你可以清晰地看到是哪一行代码占了那几百 MB 内存。 - 看 GC 忙不忙:
运行程序时加上环境变量:GODEBUG=gctrace=1 ./your_program。
它会实时打印出 GC 的频率和耗时。如果看到gc 10 ... wall time 100ms,说明你的 Map 已经让 GC 跑得很累了,这时候就需要考虑上面的第 5 条(避免指针)优化了。