第 5 章:指针与内存模型

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

理解 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 流程:

  1. 所有对象标记为白色
  2. 从根对象(全局变量、栈上变量等)开始,标记为灰色
  3. 取出灰色对象,将其引用的白色对象标记为灰色,自身标记为黑色
  4. 重复步骤 3 直到没有灰色对象
  5. 回收所有白色对象

写屏障(Write Barrier):

  • GC 与程序并发执行,需要写屏障保证正确性
  • 防止黑色对象引用白色对象(三色不变式)

5.7 面试要点

  1. Go 是值传递还是引用传递?

    • 全部是值传递。slice/map/channel 看起来像引用传递,是因为它们底层包含指针
  2. new 和 make 的区别?

    • new 返回指针,适用于任意类型
    • make 返回值,只用于 slice/map/channel
  3. 什么是逃逸分析?

    • 编译器分析变量的生命周期,决定分配在栈还是堆
    • 栈上分配更快,减少 GC 压力
  4. Go 的 GC 算法是什么?

    • 三色标记清除,并发执行,使用写屏障保证正确性
  5. 如何减少 GC 压力?

    • 减少堆分配(避免不必要的逃逸)
    • 使用 sync.Pool 复用对象
    • 预分配切片容量

练习

  1. 编写函数,通过指针交换两个变量的值
  2. 使用 go build -gcflags="-m" 分析一段代码的逃逸情况
  3. 比较两个结构体不同字段排列的内存大小
  4. 实现一个简单的对象池(使用 sync.Pool)

← 上一章:复合数据类型 | 下一章:接口与多态 →

评论

登录 后发表评论

暂无评论