lab/note/随手记/go.md
Spirale b6063cb9f9 docs(go): 添加内存逃逸学习笔记与示例代码
- 新增 go/basics/MemoryEscape/main.go,演示返回局部变量指针和闭包引用两种典型逃逸场景
- 在 note/随手记/go.md 中补充内存逃逸的概念、影响、常见原因及逃逸分析命令
- 清理 note/随手记/网址.md 中的多余空行
2026-07-23 00:07:16 +08:00

2.4 KiB
Raw Blame History

内存逃逸

什么是逃逸

在函数内部创建的对象或者变量,当函数的生命周期结束后仍然被引用或持有,则该对象或者变量被称之为逃逸对象

影响

  • 对象分配到堆上,而不是栈上。
  • 堆内存由 Runtime 分配,由 GC 管理生命周期。
  • 相比栈分配,堆分配开销更大(需要 Runtime 参与)。
  • 增加 GC 扫描和回收压力,频繁逃逸可能影响程序性能。
  • 对象生命周期变长,不再随着函数返回立即释放,而是等待 GC 回收。
  • 如果多个 goroutine 共享该对象可能产生数据竞争Data Race但这是共享对象导致的不是逃逸本身导致的。
  • 逃逸本身不会导致内存泄漏但如果堆对象长期被引用GC 无法回收,就会造成 Go 意义上的内存泄漏。

常见原因

  1. 返回局部变量的指针

    局部变量在函数返回后仍然需要使用,因此必须分配到堆上。

    func NewUser() *User {
        u := User{}
        return &u
    }
    
  2. 局部变量被闭包引用

    闭包可能在函数返回后仍然执行,因此引用的局部变量需要继续存活。

    func f() func() int {
        a := 1
        return func() int {
            return a
        }
    }
    
  3. 被 goroutine 引用

    goroutine 的执行时间可能超过当前函数的生命周期。

    func f() {
        a := 1
        go func() {
            fmt.Println(a)
        }()
    }
    
  4. 作为 interface 参数或返回值(部分情况)

    如果编译器无法证明对象不会逃逸,就会保守地分配到堆上。

    func f() interface{} {
        a := 10
        return a
    }
    

    注意:并不是所有 interface 都会逃逸。

  5. 存入生命周期更长的对象

    例如存入全局变量、堆对象或其他长期存在的数据结构。

    var users []*User
    
    func f() {
        u := User{}
        users = append(users, &u)
    }
    

    这里 u 必须逃逸,因为 users 在函数返回后仍然存在。

  6. 编译器无法证明不会逃逸

    func g(x *User)
    
    func f() {
        u := User{}
        g(&u)
    }
    

    如果编译器无法确定 g 是否保存了 u 的地址,就会保守地让 u 逃逸到堆上。

逃逸分析命令

go build -gcflags -m main.go

出现moved to heap或者escapes to heap说明存在逃逸