由淺入深聊聊Golang的slice

前言

今天本來想去外地玩耍,結果睡過頭錯過了動車,只好總結一下slice,希望能與slice之間做一個了斷。

文章由淺入深,遵從能用代碼說話就不bb的原則。

正文

1.基本操作

1.1 聲明

var stringSlice []string
stringSlice := []string{"咖啡色的羊駝"}

var intSlice []int64
intSlice := []int{18}

和數組的區別:就是**[]括號裏頭不加東西**。

初始化的的一些默認值:

func main() {
	var stringSlice []string
	var intSlice []int64
	fmt.Printf("stringSlice ==> 長度:%v \t地址:%p \t零值是否nil:%v \n",len(stringSlice),stringSlice, stringSlice==nil)
	fmt.Printf("intSlice ==> 長度:%v \t地址:%p \t零值是否nil:%v",len(intSlice),intSlice, intSlice==nil)
}

這裏需要注意的是:slice的key必須是數字 && 0開始逐漸增加

1.2 增刪改查

// 增
func add(slice []interface{}, value interface{}) []interface{} {
	return append(slice, value)
}

// 刪
func remove(slice []interface{}, i int) []interface{} {
	return append(slice[:i], slice[i+1:]...)
}

// 改
func update(slice []interface{}, index int, value interface{}) {
	slice[index] = value
}

// 查
func find(slice []interface{}, index int) interface{} {
	return slice[index]
}

這裏需要注意的是:
1.slice的增加需要依賴於append,這裏會涉及到擴容機制(後文會說)
2.刪除的話,只能是通過切割的方式重拼了,由於slice是引用類型,存的是指針,性能上不會有太多影響

1.3 插入 & 遍歷 & 清空

// 插入 
func insert(slice *[]interface{}, index int, value interface{}) {
	rear := append([]interface{}{}, (*slice)[index:]...)
	*slice = append(append((*slice)[:index], value), rear...)
}

// 遍歷
func list(slice []interface{}) {
	for k, v := range slice {
		fmt.Printf("k:%d - v:%d", k,v)
	}
}

// 清空 
func empty(slice *[]interface{}) {
	*slice = append([]interface{}{})
	//    *slice = nil
}

1.3 複製

// 複製
func main() {
	intSlice := []int{1,2,3,4,5,6}
	copySlice1 := make([]int,0,10)

	_ = copy(copySlice1,intSlice)
	fmt.Printf("長度爲0的時候:%v\n",copySlice1)

	copySlice2 := make([]int,6,10)

	_ = copy(copySlice2,intSlice)
	fmt.Printf("長度爲6的時候:%v",copySlice2)
}

這裏需要注意的是:要保證目標切片有足夠的大小,注意是大小,而不是容量

2.slice的深入瞭解

2.1 slice的基礎數據結構 & 圖

slice的基礎數據結構:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
字段 說明
array 指向指針,指向一個底層數組
len slice中元素的個數
cap slice的容量,允許slice中元素增長的最大個數

這裏的array需要單獨說下,這裏是指針類型,也說明了slice是引用類型

在這裏插入圖片描述
在slice底層,指針指向的是另一個數組。

還是有必要看一下源碼中的實現:

// 創建一個slice
func makeslice(et *_type, len, cap int) slice {
    // 檢查目標類型的最長長度,slice的len和cap都必須小於這個值 
    maxElements := maxSliceCap(et.size)
    if len < 0 || uintptr(len) > maxElements {
        panic(errorString("makeslice: len out of range"))
    }

    // !!! len必須<= cap !!!
    if cap < len || uintptr(cap) > maxElements {
        panic(errorString("makeslice: cap out of range"))
    }

    // 申請內存
    p := mallocgc(et.size*uintptr(cap), et, true)

    // 返回一個slice
    return slice{p, len, cap}
}

2.2 slice切割的底層變化

func main() {
	x:=[]int{1,2,3,4}
	y:=x[1:4]
	fmt.Println(y)
}

// 輸出:[2 3 4]

這裏需要注意的是:切割遵從左開右閉的原則,就是[1:4],取得是第二個元素到第四個以下的,不包括第四個

來一波圖解:
在這裏插入圖片描述

2.3 slice的擴容機制 & 實戰例子

slice擴容機制還是比較有意思的,上源碼:

func growslice(et *_type, old slice, cap int) slice {
    ...
    
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    ...
    
    return slice{p, old.len, newcap}
}

白話文講解:如果新cap的大小是當前2倍以上,則增長到新的cap大小;否則,如果當前cap大小<1024,則按照2倍增長,不然就每次就按照當前大小的1/4增長,直到大小>=新的cap大小。

前文很多操作都是基於append的,那麼slice在append的時候,如果發生了擴容,那麼底層的數組會重建,同時拷貝老的數據到新數組裏頭。

舉個例子:

func main() {
	initSlice := []int{1}
	// 進行擴容到2
	initSlice = append(initSlice, 2)
	// 進行擴容到4
	initSlice = append(initSlice, 3)
	x := append(initSlice, 4)
	y := append(initSlice, 5)
	fmt.Println(initSlice, x, y)
}

會輸出:

[1 2 3] [1 2 3 5] [1 2 3 5]

圖解說明:
在這裏插入圖片描述
在這裏插入圖片描述
根據前文介紹的擴容機制,initSlice的擴容軌跡是1-2-4。而slice只是引用類型,所以x和y只是copy了initSlice的指針,他們三個都是指向同一個底層數組,所以最後第四個坑被y給覆蓋了。

再舉一個正好遇到擴容時候的例子:
我們知道擴容時候是會生成新的底層數組,然後拷貝老的數組值。

func main() {
	initSlice := []int{1}
	// 此時擴容1-2,並且全部裝滿
	initSlice = append(initSlice, 2)
	
	// 以下任一append都會引發擴容
	x := append(initSlice, 3)
	y := append(initSlice, 4)
	fmt.Println(initSlice, x, y)
}

輸出:

[1 2] [1 2 3] [1 2 4]

圖解:
在這裏插入圖片描述

由於都遇到了擴容,所以x與y各自另立門戶,新建數組,slice指向的底層數組也不同了所以互不干擾了。

3.注意點

3.1 函數傳參

func main()  {
	initSlice := []int{1,2,3}
	fmt.Printf("剛開始時候:%v\n",initSlice)
	doSomeThing(initSlice)
	fmt.Printf("一番操作後:%v\n",initSlice)
}

func doSomeThing(s []int)  {
	s[0]=88
	s = append(s, 10)
	fmt.Printf("函數返回前:%v\n",s)
}

輸出:

剛開始時候:[1 2 3]
函數返回前:[88 2 3 10]
一番操作後:[88 2 3]

這裏面的變化情況抓住一個點:就是發送擴容時候底層數組是新建的!

然而我想說的是:函數傳slice,由於是引用類型,所以是會改變原值的,這時候需要考慮擴容新底層數組問題。