天天看點

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]
}