图解Go语言逃逸

示例:

go 复制代码
func main() {
    main_val := foo(666)
    println(*main_val)
}

func foo(arg_val int) *int {
    var foo_val int = 11
    return &foo_val
}

执行结果:

外部函数使用了子函数的局部变量.理论上来讲.子函数的foo_val的生命周期早就销毁了才对.所以在外层函数main函数中.如果访问子函数的foo_val局部变量.则一定是访问一个已经操作系统回收的空间.

逃逸分析过程示例:

Go语言在编译会自动决定把一个变量放在栈还是放在堆.编译器会做逃逸分析.当发现变量的作用域没有超出函数范围时.就可以放在栈上.反之则必须分配在堆.

示例:

go 复制代码
func main() {
    main_val := foo(666)
    println(*main_val, main_val)
}

func foo(arg_val int) *int {
    var foo_val1 int = 11
    var foo_val2 int = 12
    var foo_val3 int = 13
    var foo_val4 int = 14
    var foo_val5 int = 15
    for i := 0; i < 5; i++ {
       println(&arg_val, &foo_val1, &foo_val2, &foo_val3, &foo_val4, &foo_val5)
    }
    return &foo_val3
}

执行结果:

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

在编译的时候foo_val3被编译器判定为逃逸变量.因此将foo_val3放到堆中开辟.

new的变量在栈还是堆:

示例:

go 复制代码
func main() {
    main_val := foo(666)
    println(*main_val, main_val)
}

func foo(arg_val int) *int {
    var foo_val1 *int = new(int)
    var foo_val2 *int = new(int)
    var foo_val3 *int = new(int)
    var foo_val4 *int = new(int)
    var foo_val5 *int = new(int)
    for i := 0; i < 5; i++ {
       println(&arg_val, &foo_val1, &foo_val2, &foo_val3, &foo_val4, &foo_val5)
    }
    return foo_val3
}

执行结果:

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

流程图:

普遍的逃逸规则:

逃逸的普遍规则就是如果变量需要使用堆空间.就应该进行逃逸.实际上Go语言并不仅把逃逸的规则定的如此泛泛.Go语言中有许多场景具备出现逃逸的现象.

一般在给一个引用类对象中的引用类成员进行赋值时可能出现逃逸现象.可以理解为.访问一个引用对象实际上是底层通过一个指针来间接的访问.但如果访问里面的引用成员.则会有第二次间接访问.这样操作这部分对象时极大可能会出现逃逸的现象.

Go语言中有引用类型func(函数类型) interface(接口类型) slice(切片类型) map(字典类型) channel(管道类型) 和*(指针类型)等.

逃逸范例1:

go 复制代码
func main() {
    data := []interface{}{100, 200}
    data[0] = 100
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知data0=100发生了逃逸.

逃逸范例2:

如果变量是mapstringinterface{}类型且尝试通过赋值.则必定出现逃逸.

go 复制代码
func main() {
    data := make(map[string]interface{})
    data["key"] = 200
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过得知.data"key"=200发生了逃逸.

逃逸范例3:

如果mapinterface{}interface{}类型尝试通过赋值,则会导致key和value的赋值出现逃逸.

go 复制代码
func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知data100=200中.100和200均发生了逃逸.

逃逸范例4:

如果变量是mapstring\[\]string数据类型.则赋值会发生\[\]string逃逸.

go 复制代码
func main() {
    data := make(map[string][]string)
    data["key"] = []string{"value"}
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知\[\]string{...}切片发生了逃逸.

逃逸范例5:

如果变量是\[\]*int数据类型.则赋值的右值会发生逃逸现象.

go 复制代码
func main() {
    a := 10

    data := []*int{nil}
    data[0] = &a
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果可知.moved to heap:a 最终将变量a移动到了堆上.

如果变量是func(* int)函数类型.则进行函数赋值.会使传递的形参出现逃逸现象.

go 复制代码
func main() {
    data := 10
    f := foo
    f(&data)
    fmt.Println(data)
}

func foo(a *int) {
    return
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知data已经被逃逸到堆上.

逃逸范例7:

如果变量是func(\[\]string)函数类型.则进行\[\]string{"value"}赋值时会使传递的参数出现逃逸现象.

go 复制代码
func main() {
    s := []string{"abeld"}
    foo(s)
    fmt.Println(s)
}

func foo(a []string) {
    return
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知s escapes to heap.s被逃逸到堆上.

逃逸范例8:

go 复制代码
func main() {
    ch := make(chan []string)

    s := []string{"aceld"}
    go func() {
       ch <- s
    }()
}

用命令go build -gcflags="-m" main.go执行可以倒如下结果.

通过结果得知\[\]string{...} escapes to heap,s被逃逸到堆上.

语雀地址www.yuque.com/itbosunmian...?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

悠悠的往事.回流的水.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路

相关推荐
EatFan5 分钟前
从单体到模块化:我的 Spring Boot 项目为什么拆成 framework、module、server?
java·spring boot·后端
回家路上绕了弯12 分钟前
Codex 与 ZCode 有什么区别?从开发工作流看 AI 编程工具怎么选
后端
Java内核笔记14 分钟前
Spring Boot 4 可观测性源码剖析:OpenTelemetry 全链路打通日志、指标、追踪
java·后端
学渣超17 分钟前
从一次早高峰数据库告警说起:你真的理解缓存该如何落地应用吗?
redis·后端·架构
中趴菜22 分钟前
接口返回的JSON为什么有反斜杠
后端
Bs_MoneyMagnet31 分钟前
基于springboot+vue的在线音乐管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
小番茄程序猿32 分钟前
Agent 工程化实测:p95 从 836ms 降到 12ms,而真正的收获是发现瓶颈根本不在 Agent 这层
后端
仍然.40 分钟前
SpringCloud---Seata
spring boot·后端·spring cloud
写后端的胖头鱼1 小时前
【高频面试题】分布式锁在项目中的应用
java·分布式·后端·分布式锁·高频面试题