For-learning-Go-Tutorial

Go语言是谷歌2009发布的第二款开源编程语言。

Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。

因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用!

Golang逃逸分析

通常在编译代码的时候,编译器根据分析,判断将变量分配在栈或堆上。函数定义中,一般将局部变量和参数分配到栈上(stack frame)上。但是,如果编译器不能确定在函数返回(return)时,变量是否被引用(reference),分配到堆上;如果局部变量非常大,也应分配在堆上。

如果对变量取地址(*和&操作),则有可能分配在堆上。此外,还需要进行逃逸分析(escape analytic),判断return后变量是否被引用,不引用分配到栈上,引用分配到堆上。

在golang中逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。 逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。

但是golang 编译器决定变量应该分配到什么地方时会进行逃逸分析,下面我们看段代码:

  1. package main
  2. import ()
  3. func foo() *int {
  4. var x int
  5. return &x
  6. }
  7. func bar() int {
  8. x := new(int)
  9. *x = 1
  10. return *x
  11. }
  12. func main() {}

运行后:

  1. > go run -gcflags '-m -l' escape.go
  2. ./main.go:6: moved to heap: x
  3. ./main.go:7: &x escape to heap
  4. ./main.go:11: bar new(int) does not escape

foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。在官网 (golang.org) FAQ 上有一个关于变量分配的问题如下:

From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.

However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

翻译过来就是:

如何得知变量是分配在栈(stack)上还是堆(heap)上?

准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。 然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。

其实在golang中所有静态内存的其实分配都是在 stack 上进行的,而函数体在执行结束出栈后所有在栈上分配的内存都将得到释放,如果此时直接返回当前作用域变量的指针,这在下层函数的寻址行为就会因为出栈的内存释放而造成空指针异常。这个时候我们就得需要用到malloc在堆上(heap)动态分配内存,自己管理内存的生命周期,自己手动释放才是安全的方式。

然而escape analysis的存在让go完美规避了这些问题,编译器在编译时对代码做了分析,如果发现当前作用域的变量没有超出函数范围,则会自动在stack上分配,如果找不到了,则会在heap上分配。这样其实开发者就不用太关心堆栈的使用边界,在代码层面上完全不需要关心内存的分配,把底层要考虑的问题交给编译器,同时也减小了gc回收的压力。

go在一定程度消除了堆和栈的区别,因为go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。

那么逃逸分析的作用是什么呢?

1.逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。

2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配 ,而没有发生逃逸的则有编译器在栈上分配)。

3.同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

我们在开发的时候其实也可以自己去设置和查看逃逸分析的log,我们可以分析逃逸日志,只要在编译的时候加上-gcflags ‘-m’,但是我们为了不让编译时自动内连函数,一般会加-l参数,最终为-gcflags ‘-m -l’.在main中可以用:

  1. go run -gcflags '-m -l' main.go

那么接着就会有个问题什么时候会逃逸,什么时候不会逃逸呢?

接下来我们看个例子:

  1. package main
  2. type Elegance struct{}
  3. func main() {
  4. var e Elegance
  5. p := &e
  6. _ = *identity(y)
  7. }
  8. func identity(m *Elegance) *Elegance {
  9. return m
  10. }

运行后:

  1. main.go:11: leaking param: m to result ~r1 level=0
  2. main.go:7: main &e does not escape

在这里的m变量是“流式”,因为identity这个函数仅仅输入一个变量,又将这个变量作为返回输出,但identity并没有引用m,所以这个变量没有逃逸,而e没有被引用,且生命周期也在mian里,e没有逃逸,分配在栈上。

通常在go中函数都是运行在栈上的,在栈声明临时变量分配内存,函数运行完毕在回收该段栈空间,并且每个函数的栈空间都是独立的,不能被访问到的。但是在某些情况下,栈上的空间需要在该函数被释放后依旧能访问到,这时候就涉及到内存的逃逸了

  1. type data struct {
  2. name string
  3. }
  4. func patent1()data{
  5. p := data{"keke"}
  6. return p
  7. }
  8. func patent2() *data {
  9. p := data{"jame"}
  10. return &p
  11. }
  12. func main(){
  13. p1 := patent1()
  14. p2 := patent2()
  15. }

这里的patent1和patent2函数都有返回值,唯一不同的地方是patent1返回data结构体,patent2返回data结构体指针。在大多数语言例如C类似patent2的函数是不对的,因为p是一个临时变量,返回过后就会被释放掉,返回毫无意义。但是在golang中,这种语法是允许的,它能正确的把p的地址返回到上层调用函数而不被释放。

这样该函数在运行完毕后肯定是要释放的,内部分配的临时内存也要释放,所以p也应该被释放。而为了让p能被正确返回到上层调用,golang采取了一种内存策略,把p从栈拿到堆的中去,此时p就不会跟随patent2一同消亡了,这个过程就是逃逸。