Go语言的传参和传引用

传参和传引用的问题

不少非官方的文档和教材(包括一些已经出版的图书), 对Go语言的传参和引用的讲解 都有不少问题. 致使众多Go语言新手对Go的函数参数传参有不少误解.golang

而传参和传引用是编程语言的根本问题, 若是这个问题理解错误可能会致使不少问题.编程

传slice不是传引用!

首先, Go语言的函数调用参数所有是传值的, 包括 slice/map/chan 在内全部类型, 没有传引用的说法.c#

具体请看Go语言的规范:数组

After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.闭包

from: http://golang.org/ref/spec#Calls编程语言

什么叫传引用?

好比有如下代码:ide

var a Object
doSomething(a) // 修改a的值
print(a)

若是函数doSomething修改a的值, 而后print打印出来的也是修改后的值, 那么就能够认为doSomething是经过引用的方式使用了参数a.函数

为何传slice不是传引用?

咱们构造如下的代码:lua

func main() {
	a := []int{1,2,3}
	fmt.Println(a)
	modifySlice(a)
	fmt.Println(a)
}

func modifySlice(data []int) {
	data = nil
}

其中modifySlice修改了切片a, 输出结果以下:.net

[1 2 3]
[1 2 3]

说明a在调用modifySlice先后并无任何变化, 所以a必然是传值的!

为何不少人误觉得slice是传引用呢?

多是FAQ说slice是引用类型, 但并非传引用!

下面这个代码多是错误的根源:

func main() {
	a := []int{1,2,3}
	fmt.Println(a)
	modifySliceData(a)
	fmt.Println(a)
}

func modifySliceData(data []int) {
	data[0] = 0
}

输出为:

[1 2 3]
[0 2 3]

函数modifySliceData确实经过参数修改了切片的内容.

可是请注意: 修改经过函数修改参数内容的机制有不少, 其中传参数的地址就能够修改参数的值(实际上是修改参数中指针指向的数据), 并非只有引用一种方式!

传指针和传引用是等价的吗?

好比有如下代码:

func main() {
	a := new(int)
	fmt.Println(a)
	modify(a)
	fmt.Println(a)
}

func modify(a *int) {
	a = nil
}

输出为:

0xc010000000
0xc010000000

能够看出指针a自己并无变化. 传指针或传地址也只能修改指针指向的内存的值, 并不能改变指针自己在值.

所以, 函数参数传传指针也是传值的, 并非传引用!

全部类型的函数参数都是传值的!

包括slice/map/chan等基础类型和自定义的类型都是传值的.

可是由于slicemap/chan底层结构的差别, 又致使了它们传值的影响并不彻底等同.

重点概括以下:

  • GoSpec: the parameters of the call are passed by value!

  • map/slice/chan 都是传值, 不是传引用

  • map/chan 对应指针, 和引用相似

  • slice 是结构体和指针的混合体

  • slice 含 values/count/capacity 等信息, 是按值传递

  • slice 中的 values 是指针, 按值传递

  • 按值传递的 slice 只能修改values指向的数据, 其余都不能修改

  • 以指针或结构体的角度看, 都是值传递!

那Go语言有传引用的说法吗?

Go语言其实也是有传引用的地方的, 可是不是函数的参数, 而是闭包对外部环境是经过引用访问的.

查看如下的代码:

func main() {
	a := new(int)
	fmt.Println(a)
	func() {
		a = nil
	}()
	fmt.Println(a)
}

输出为:

0xc010000000
<nil>

由于闭包是经过引用的方式使用外部环境的a变量, 所以能够直接修改a的值.

好比下面2段代码的输出是大相径庭的, 缘由就是第二个代码是经过闭包引用的方式输出i变量:

for i := 0; i < 5; i++ {
	defer fmt.Printf("%d ", i)
	// Output: 4 3 2 1 0
}

fmt.Printf("\n")
	for i := 0; i < 5; i++ {
	defer func(){ fmt.Printf("%d ", i) } ()
	// Output: 5 5 5 5 5
}

像第二个代码就是于闭包引用致使的反作用, 回避这个反作用的办法是经过参数传值或每次闭包构造不一样的临时变量:

// 方法1: 每次循环构造一个临时变量 i
for i := 0; i < 5; i++ {
	i := i
	defer func(){ fmt.Printf("%d ", i) } ()
	// Output: 4 3 2 1 0
}
// 方法2: 经过函数参数传参
for i := 0; i < 5; i++ {
	defer func(i int){ fmt.Printf("%d ", i) } (i)
	// Output: 4 3 2 1 0
}

什么是引用类型, 和指针有何区别/联系 ?

在Go语言的官方FAQ中描述, maps/slices/channels 是引用类型, 数组是值类型:

Why are maps, slices, and channels references while arrays are values?

There's a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

from: http://golang.org/doc/faq#references

我我的理解, 引用类型和指针在底层实现上是同样的. 可是引用类型在语法上隐藏了显示的指针操做. 引用类型和函数参数的传引用/传值并非一个概念.

咱们知道 maps/slices/channels 在底层虽然隐含了指针, 可是使用中并无须要使用指针的语法. 可是引用内存毕竟是基于指针实现, 所以就必须依赖 make/new 之类的函数才能构造出来. 固然它们都支持字面值语法构造, 可是本质上仍是须要一个构造的过程的.

要用好Go语言的引用类型, 必需要了解一些底层的结构(特别是slice的混合结构).

咱们能够本身给Go语言模拟一个引用类型. 咱们能够将值类型特定的数组类型定义为一个引用类型(同时提供一个构造函数):

type RefIntArray2 *[2]int

func NewRefIntArray2() RefIntArray2 {
	return RefIntArray2(new([2]int))
}

这样咱们就能够将 RefIntArray2 看成引用类型来使用.

func main() {
	refArr2 := NewRefIntArray2()
	fmt.Println(refArr2)
	modifyRefArr2(refArr2)
	fmt.Println(refArr2)
}

func modifyRefArr2(arr RefIntArray2) {
	arr[0] = 1
}

输出为:

&[0 0]
&[1 0]

之因此选择数组做为例子, 是由于Go语言的数组指针能够直接用[]访问的语法糖. 因此, 引用类型通常都是底层指针实现, 只是在上层加上的语法糖而已.

注: 本节根据 @hooluupog@LoongWong 的评论作的补充.

总结

  • 函数参数传值, 闭包传引用!
  • slice 含 values/count/capacity 等信息, 是按值传递
  • 按值传递的 slice 只能修改values指向的数据, 其余都不能修改
  • slice 是结构体和指针的混合体
  • 引用类型和传引用是两个概念

https://chai2010.cn/

相关文章
相关标签/搜索