3.3. 逃逸分析
3.3.1. 什么是逃逸分析
逃逸分析是 Go 编译器的一项优化技术,用于决定变量应该分配在栈上还是堆上。
分配位置 |
特点 |
|---|---|
栈 |
快速分配/释放,函数返回自动清理 |
堆 |
需要 GC 管理,分配较慢 |
3.3.2. 查看逃逸分析结果
go build -gcflags='-m' main.go
go build -gcflags='-m -m' main.go # 更详细
3.3.3. 常见逃逸场景
3.3.3.1. 场景 1:返回局部变量的指针
// 发生逃逸:返回局部变量的指针
func createUser() *User {
u := User{Name: "Alice"} // u 逃逸到堆
return &u
}
// 输出:moved to heap: u
3.3.3.2. 场景 2:接口类型
// 发生逃逸:赋值给接口类型
func printAny(v interface{}) {
fmt.Println(v)
}
func main() {
x := 42
printAny(x) // x 逃逸到堆(需要装箱)
}
3.3.3.3. 场景 3:闭包引用
// 发生逃逸:闭包引用外部变量
func closure() func() int {
x := 0 // x 逃逸到堆
return func() int {
x++
return x
}
}
3.3.3.4. 场景 4:切片扩容
// 可能逃逸:切片容量不确定
func appendSlice(s []int) []int {
return append(s, 1, 2, 3) // 如果需要扩容,新切片可能在堆上
}
3.3.3.5. 场景 5:大对象
// 发生逃逸:对象太大
func createLargeArray() [1024 * 1024]byte {
var arr [1024 * 1024]byte // 太大,逃逸到堆
return arr
}
3.3.3.6. 场景 6:动态类型
// 发生逃逸:类型在编译期未知
func createValue(typ string) interface{} {
switch typ {
case "int":
return 42
case "string":
return "hello"
}
return nil
}
3.3.4. 避免逃逸的技巧
3.3.4.1. 技巧 1:使用值类型而非指针
// ❌ 逃逸
func getUser() *User {
return &User{Name: "Alice"}
}
// ✅ 不逃逸
func getUser() User {
return User{Name: "Alice"}
}
3.3.4.2. 技巧 2:预分配切片容量
// ❌ 可能逃逸
func process(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
// ✅ 减少逃逸风险
func process(n int) []int {
result := make([]int, 0, n) // 预分配
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
3.3.4.3. 技巧 3:避免在循环中使用接口
// ❌ 每次迭代都逃逸
func sum(nums []int) int {
var total interface{} = 0
for _, n := range nums {
total = total.(int) + n
}
return total.(int)
}
// ✅ 使用具体类型
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
3.3.4.4. 技巧 4:使用 sync.Pool 复用对象
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) string {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
return buf.String()
}
3.3.4.5. 技巧 5:在函数内使用固定大小数组
// ❌ 切片逃逸
func hash(data []byte) []byte {
result := make([]byte, 32)
// ...
return result
}
// ✅ 数组不逃逸(如果不返回指针)
func hash(data []byte, out *[32]byte) {
// 写入 out
}
3.3.5. 逃逸分析实战
3.3.5.1. 示例:JSON 编码优化
// ❌ 多次逃逸
func toJSON(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
// ✅ 使用 Encoder 减少分配
func toJSON(w io.Writer, v interface{}) error {
enc := json.NewEncoder(w)
return enc.Encode(v)
}
// ✅ 使用 sync.Pool
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func toJSONBytes(v interface{}) ([]byte, error) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
if err := json.NewEncoder(buf).Encode(v); err != nil {
return nil, err
}
// 复制结果,因为 buf 会被复用
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result, nil
}
3.3.5.2. 示例:HTTP Handler 优化
// ❌ 每次请求都分配
func handler(w http.ResponseWriter, r *http.Request) {
response := &Response{
Code: 200,
Message: "OK",
Data: getData(),
}
json.NewEncoder(w).Encode(response)
}
// ✅ 复用 Response 结构
var responsePool = sync.Pool{
New: func() interface{} {
return new(Response)
},
}
func handler(w http.ResponseWriter, r *http.Request) {
response := responsePool.Get().(*Response)
defer responsePool.Put(response)
response.Code = 200
response.Message = "OK"
response.Data = getData()
json.NewEncoder(w).Encode(response)
}
3.3.6. 何时关注逃逸
场景 |
是否需要关注 |
|---|---|
高性能服务的热点路径 |
✅ 是 |
批处理任务 |
❌ 通常不需要 |
每秒处理数万请求 |
✅ 是 |
一次性脚本 |
❌ 不需要 |
内存受限环境 |
✅ 是 |