其实Go语言函数中所有的传参传递的都是拷贝。只不过有的copy是值类型的副本,有些copy是指针类型的副本。

Go语言实战

《Go语言实战》这本书有下面这两段描述。

内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。

Go语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型。每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。

我的理解

回顾一下数据类型分类:

  1. 基础类型:boolean、number、string
  2. 聚合类型:array、struct
  3. 引用类型:pointer、slice、map、channel、func
  4. 接口类型:interface

这里1,2是值类型,传参的时候真是把值完完全全的复制一份,形参会在当前函数作用域下申请新的内存空间存放传入的参数值,也就是在函数中你对形参变量的修改,改的是函数体中栈内存的数据,不会影响到调用函数的实参变量。

而3,4类型是引用类型,也就是这些类型的变量都不是具体的值,他们只记录具体值对应的内存地址。传参时候你把引用类型的变量传给函数,函数也会在自己的作用域内部建立临时的变量内存空间,同样copy一份实参的值到这些临时变量中;只不过copy过来的值是指向存放真实数据的内存对应的地址。因此实参和形参存放的值是相同的,都是地址值,这个地址对应的内存空间存放着真实数据。

所以说Go语言中函数传参都是拷贝副本,只是副本是值类型或引用类型的区别。

值类型和引用类型占用内存空间的情况,我们已经在上一篇分享过了。通过对所有类型,以及对不同类型占用内存空间的了解,我想理解传值和传引用(指针)应该毫不费力。

string

这里我想对string重点说明一下。

string是基础数据类型,可以看做是值类型,但是底层技术实现却类似引用类型。很绕是吧,怎样理解string类型呢?看下面的示意图:

image-20201230233506184

看明白了吗?

字符串底层技术实现的确是引用,字符串变量占用16个字节,即一个指针指向首字符起始地址,一个整形存放字节长度。和其它引用类型不一样,不允许修改字符串变量指向的内存(用unsafe包中的方法除外)。

可以这么理解字符串,他就像具备两个字段的struct一样,是一个值类型;好比下面的定义:

1
2
3
4
type struct string {
    addr    uintptr
    length  uint
}

字符串变量不赋值就是空字符串;一旦赋值,他就能表示一段字符串值,但是因为你永远无法改变这些值,改变字符串变量相当于存放新的字符串。你不需要知道值存在哪里;你甚至可以当他不存在,如果这么想,字符串变量当做值类型来处理就好理解了。

slice

Golang中的切片类型极富特色,需要好好理解。下面这篇博文介绍的不错。

下面是几个简单的测试代码,不妨参考一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func PrintArraySlice() {
	fmt.Println("0 ----------")
	var arr []int
	fmt.Println(arr)
	fmt.Println(len(arr), " | ", cap(arr))

	fmt.Println("1 ----------")
	arr = append(arr, 7, 9, 8)
	fmt.Println(arr)
	fmt.Println(len(arr), " | ", cap(arr))

	fmt.Println("2 ----------")
	arr = []int{0, 1, 2, 3, 4, 5, 6, 7}
	fmt.Println(arr)
	fmt.Println(len(arr), " | ", cap(arr))
	fmt.Println(arr[2:])
	fmt.Println(arr[2:8])
	// fmt.Println(arr[2:9]) // raise panic
	fmt.Println(arr[:2])

	fmt.Println("3 ----------")
	arr2 := arr[3:5]
	fmt.Println(arr2)
	fmt.Println(len(arr2), " | ", cap(arr2))

	fmt.Println("4 ----------")
	arr2 = arr[:0]
	fmt.Println(arr2)
	fmt.Println(len(arr2), " - ", cap(arr2))

	fmt.Println("5 ----------")
	arr2 = append(arr2, 9)
	fmt.Println(arr2)
	fmt.Println(arr)
	fmt.Println(len(arr2), " - ", cap(arr2))

	fmt.Println("6 ----------")
	arr2 = append(arr2, 8, 7, 6, 5, 4, 3, 2, 1, 0)
	fmt.Println(arr2)
	fmt.Println(arr)
	fmt.Println(len(arr2), " - ", cap(arr2))
}

输出结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0 ----------
[]
0  |  0
1 ----------     
[7 9 8]
3  |  3
2 ----------     
[0 1 2 3 4 5 6 7]
8  |  8
[2 3 4 5 6 7]    
[2 3 4 5 6 7]    
[0 1]
3 ----------     
[3 4]
2  |  5
4 ----------
[]
0  -  8
5 ----------
[9]
[9 1 2 3 4 5 6 7]
1  -  8
6 ----------
[9 8 7 6 5 4 3 2 1 0]
[9 1 2 3 4 5 6 7]
10  -  16

切片详细介绍,请参考:

https://www.jianshu.com/p/354fce23b4f0

map

Golang中map的实现和C++中的实现有区别,C++采用红黑树的方式实现,Golang采用哈希表加链表法解决快速索引和冲突查找。具体可以看看下面文章的分析。

参考:

https://www.jb51.net/article/201285.htm

https://www.jianshu.com/p/5d169f887865

https://my.oschina.net/renhc/blog/2208417

(完)