关于 Go 中 Map 类型和 Slice 类型的传递

Map 类型

先看例子 m1:

?

1
`func main() {``  ``m := ``make``(map[int]int)``  ``mdMap(m)``  ``fmt``.Println(m)``}` `func mdMap(m map[int]int) {``  ``m[1] = 100``  ``m[2] = 200``}`

结果是

?

1
`map[2:200 1:100]`

我们再修改如下 m2:

?

1
`func main() {``  ``var m map[int]int``  ``mdMap(m)``  ``fmt``.Println(m)``}` `func mdMap(m map[int]int) {``  ``m = ``make``(map[int]int)``  ``m[1] = 100``  ``m[2] = 200``}`

发现结果变成了

?

1
`map[]`

要理解这个问题,需要明确在 Go 中不存在引用传递,所有的参数传递都是值传递。

现在再来分析下,如图:

img

可能有些人会有疑问,为什么途中的 m 像是一个指针呢。查看官方的 Blog 中有写:

Map types are reference types, like pointers or slices, …

这边说 Map 类型是引用类型,像是指针或是 Slice(切片)。所以我们基本上可以把它当作是指针来看待(注意,只是近似,或者说其中含有指针,其内部仍然含有其他信息,这里只是为了便于理解),只不过这个指针有些特殊罢了。

m1 中,当调用 mdMap 方法时重新开辟了内存,将 m 的内容,也就是 map 的地址拷贝入了 m’,所以此时当操作 map 时,m 和 m’ 所指向的内存为同一块,就导致 m 的 map 发生了改变。

而在 m2 中,在调用 mdMap 之前,m 并未分配内存,也就是说并未指向任何的 map 内存区域。从未导致 m’ 的 map 修改不能反馈到 m 上。

Slice 类型

现在看一下 Slice。

s1:

?

1
`func main() {``  ``s := make([]int, 2)``  ``mdSlice(s)``  ``fmt.Println(s)``}` `func mdSlice(s []int) {``  ``s[0] = 1``  ``s[1] = 2``}`

s2:

?

1
`func main() {``  ``var` `s []int``  ``mdSlice(s)``  ``fmt.Println(s)``}` `func mdSlice(s []int) {``  ``s = make([]int, 2)``  ``s[0] = 1``  ``s[1] = 2``}`

不出所料:

s1 结果为

[1 2]

s2 为

[]

因为正如官方所说,Slice 类型与 Map 类型一样,类似于指针,Slice 中仍然含有长度等信息。

修改一下 s1,变成 s3:

?

1
`func main() {``  ``s := make([]int, 2)``  ``mdSlice(s)``  ``fmt.Println(s)``}` `func mdSlice(s []int) {``  ``s = append(s, 1)``  ``s = append(s, 2)``}`

不再修改 slice 原先的两个元素,而加上另外两个,结果为:

[0 0]

发现修改并没有反馈到原先的 slice 上。

这里我们需要把 slice 想象为特殊的指针,其已经保存了所指向内存区域长度,所以 append 之后的内存并不会反映到 main() 中:

img

那如何才能反映到 main() 中呢?没错,使用指向 Slice 的指针。

?

1
`func mdSlice(s *[]int) {``  ``*s = append(*s, 1)``  ``*s = append(*s, 2)``}`

内存如图所示:

img

注意本文中内存区域分配是否连续完全随机,不影响程序,只是为了图解清晰。

Chan 类型

Go 中 make 函数能创建的数据类型就 3 类:Slice, Map, Chan。不比多说,相比读者已经能想象 Chan 类型的内存模型了。的确如此,读者可以自己尝试,这边就不过多赘述了。(可以通通过 == nil 的比较来进行测试)。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

2.2 slice

一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。长度是下标操作的上界,如x[i]中i必须小于长度。容量是分割操作的上界,如x[i:j]中j不能大于容量。

img

数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。 如同分割一个字符串,分割数组也不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量。在例子中,对[]int{2,3,5,7,11}求值操作会创建一个包含五个值的数组,并设置x的属性来描述这个数组。分割表达式x[1:3]并不分配更多的数据:它只是写了一个新的slice结构的属性来引用相同的存储数据。在例子中,长度为2–只有y[0]和y[1]是有效的索引,但是容量为4–y[0:4]是一个有效的分割表达式。

由于slice是不同于指针的多字长结构,分割操作并不需要分配内存,甚至没有通常被保存在堆中的slice头部。这种表示方法使slice操作和在C中传递指针、长度对一样廉价。Go语言最初使用一个指向以上结构的指针来表示slice,但是这样做意味着每个slice操作都会分配一块新的内存对象。即使使用了快速的分配器,还是给垃圾收集器制造了很多没有必要的工作。移除间接引用及分配操作可以让slice足够廉价,以避免传递显式索引。

slice的扩容

其实slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定义:

1
2
3
4
5
6
struct    Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};

在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:

  • 如果新的大小是当前大小2倍以上,则大小增长为新大小
  • 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

make和new

Go有两个数据结构创建函数:new和make。两者的区别在学习Go语言的初期是一个常见的混淆点。基本的区别是new(T)返回一个*T,返回的这个指针可以被隐式地消除引用(图中的黑色箭头)。而make(T, args)返回一个普通的T。通常情况下,T内部有一些隐式的指针(图中的灰色箭头)。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。

img

有一种方法可以统一这两种创建方式,但是可能会与C/C++的传统有显著不同:定义make(*T)来返回一个指向新分配的T的指针,这样一来,new(Point)得写成make(*Point)。但这样做实在是和人们期望的分配函数太不一样了,所以Go没有采用这种设计。

slice与unsafe.Pointer相互转换

有时候可能需要使用一些比较tricky的技巧,比如利用make弄一块内存自己管理,或者用cgo之类的方式得到的内存,转换为Go类型使用。

从slice中得到一块内存地址是很容易的:

1
2
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])

从一个内存指针构造出Go语言的slice结构相对麻烦一些,比如其中一种方式:

1
2
var ptr unsafe.Pointer
s := ((*[1<<10]byte)(ptr))[:200]

先将ptr强制类型转换为另一种指针,一个指向[1<<10]byte数组的指针,这里数组大小其实是假的。然后用slice操作取出这个数组的前200个,于是s就是一个200个元素的slice。

或者这种方式:

1
2
3
4
5
6
7
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))

把slice的底层结构写出来,将addr,len,cap等字段写进去,将这个结构体赋给s。相比上一种写法,这种更好的地方在于cap更加自然,虽然上面写法中实际上1<<10就是cap。

或者使用reflect.SliceHeader的方式来构造slice,比较推荐这种做法:

1
2
3
4
5
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

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

https://halfrost.com/go_slice/

https://tiancaiamao.gitbooks.io/go-internals/content/zh/02.2.html