一、定义
我们都知道在 Go 语言中,数组的长度是不可变的,那么为了更加灵活的处理数据,Go 提供了一种功能强悍的类型切片(slice),slice 可以理解为 “动态数组”。但是 slice 并不是真正意义上的动态数组,而是一个引用类型。slice 总是指向一个底层 array,slice 的声明也可以像 array 一样,只是不需要长度。slice 的声明和数组类似,如下
1 | 复制代码var iSlice []int |
这里的声明和数组一样,只是少了长度,注意两者的比较
1 | 复制代码//声明一个保存 int 的 slice |
还有一种声明的方法是使用 make() 函数,如下
1 | 复制代码slice1 := make([]int, 5, 10) |
用 make() 函数创建的时候有三个参数,make(type, len[, cap])
,依次是类型、长度、容量。
如图所示,上图表示创建了 slice1 ,长度是 5,默认的值都是 0,容量是 10,这样声明就开辟了一块容量是 10
的连续的一块内存。当然如果我们不指定容量也是可以的,如下
1 | 复制代码slice2 := make([]int, 5) |
这样就会根据实际情况动态分配内存,而不是最开始指定一块固定大小的内存。需要注意的是我们一般使用 make() 函数来创建 slice,因为我们可以指定 slice 的容量,这样在最开始创建的时候就分配好空间,避免数据多次改变导致多次重新改变 cap 分配空间带来不必要的开销。
二、slice 的特性
关于 slice 的一些基本特性,《Go Web 编程》 这本书里已经讲的很详细,有对基本知识不清楚的童鞋可以去补习一下,这里就不一一叙述了。我么来看一个例子,
1 | 复制代码package main |
这个时候我们运行,控制台打印
我们会看到 aSlice 进行 append 操作以后,它的容量增加了一倍,cap 并没有变成我们想象中的 6 ,而是变成了 10
如果我们最开始 slice 的容量是 10,长度是 5 ,那么再加一个元素是不会改变切片的容量的。也就是说,当我们往 slice中增加元素超过原来的容量时,slice 会自增容量,当现有长度 < 1024 时 cap 增长是翻倍的,当超过 1024,cap 的增长是 1.25 倍增长。我们来看一下 slice.go 的源码会发现有这样一个函数,里面说明了 cap 的增长规则
1 | 复制代码func growslice(et *_type, old slice, cap int) slice { |
从上面的源码,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容。其扩容时的大小增长规则是:
- 如果新的 slice 大小是当前大小2倍以上,则大小增长为新大小
- 否则循环以下操作:如果当前slice大小小于1024,按每次 2 倍增长,否则每次按当前大小 1/4 增长,直到增长的大小超过或等于新大小。
- append 的实现只是简单的在内存中将旧 slice 复制给新 slice
来看一个例子,
1 | 复制代码package main |
我们会看到控制台输出
变化过程如下图所示
上面说明,在 slice 的 cap 范围内增加元素, slice 只会发生 len 的变化不会发生 cap 的变化,同样也说明 slice 实际上是指向一个底层的数组,当多个 slice 指向同一个底层数组的时候,其中一个改变,其余的也会跟着改变,这里需要注意一下。我们同样从 slice.go 的源码中 slice 的定义可以看出,
1 | 复制代码type slice struct { |
这里关于底层的东西就不多叙述,有兴趣的可以看看 一缕殇流化隐半边冰霜 冰霜的 深入解析 Go 中 Slice 底层实现 这篇文章,对 slice 的底层实现的讲解。接下来我们把上面的代码改变一下
1 | 复制代码package main |
我们可以看到下面的输出
上面代码可以用下图说明
也就是说,当 append 的数据超过原来的容量以后,就会重新分配一块新的内存,并把原来的数据 copy 过来,并且保留原来的空间,供原来的 slice(aSlice) 使用这样 aSlice 和 bSlice 就各自指向不同的地址,当 aSlice 改变时,bSlice 不会改变。
关于 cap 还有一点需要注意,我们来用一个例子说明
1 | 复制代码package main |
控制台打印
这里我们会发现 Slice_b 对 Slice_a 进行重新切片后,并没有报错,而是还有输出,这是因为 Slice_a 的 cap 是 8 ,并不是我们想象的 3,slice 指向的是一块连续的内存,所以 Slice_a 的容量其实是一直到 Array_a 的最后的。所以这里 Array_b 对 Array_a 进行切片后会得到值,《Go Web 编程》 上这张图形象的解释了对数组的切片结果,这里是需要注意的一个点。
三、关于 copy
我们来看下面代码
1 | 复制代码package main |
也就是说 copy() 函数有两个参数,一个是 to 一个是 from,就是将第二个 copy 到第一个上面,如果第一个长度小于第二个,那么就会 copy 与第一个等长度的值,如 copy( aSlice, bSlice) 的结果是 [4 5 6] ,反之则是短的覆盖长的前几位。当然我们也可以指定复制长度
1 | 复制代码package main |
关于 slice 的 copy 的规则逻辑我们也可以在源码中看出
1 | 复制代码func slicecopy(to, fm slice, width uintptr) int { |
我们看源码接着往下看会发现这样一个方法
1 | 复制代码func slicestringcopy(to []byte, fm string) int { |
我们会发现这个函数的两个参数分别是 []byte 和 string ,这里其实是 Go 实现了一个将 string 复制到 []byte 上的方法,这个方法有什么用,我们来看个例子
1 | 复制代码package main |
控制台输出
在 Go 中字符串是不可以改变的,我们可以用上面的方法来改变字符串,这里可以看到是实现了 string 和 []byte 的互相转换,达到了修改 string 的目的。我们去看看 string.go 的源码会发现,有下面的方法
1 | 复制代码func stringtoslicebyte(buf *tmpBuf, s string) []byte { |
可以看到上面有个 copy(b, s) ,这里就是将 string 复制到 []byte 上,在 slice.go 已经实现过了的。从源码中我们也可以看出每次 b 都是重新分配的,然后将 s 复制 给 b,从我们上面程控制台输出也可以看到每次地址都有变化,所以说 string 和 []byte 的相互转换是有内存开销的,不过对于现在的机器来说,这点开销也不算什么。
最后,这是我学习 Go 的 slice 的一些理解与总结,由于能力有限,如果有理解不到位的地方,可以随时留言与我交流。
参考:
本文转载自: 掘金