2 5 数组和切片

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);

此时,ab的类型也是int*ab此时分配内存的方式类似于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[:]

但是有一种致命的缺点,这是浅拷贝,slice3slice2是同一个切片,无论改动哪个,另一个都会产生变化。

可能这么说你还是不能加深理解。在源码 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]

我们分别修改slice3slice2

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 函数的使用,还介绍了切片的复制和截取。



本图书由小熊©2021 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。