Golang 的值传递

先导文章:(正经版)面试官:切片作为函数参数是传值还是传引用?

Go语言中的 new 和 make 主要区别如下:

  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;

传入参数和传出参数

  • 传入参数为本身有值,传入函数让函数使用;传出参数本身没值,从函数中带出值(相当于函数的返回值)。

函数参数为指针

将指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值。

传值

当我们验证了 Go 语言中大多数常见的数据结构之后,其实能够推测出 Go 语言在传递参数时使用了传值的方式,接收方收到参数时会对这些参数进行复制;了解到这一点之后,在传递数组或者内存占用非常大的结构体时,我们应该尽量使用指针作为参数类型来避免发生数据拷贝进而影响性能

函数小结

  1. 通过堆栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右;
  2. 函数返回值通过堆栈传递并由调用者预先分配内存空间;
  3. 调用函数时都是传值,接收方会对入参进行复制再计算;

golang 的内存分配之堆和栈

  • 栈 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。

下面来看看一个例子:

1
2
3
4
func **F**() {
	temp :**=** make([]**int**, 0, 20)
	...
}

上面的例子,内函数内部申请的临时变量,即使你是用make申请到的内存,如果发现在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配比堆上快很多

下面在看看一个堆的例子:

1
2
3
4
func **F**() []**int**{
	a :**=** make([]**int**, 0, 20)
	**return** a
}

而上面这段代码,申请的代码和上面的一模一样,但是申请后作为返回值返回了,编译器会认为在退出函数之后还有其他地方在引用,当函数返回之后并不会将其内存归还。那么就申请到堆里。

如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销。

堆和栈相比

  • 堆适合不可预知的大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
  • 栈内存分配则会非常快,栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块。之后要通过垃圾回收才能释放。

逃逸分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
   resPtr := test3()
   fmt.Println("rePtr:", *resPtr)
}

func test3() *string {
   // 没有被返回,没有逃逸
   name := "lynn"
   p0 := &name
   fmt.Println("p0", *p0)

   // 地址返回 内存逃逸
   city := "上海"
   ptr := &city
   fmt.Println("地址为:", ptr)

   return ptr
}
  • 查看gcflags 追踪内存分配
1
2
3
go build --gcflags "-m -m -l" 10.内存逃逸.go > 1.txt 2>&1

grep -E "name|city" 1.txt --color

扩展阅读:Golang 内存分配之逃逸分析