第 5 章:指针与内存模型
jerry北京市2026年4月30日Go 9 次阅读 约 8 分钟

理解 Go 的指针机制、值传递与引用传递、new/make 的区别,以及逃逸分析。
5.1 指针基础
指针存储变量的内存地址:
x := 42
p := &x // p 是指向 x 的指针,类型为 *int
fmt.Println(p) // 0xc0000b4008(内存地址)
fmt.Println(*p) // 42(解引用,获取指针指向的值)
*p = 100
fmt.Println(x) // 100(通过指针修改了 x)
Go 的指针相比 C/C++:
- 不支持指针运算(不能
p++) - 更安全,有垃圾回收
- 可以返回局部变量的指针(编译器会自动逃逸到堆上)
5.2 值传递 vs 引用传递
Go 中所有传参都是值传递(拷贝),但不同类型的行为不同:
// 值类型:int, float, bool, string, array, struct
func modifyValue(x int) {
x = 100 // 修改的是副本
}
a := 42
modifyValue(a)
fmt.Println(a) // 42(未改变)
// 通过指针实现"引用传递"效果
func modifyPointer(x *int) {
*x = 100
}
modifyPointer(&a)
fmt.Println(a) // 100(被修改了)
引用类型(底层包含指针):slice、map、channel、function、interface
// slice 传参:底层数组共享,但 append 可能导致分离
func modifySlice(s []int) {
s[0] = 100 // 修改底层数组,外部可见
}
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [100 2 3]
// 但 append 不会影响外部
func appendSlice(s []int) {
s = append(s, 4) // 外部的 s 不会改变
}
5.3 new 和 make
// new:分配内存,返回指针,值为零值
p := new(int) // *int,值为 0
s := new([]int) // *[]int,值为 nil
// make:只用于 slice、map、channel,返回初始化后的值(非指针)
s := make([]int, 5) // []int,len=5, cap=5
m := make(map[string]int) // map[string]int,已初始化可直接使用
ch := make(chan int, 10) // chan int,缓冲区大小 10
| 特性 | new(T) |
make(T, ...) |
|---|---|---|
| 适用类型 | 任意类型 | slice、map、channel |
| 返回值 | *T(指针) |
T(值) |
| 初始化 | 零值 | 根据类型初始化内部结构 |
5.4 逃逸分析(面试重点)
Go 编译器通过逃逸分析决定变量分配在栈上还是堆上:
// 不逃逸:变量在函数内使用,分配在栈上
func noEscape() int {
x := 42
return x // 返回值的拷贝
}
// 逃逸:返回局部变量的指针,必须分配在堆上
func escape() *int {
x := 42
return &x // x 逃逸到堆上
}
查看逃逸分析结果:
go build -gcflags="-m" main.go
# 输出类似:./main.go:5:2: moved to heap: x
常见逃逸场景:
- 返回局部变量的指针
- 发送指针到 channel
- 闭包引用局部变量
- interface 类型参数(如
fmt.Println) - 切片/map 存储指针
为什么关心逃逸?
- 栈分配快,自动回收,无 GC 压力
- 堆分配慢,需要 GC 回收
- 减少逃逸 = 减少 GC 压力 = 更好的性能
5.5 内存对齐
结构体字段的内存对齐会影响大小:
// 不好的排列:占 24 字节
type Bad struct {
a bool // 1 字节 + 7 字节填充
b float64 // 8 字节
c int32 // 4 字节 + 4 字节填充
}
// 好的排列:占 16 字节
type Good struct {
b float64 // 8 字节
c int32 // 4 字节
a bool // 1 字节 + 3 字节填充
}
fmt.Println(unsafe.Sizeof(Bad{})) // 24
fmt.Println(unsafe.Sizeof(Good{})) // 16
原则:按字段大小从大到小排列,减少内存填充。
5.6 垃圾回收(GC)概述
Go 使用三色标记清除算法(Tri-color Mark and Sweep):
三种颜色:
- 白色:未被访问,GC 结束后回收
- 灰色:已被访问,但引用的对象还未扫描
- 黑色:已被访问,引用的对象也已扫描
GC 流程:
- 所有对象标记为白色
- 从根对象(全局变量、栈上变量等)开始,标记为灰色
- 取出灰色对象,将其引用的白色对象标记为灰色,自身标记为黑色
- 重复步骤 3 直到没有灰色对象
- 回收所有白色对象
写屏障(Write Barrier):
- GC 与程序并发执行,需要写屏障保证正确性
- 防止黑色对象引用白色对象(三色不变式)
5.7 面试要点
-
Go 是值传递还是引用传递?
- 全部是值传递。slice/map/channel 看起来像引用传递,是因为它们底层包含指针
-
new 和 make 的区别?
- new 返回指针,适用于任意类型
- make 返回值,只用于 slice/map/channel
-
什么是逃逸分析?
- 编译器分析变量的生命周期,决定分配在栈还是堆
- 栈上分配更快,减少 GC 压力
-
Go 的 GC 算法是什么?
- 三色标记清除,并发执行,使用写屏障保证正确性
-
如何减少 GC 压力?
- 减少堆分配(避免不必要的逃逸)
- 使用 sync.Pool 复用对象
- 预分配切片容量
练习
- 编写函数,通过指针交换两个变量的值
- 使用
go build -gcflags="-m"分析一段代码的逃逸情况 - 比较两个结构体不同字段排列的内存大小
- 实现一个简单的对象池(使用 sync.Pool)
评论
登录 后发表评论
暂无评论