图解Go语言逃逸

0 阅读4分钟

示例:

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

示例:

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的变量在栈还是堆:

示例:

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:

func main() {
    data := []interface{}{100, 200}
    data[0] = 100
}

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

通过结果得知data[0]=100发生了逃逸.

逃逸范例2:

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

func main() {
    data := make(map[string]interface{})
    data["key"] = 200
}

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

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

逃逸范例3:

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

func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200
}

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

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

逃逸范例4:

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

func main() {
    data := make(map[string][]string)
    data["key"] = []string{"value"}
}

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

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

逃逸范例5:

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

func main() {
    a := 10

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

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

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

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

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"}赋值时会使传递的参数出现逃逸现象.

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:

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 欢迎大家访问.提意见.

悠悠的往事.回流的水.

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

念何架构之路