• 一、Go 语言中逃逸分析是怎么进行的?
  • 二、Go 语言的 GoRoot 和 GoPath 有什么用?
  • 三、请介绍 Go 语言的编译链接过程?

一、Go 语言中逃逸分析是怎么进行的?

以下观点是凭直觉的观点:

  • 认为函数的参数和返回值如果使用变量的值会对整个变量做拷贝,速度慢。
  • 认为函数的参数和返回值如果使用指针类型,只需要拷贝内存地址,速度更快。

但是这并不一定正确。Go 语言中,变量分配到 stack(栈)还是heap(堆)上对性能都是会有影响的。

  • stack上分配内存效率比heap更高,而且 stack 上分配的内存不用做GC。
  • 放在heap上的内存,需要由GC来做内存回收,而且容易产生内存碎片。
  • 编译器在编译期决定变量分配在 stack 还是 heap 上,需要做逃逸分析,逃逸分析在编译阶段就完成了。

先看第一点,为什么stack上分配内存效率要比heap更高。首先stack中存入的是一个方法的生命周期,包括局部变量和返回值。stack作为一个先进后出的数据结构,在方法生命周期结束时,所有的数据都被弹出,内存得到释放,不需要额外引入 GC 来做垃圾回收。而heap中分配内存空间后需要 GC 回收。

再看第二点,heap 的 GC 垃圾回收机制很容易产生内存碎片。就像JAVA虚拟机中内存回收机制一样。后续会单独出一篇文章写关于GO语言中的GC。

最后看第三点,引出了逃逸分析的概念。什么是逃逸分析:Go 编译器解析源代码,决定哪些变量分配在 stack 内存空间,哪些变量分配在 heap 内存空间的过程就叫做逃逸分析,属于 Go 代码编译的一个分析阶段。

通过逃逸分析,编译器会尽可能把能分配在stack上的对象分配在stack上,避免heap内存频繁GC垃圾回收带来的系统开销,影响程序性能。

案例1:
func getUser() User {
    var user User
    // Do something
    return user
}

变量 user 定义的时候会在该 goroutine 的 stack 上分配 user 的内存空间。当函数返回时,getUser 的调用方如果有接收返回值,那 user 的值会被拷贝给对应的接收变量。getUser 方法的生命周期结束后,stack 上变量 user 的内存控件会被释放(弹栈)。

注:结构体 user 占用的内存空间比较小,未达到 2kb,goroutine 的 stack 空间足够存储,如果 user 占用的空间过大,在 stack 里存储不了,就会分配内存到 heap 上。详情可见https://cloud.tencent.com/developer/article/1700947

案例2:
func getUser() *User {
    var user User
    // Do something
    return &user
}

函数 getUser 因为返回的是一个指针,如果变量 user 分配在 stack 上,那函数返回后,user 的内存空间会被释放,就会导致接收函数返回值的变量无法访问原来 user 的内存空间,成为一个悬浮指针。

所以这种情况就会发生内存逃逸,user 会被编译器分配到 heap 上,而不是 stack 上。

案例3:
func main() {
    p := &User{}
    f(p)
}

指针变量 p 是函数 f 的实参,因为我们是在 main 所在的 goroutine 里调用函数 f,并没有跨 goroutine,所以指针变量 p 分配在 stack 上就可以,不需要分配在 heap 上。

一般而言,遇到以下情况会发生逃逸行为,Go 编译器会将变量存储在 heap 上。

  • 函数内局部变量在函数外部被引用
  • 接口(interface)类型的变量
  • size未知或者动态变化的变量,如slice、map、channel、[]byte
  • size过大的局部变量,因为 stack 内存空间比较小。

此外,还可以借助内存逃逸分析工具

因为内存逃逸分析是编译器在编译器就完成的,可以使用一下编译命令来做内存逃逸分析:

  • go build -gcflags=”-m”,可以展示逃逸分析、内联优化等各种优化结果
  • go build -gcflags=”-m -l”,-l 会禁用内联优化,这样可以过滤掉内联优化的结果展示,让我们可以关注逃逸分析的结果。
  • go build -gcflags=”-m -m”,多一个 -m 会展示更详细的分析结果。

二、Go 语言的 GoRoot 和 GoPath 有什么用?

GoROOT:是 GO 的安装目录、标准库目录,默认位置由 GO 安装程序设置,例如:/usr/local/go (linux)或 C:\Go(Windows)

GoPATH:是用户工作目录的路径,用于存储 Go 项目源代码、依赖包和构建生成的二进制文件。

  • 定义了 Go 项目的工作目录,Go 代码必须放在 GOPATH 下的 src 目录中。
  • 下载的依赖包会被存放在 GOPATH 下的 pkg 目录。
  • 编译后的可执行文件会被放在 GOPATH 下的 bin 目录。
  • Go 1.8 及以上版本中,如果没有设置 GOPATH,Go 会使用默认的工作目录,例如:$HOME/go(Liunx/macOS)或 %USERPROFILE%\go(Windows)
GOPATH/
├── src/   # 源代码目录
│   └── myproject/
│       └── main.go
├── pkg/   # 编译后的包文件
│   └── <os>_<arch>/
└── bin/   # 可执行文件
   └── myproject
扩展知识:

设置 GOROOT 和 GOPATH

查看当前 GOROOT 和 GOPATH

go env GOROOT
go env GOPATH

手动设置 GOPTH

export GOPATH=$HOME/go  # Linux/Mac
set GOPATH=C:\go-workspace  # Windows

同时支持多个 GOPATH(用冒号:分隔)

export GOPATH=$HOME/go:$HOME/projects/go

Go Modules 与 GOPATH 的关系

Go Modules 是 Go 1.11 引入的依赖管理方式,逐渐取代了 GOPATH。启动 Go Modules 后,项目代码不必放在 GOPATH下,可以放在任何位置。GOROOT 依旧保留,用于查找标准库代码。

启用 Go Modules

go mod init myproject

此时,项目的依赖管理与 GOPATH 无关,而是基于 go.mod 文件。

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注