天天看点

GO操作切片数组时不当,数据被覆盖

场景

在一个数组对象的 index 索引处插入一个值

原有代码

func (list *ArrayList) Insert(index int, newval interface{}) error {
	if index < 0 || index >= list.TheSize {
		return errors.New("index out of range")
	}

	// 1. 用一个临时数组保存该索引后的所有值,但是这里用了切片,所以底层的数据是同一个内存空间的数据
	tmpList := list.dataStore[index:]
	// 2. 将 index 前的数据当做一个单独数组,直接添加新值,实际在同一个内存空间进行操作
	// 并且将 index 处的值更新为 val,tmpList中对应内存地址的值也发生了改变
	newList := append(list.dataStore[:index], newval)
	// 3. 将两个数组拼接起来,实际上数据在上一步中已经发生了错误
	newList = append(newList, tmpList...)
	list.dataStore = newList
	list.TheSize++

	return nil
}
           

所以,若是对GO数组或者切片中的数据进行下标赋值类的操作时,需要格外注意内存地址相关问题

切片相关

说起切片,我想先从切片的长度和容量说起,切片在初始化中,可以指定长度和容量,这个长度和容量到底代表的是什么呢?

切片的底层实际上是对一个数组片段的描述,它包含了一个指向数组片段的指针,片段的长度,以及该片段的最大值

GO操作切片数组时不当,数据被覆盖

我们可以从以下一个例子初步进行理解

func main() {
	a := []int{0, 1, 2}
	printSlice("a", a)

	b := a[:1]
	printSlice("b", b)

	c := b[:2]
	printSlice("c", c)

	d := c[1:]
	printSlice("d", d)
}

func printSlice(name string, a []int) {
	fmt.Println("slice", name, ", len:", len(a), "cap:", cap(a), "ptr:", &a[0])
}
           

运行结果

slice a , len: 3 cap: 3 ptr: 0xc000010360
slice b , len: 1 cap: 3 ptr: 0xc000010360
slice c , len: 2 cap: 3 ptr: 0xc000010360
slice d , len: 1 cap: 2 ptr: 0xc000010368
           

运行过程:

  1. 第一次输出,切片a,长度3,容量3
    GO操作切片数组时不当,数据被覆盖
  2. 切片b,在切片a的基础上,左指针指向 a[0],右指针指向 a[1],但是左闭右开,b 切片只包含元素 a[0],长度为1,底层的数组片段未发生变化,且开始位置未变化,所以容量为3
    GO操作切片数组时不当,数据被覆盖
  3. 切片c,在切片b的基础上,左指针指向 a[0],右指针指向 a[2],左闭右开,c切片包含两个元素,长度为2,同理底层的数组片段未发生变化,且开始位置未变化,容量为3
    GO操作切片数组时不当,数据被覆盖
  4. 切片d,在切片c的基础上,左指针指向 a[1],右指针指向 a[2],左闭右开,c切片包含1个元素,长度为1,同理底层的数组片段未发生变化,但是开始位置右移一位,容量为2
    GO操作切片数组时不当,数据被覆盖
    值得注意的是:从地址值看出,上述运行结果中该切片没有进行容量扩容,四个切片使用的是同一段内存地址,四个切片在使用下标进行数据修改时,会同时修改4个切片的数据
b[0] = 'b'
c[1] = 'c'

fmt.Println(a, b, c, d)
           

运行结果,可以对照上面的图片进行比较

[98 99 2] [98] [98 99] [99]
           

关于扩容

当切片的长度等于切片的容量,并需要继续增加元素时,切片的容量会进行扩容。

切片的容量一般来说会按照原切片容量的两倍进行扩容,当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。

同时切片会重新开辟一个切片内存地址,并存储原切片的数据

比如:

func main() {
	a := make([]int, 1) // 容量为1
	printSlice(a)
	a = append(a, 1, 2, 3) // 添加三个元素后,容量会直接变为4
	printSlice(a)
}

func printSlice(a []int) {
	fmt.Println("len:", len(a), "cap:", cap(a), "ptr:", &a[0])
}
           

运行结果

len: 1 cap: 1 ptr: 0xc00000a0a0
len: 4 cap: 4 ptr: 0xc000010380
           

容量扩容相关例子:

func main() {
	a := make([]int, 5)
	b := a[:4]
	// a, b 是同一个内存地址上的数据,所以,修改 a[0] 的数据,切片 b 相同内存地址的值也会发生变化
	a[0] = 1
	fmt.Println("a:", a, "b:", b)
	// 运行结果:a: [1 0 0 0 0] b: [1 0 0 0]

	// 当 a append了一个数据之后,发生了扩容,切片 a 重新开辟了一个容量为 10 的内存空间,
	// 并将原切片数据复制了过去,此时再改变 a[0],切片 b 不再改变
	a = append(a, 0)
	a[0] = 2
	fmt.Println("a:", a, "b:", b)
	// 运行结果:a: [2 0 0 0 0 0] b: [1 0 0 0]
}