Page 238 - 《软件学报》2026年第3期
P. 238
李清伟 等: Go 语言程序的内存性能与安全问题实证研究 1201
5. // BAD: &i should not escape
6. x.p1 = &i
7. sink = x.p2
8. }
1.3 Go 语言程序中的内存安全问题
传统的内存安全问题主要包括如表 1 所示的 6 大类. 表中相应地总结了 Go 语言针对这些内存安全问题所采
取的措施. Go 语言对这些内存安全问题采用两种处理方式, 一类是运行时检测与错误报告, 另一类是编译期分析
与运行时机制结合. 例如, 对于内存泄漏、释放后使用、双重释放等问题, Go 语言采用垃圾回收的方式避免程序
员手动管理内存带来的问题. 针对需要显式释放的资源 (如网络连接、数据库连接、文件描述符、锁等), Go 语言
引入了 defer 关键字, 便于开发者释放和清理资源. 针对悬垂指针问题, Go 语言采用逃逸分析判断对象的生命期,
将生命期较长的对象分配在堆上, 并通过垃圾回收避免悬垂指针. 针对数组访问越界和空指针解引用问题, Go 编
译器生成检测代码并在运行时进行检查.
表 1 经典内存安全问题与 Go 语言采取的措施
经典内存安全问题 Go语言采取的措施
内存泄漏 (memory leak, LEAK) 垃圾回收
释放后使用 (use-after-free) 垃圾回收
双重释放 (double-free) 垃圾回收
悬垂指针 (dangling pointer, DP) 逃逸分析与垃圾回收
下标越界 (index out of bound) 运行时检查
空指针解引用 (null (nil) pointer dereference, NPD) 运行时检查
尽管 Go 语言在内存安全方面做出了诸多努力, 本文的实证分析表明, 内存泄漏和空指针解引用这两类问题
仍然在 Go 语言开源项目中存在, 表明当前的机制尚未完全解决这些问题.
虽然 Go 语言引入垃圾回收来减少内存管理的复杂性, 但这并不意味着能够完全避免内存泄漏. Go 的垃圾回
收使用基于标记-清扫的算法, 在运行时会从根集出发, 追踪和识别对象的引用关系, 将不再被使用的对象回收. 然
而, 由于垃圾回收需要追踪不同对象之间的引用关系, 即使部分对象已经不再被访问, 只要这些对象仍然被其他对
象引用, 它们就无法被回收, 从而造成内存泄漏问题. 另外, Go 语言通过 defer 关键字来帮助开发者管理资源释放,
但仍然需要程序员显式地调用释放函数. 如果开发者未能正确在适当时机使用 defer 或忘记释放资源, 那么这些资
源就会一直占用内存, 从而可能导致内存泄漏问题.
[7]
例如, grpc-go 仓库曾报告了如图 1 所示的内存泄漏问题 Issue#4758 : 随着图 1(b) 中用户 Clients 连接数的不
断增长, 内存使用量不断增加. 但是当用户退出连接之后, 内存使用量仍然保持不变, 没有被回收. 导致该问题的代
码如代码 3 中被删减的代码, q.queue[0] 所引用的对象后续不再被使用, 但由于没有将 q.queue[0] 置为 nil, 导致垃
圾回收器认为 q.queue[0] 所引用的对象仍然活跃, 不能被回收. 在该语句被执行多次后, 就会存在较多对象无法被
及时回收, 造成内存泄漏问题. 这类问题可以抽象总结为模式: 元素类型为指针类型的切片在切片表达式操作前没
有进行置空操作. 由于这类模式容易为静态分析工具所识别, 因此, 希望总结这类简单问题模式, 即回答 RQ2, 以
便后续静态分析工具能够识别和检测这类问题.
另外, 尽管 Go 语言通过生成代码插入运行时检查, 防止数组越界和空指针解引用等问题, 但是这些问题仍然
会导致程序在运行时崩溃, 从而终止执行以确保安全性. 如果能使用静态分析工具在程序运行前发现这些问题, 可
以避免因测试覆盖不足导致问题在应用部署后才出现. 静态分析也能够全面检查程序代码, 覆盖所有可能的执行
路径, 从而更有效地发现潜在问题. 因此, 静态分析检测内存安全问题有重要意义, 能够提高程序的稳定性和可
靠性.

