2.5 数组和切片 #
本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.5-arrray https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.5-slice
2.5.1 Golang中的数组 #
其实在 循环 那一节用到过数组,我快速介绍一下。
- 数组中是固定长度的连续空间(内存区域)
- 数组中所有元素的类型是一样的
var a1 [10]int
//初始化数组
var b1 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
多维数组
//声明二维数组,只要 任意加中括号,可以声明更多维,相应占用空间指数上指
var arr [3][3]int
//赋值
arr = [3][3]int{
{1, 2, 3},
{2, 3, 4},
{3, 4, 5},
}
2.5.2 何谓切片? #
类比c
语言,一个int
型数组int a[10]
,a
的类型是int*
,也就是整型指针,而c
语言中可以使用malloc()
动态的分配一段内存区域,c++
中可以用new()
函数。例如:
int* a = (int *)malloc(10);
int* b = new int(4);
此时,a
和b
的类型也是int*
,a
和b
此时分配内存的方式类似于go
语言中的切片。
Go
的数组和切片都是从c
语言中延续过来的设计。
2.5.3 有何不同? #
var sliceTmp []int
可以看到和c
不同的是,go
可以声明一个空切片(默认值为nil
),然后再增加值的过程中动态的改变切片值大小。
2.5.4 怎么动态增加? #
增加的方式只有一种,使用append
函数追加。
sliceTmp = append(sliceTmp, 4)
sliceTmp = append(sliceTmp, 5)
每个切片有长度len
和容量cap
两个概念,长度是我们最熟知的,和数组长度相同,可以直接用来遍历。
for _,v := range slice1{
fmt.Println(v)
}
用切糕来对比
每个切片,在声明或扩建时会分配一段连续的空间,称为容量cap
,是不可见的;真正在使用的只有一部分连续的空间,称为长度len
,是可见的。
每次append
时,如果发现cap
已经不足以给len
使用,就会重新分配原cap
两倍的容量,把原切片里已有内容全部迁移过去。
新分配的空间也是连续的,不过不一定直接在原切片内存地址处扩容,也有可能是新的内存地址。
2.5.5 切片的长度与容量,len cap append copy #
slice1 := []int{1, 2, 3}
普通切片的声明方式,长度和容量是一致的。
len=3 cap=3 slice=[1 2 3]
当然,控制权在我们手上,我们可以自己控制长度和容量,
slice1 = make([]int, 3, 5) // 3 是长度 5 是容量
输出
len=3 cap=5 slice=[0 0 0]
尝试使用一般的方式扩容
slice1[len(slice1)] = 4
//报错 panic: runtime error:
//index out of range [3] with length 3
这种方式是会报错的,虽然容量是 5 ,但是数组长度是3,这里是以长度为准,而不是容量,append
内部如果超过容量相当于创建了一个新数组,每个新数组都是定长的,只不过外部是切片。
尝试扩容
slice1 = append(slice1, 4)
输出,可以发现len
扩容了!
len=4 cap=5 slice=[0 0 0 4]
让我们连续扩容,让容量超过5
slice1 = append(slice1, 5)
slice1 = append(slice1, 6) // 到这里长度超过了容量,容量自动翻倍为 5*2
输出
len=6 cap=10 slice=[0 0 0 4 5 6]
上面的过程,我 用自己的代码模拟一遍
// 上面容量自动翻倍的过程可以看作和下面一致
slice1 = make([]int, 3, 5) // 3 是长度 5 是容量
slice1 = append(slice1, 4)
slice1 = append(slice1, 5)
// 长度不变,容量自动翻倍为 5*2
slice2 := make([]int, len(slice1),
(cap(slice1))*2)
// 拷贝 slice1 的内容到 slice2
// 注意是后面的拷贝给前面
copy(slice2, slice1)
slice2 = append(slice2, 6)
你理解容量,长度的概念了吗?
2.5.6 切片的复制 #
切片的复制,回顾一下,我们原来是用copy
函数
slice2 := make([]int, len(slice1), cap(slice1))
/* 拷贝 slice1 的内容到 slice2 */
copy(slice2, slice1) // 注意是后面的拷贝给前面
切片还有一种方式复制方式,比较快速
slice3 := slice2[:]
但是有一种致命的缺点,这是浅拷贝,slice3
和slice2
是同一个切片,无论改动哪个,另一个都会产生变化。
可能这么说你还是不能加深理解。在源码 bytes.buffer 中出现了这一段
func (b *Buffer) Bytes() []byte {
return b.buf[b.off:]
}
我们在读入读出输入流的时候,极易出现这样的问题
下面的例子,使用abc
模拟读入内容,修改返回值内容
buffer := bytes.NewBuffer(make([]byte, 0, 100))
buffer.Write([]byte("abc"))
resBytes := buffer.Bytes()
fmt.Printf("%s \n", resBytes)
resBytes[0] = 'd'
fmt.Printf("%s \n", resBytes)
fmt.Printf("%s \n", buffer.Bytes())
输出,可以看出会影响到原切片内容
abc
dbc
dbc
这种情况在并发使用的时候尤为危险,特别是流式读写的时候容易出现上一次没处理完成,下一次的数据覆盖写入的错乱情况
2.5.7 截取部分元素 #
切片之所以为切片,就是可以把部分元素截取出来
slice2
的值是[0 0 0 4 5 6]
,现在有一个需求,要截取第2个元素出来
slice3 := slice2[0:1]
输出
len=1 cap=10 slice=[0]
我们分别修改slice3
和slice2
slice3[0] = 1
slice2[0] = 2
printSlice(slice2)
printSlice(slice3)
发现输出
len=6 cap=10 slice=[2 0 0 4 5 6]
len=1 cap=10 slice=[2]
说明,截取出现的元素依然是同一块内存(切片是引用类型的)。
所以截取部分元素之后,还是得用copy来复制一遍,如下。
slice2 = []int{0, 0, 0, 1, 2, 3}
slice3 = make([]int, 1, 1)
copy(slice3, slice2[0:1])
2.5.8 工具函数补充 #
排序工具函数
slice2 = []int{0, 3, 0, 1, 2, 0}
sort.Ints(slice2)
fmt.Println(slice2)
输出
[0 0 0 1 2 3]
其他知识参考 排序用户自定义数据集
2.5.9 小结 #
本节介绍了切片与数组的区别,动态增加,容量和长度的概念,以及len cap append copy 函数的使用,还介绍了切片的复制和截取。