go语言杂谈-----函数返回局部变量问题(“逃逸分析”)

go语言杂谈-----函数返回局部变量问题(“逃逸分析”)_第1张图片

    在说“逃逸分析”之前,先来啰嗦一下go语言中指针的基本用法,熟悉的看官可以直接跳过这一部分。

1. 指针

Go语言支持指针,通过在变量名前加&来获取变量的地址。
(1) 指针的简单使用,示例如下:
    year := 2020
    ptr := &year
    // 打印ptr的类型和值
    fmt.Printf("%T %v\n", ptr, ptr) // *int 0xc00000a0c8
    // 通过指针解引用的形式获取year的值
    fmt.Println(*ptr)  // 2020
    // 直接通过变量名获取year的值
    fmt.Println(year)  // 2020

(2) 结构体指针访问结构体字段仍然使用"."点操作符,Go语言中没有"->"操作符。示例如下:

    package main

    import "fmt"

    type User struct {
        name string
        age  int
    }

    func main() {
        user := User{
            name:"lioney",
            age:18,
        }
        ptr := &user
        fmt.Println(ptr.name, ptr.age)  // lioney 18
    }

(3) Go语言不支持指针的运算

Go语言由于支持垃圾回收,如果支持指针运算,则会给垃圾回收的实现带来很多不便,在C和C++里面指针运算很容易出现问题,因此Go直接在语言层面禁止指针运算。代码如下:
    year := 2020
    ptr := &year
    ptr ++  // invalid operation: ptr++ (non-numeric type *int)

2. Go语言返回函数内的局部变量

(1)返回函数内局部变量的值:

  • 在C/C++语言中,局部变量分配在栈空间,因为函数返回后,系统会自动回收函数里定义的局部变量,所以在返回局部变量的值时,实际是返回局部变量的副本。
  • 在Go语言中返回局部变量的值也是一样的,返回的也是局部变量的副本。代码如下:
    package main

    import "fmt"

    func foo() int {    //int类型函数
        tmp := 1
        fmt.Println(&tmp)  // 0xc00000a0e0
        return tmp      //返回局部变量
    }

    func main() {
        v := foo()
        fmt.Println(&v)  // 0xc00000a0c8(和tmp地址不同)
    }

(2)返回函数内局部变量的地址

  • 在C/C++语言中,操作函数返回后的局部变量的地址,一定会发生空指针异常。要解决这种问题,只需将内存空间分配在堆中即可。
  • 但在Go语言中,函数内部局部变量,无论是动态new出来的变量还是创建的局部变量,它被分配在堆还是栈,是由编译器做“逃逸分析”之后做出的决定。
  • 关于Go语言的“逃逸分析”,可以参考go FAQ里的讲解,大意如下:

    • Go编译器在给函数中的局部变量分配空间时会尽可能地分配在栈空间中,但是如果编译器无法证明函数返回后是否还有该变量的引用存在,则编译器为避免悬挂空指针的错误,就会将该局部变量分配在堆空间中;
    • 如果局部变量占用内存很大,Go编译器会认为将其存储在堆空间中更有意义;
    • Go编译器如果看到了程序中有使用某个变量的地址,则该变量会变成在堆空间上分配内存的候选对象,此时Go编译器会通过分析,判断出该指针的使用会不会超过函数的范围,如果没超过,该变量就可以驻留在栈空间上;如果超过了,就必须将其分配在堆空间中。
对于Go语言中的“逃逸分析”,我们可以看下面的代码:
    package main

    import "fmt"

    func foo() *int {    // 返回int类型指针
        tmp := 2020
        return &tmp      // 返回局部变量tmp的地址
    }

    func main() {
        var ptr *int
        // main函数中引用了foo函数内的局部变量tmp
        // 根据“逃逸分析”,编译器会将其分配在堆空间上
        ptr = foo()
        // foo函数执行结束后tmp不会被释放
        fmt.Println(*p) // 结果为2020,不会报错
    }
  • 但是,无论局部变量分配在堆空间还是栈空间上,我们都不用担心内存泄漏问题,因为Go语言有强大的垃圾回收机制。Go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。
  • 同时,我们也应该知道,变量保存的位置会影响程序的运行效率,在某些场景下,我们需要考虑编译器的“逃逸分析”可能产生的影响。

参考文献

  1. 博客https://blog.csdn.net/li_1013...
  2. 李文塔《Go语言核心编程》第一章相关内容

go语言杂谈-----函数返回局部变量问题(“逃逸分析”)_第2张图片

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!

你可能感兴趣的