Go語言之切片(slice)快速入門篇

尹正杰發表於2024-07-25

                                              作者:尹正傑

版權宣告:原創作品,謝絕轉載!否則將追究法律責任。

目錄
  • 一.切片(slice)概述
    • 1.陣列的侷限性
    • 2.切片(slice)概述
    • 3.切片的記憶體分析
  • 二.切片的三種定義方式
    • 1.切片表示式(基於已經存在的陣列來建立切片)
    • 2.透過make指令建立切片
    • 3.宣告切片型別
  • 三.切片的遍歷
    • 1.基於for迴圈遍歷
    • 2.基於for-range迴圈遍歷
  • 四.切片的擴容
    • 1.透過append函式擴容切片
    • 2.切片自動擴容
    • 3.切片的擴容策略[瞭解即可]
  • 五.切片使用注意事項
    • 1.切片使用注意事項
    • 2.切片的複製
    • 3.切片元素刪除
    • 4.切片不能直接比較
    • 5.判斷切片是否為空
  • 六.練習題
    • 1.觀看程式碼手寫執行結果
    • 2.使用sort包對陣列進行排序

一.切片(slice)概述

1.陣列的侷限性

陣列的三個特點:
	- 1.長度固定;
	- 2.連續記憶體空間;
	- 3.同一型別集合;

因為陣列的長度是固定的並且陣列長度屬於型別的一部分,所以陣列有很多的侷限性,比如陣列(array)無法實現擴容和縮容。

2.切片(slice)概述


切片(slice)是Golang中一種特有的資料型別,如上圖所示, 切片的本質就是對底層陣列的封裝,它包含了三個資訊:
	- 1.底層陣列的指標;
	- 2.切片的長度(len);
	- 3.切片的容量(cap);
	
	
切片是一個擁有相同型別元素的可變長度的序列。它是基於陣列型別做的一層封裝。支援自動擴容。切片的三個特點:
	- 1.長度可變;
	- 2.連續記憶體空間;
	- 3.同一型別集合;

切片是陣列一個連續片段的引用,所以切片是一個引用型別,它的內部結構包含地址、長度和容量。切片一般用於快速地操作一塊資料集合。

這個片段可以是整個陣列,或者由起始和終止索引識別符號的一些項的子集,終止索引標識的項不包括在切片內。

	
切片和陣列的區別:
	- 1.切片長度不固定,可以根據需求自動擴容,陣列長度是固定的;
	- 2.陣列的長度和容量相等切不可變,而切片的長度和容量並不一定相等;

3.切片的記憶體分析

package main

import "fmt"

func main() {
	// 定義陣列
	var intArray [5]uint8 = [5]uint8{1, 3, 5, 7, 9}

	// 切片構建在陣列之上,如果基於陣列的索引取切片一定要注意口訣: "前包後不包"。
	var slice []uint8 = intArray[1:4]

	fmt.Printf("intArray陣列: %v, 長度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))

	fmt.Printf("slice切片: %v, 長度: %d, 容量: %d\n", slice, len(slice), cap(slice))

	fmt.Printf("intArray[1]陣列的記憶體地址: %p\n", &intArray[1])

	fmt.Printf("slice[0]切片的記憶體地址: %p\n", &slice[0])
	fmt.Printf("slice[1]切片的記憶體地址: %p\n", &slice[1])

	// 修改切片的資料
	slice[1] = 88

	// 檢視陣列和切片的資料是否修改
	fmt.Printf("intArray陣列: %v, 長度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
	fmt.Printf("slice切片: %v, 長度: %d, 容量: %d\n", slice, len(slice), cap(slice))

}

二.切片的三種定義方式

1.切片表示式(基於已經存在的陣列來建立切片)

package main

import "fmt"

func main() {

	/*
			切片表示式從字串、陣列、指向陣列或切片的指標構造子字串或切片。

			切片表示式有兩種變體:(省略了low則預設為0,省略了high則預設為切片運算元的長度)
		    	(1)一種指定low和high兩個索引界限值的簡單的形式;
					array[low:high]
		    	(2)另一種是除了low和high索引界限值外還指定容量的完整的形式;
					array[low:high:max]

			完整切片表示式需要滿足的條件是0 >= low >= high >= max >= cap(a),其他條件和簡單切片表示式相同;

			當設定了max時,則切片的容量設定為"max-low";
	*/

	a := [5]int{1, 2, 3, 4, 5}

	s1 := a[2:] // 等同於 a[2:len(a)]
	s2 := a[:3] // 等同於 a[0:3]
	s3 := a[:]  // 等同於 a[0:len(a)]

	s4 := a[1:3:5]
	s5 := a[1:3:3]

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4))
	fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5))

}

2.透過make指令建立切片

package main

import "fmt"

func main() {
	/*
		如果需要動態的建立一個切片,我們就需要使用內建的make()函式,格式如下:
			make([]T, size, cap)

		透過make函式常見切片需要傳入三個引數:
			T:
				切片的型別
			size:
				切片的長度
			cap:
				切片的容量,容量並不會影響當前元素的個數。 

		make建立切片本質上就是在底層建立了一個陣列,該陣列對外不可見,所以不可以直接操作這個陣列,要透過切片去間接的訪問各個元素。
	*/
	slice := make([]int, 4, 20)

	// 為切片賦值
	slice[1] = 100
	slice[3] = 200

	fmt.Printf("切片的長度:%d,容量:%d,資料:%v\n", len(slice), cap(slice), slice)

}

3.宣告切片型別

package main

import (
	"fmt"
)

func main() {
	// 宣告切片型別,定義一個切片,直接就指定具體陣列,使用原理類似make
	var (
		// 宣告一個字串切片
		bigdata = []string{"hadoop", "spark", "flink", "kudu", "hbase", "hive"}

		// 宣告一個整型切片並初始化
		scores = []int{99, 88, 77}

		// 宣告一個布林切片並初始化
		svip = []bool{false, true}
	)

	fmt.Printf("bigdata切片的長度:[%d],容量:[%d],資料:%v\n", len(bigdata), cap(bigdata), bigdata)
	fmt.Printf("scores切片的長度:[%d],容量:[%d],資料:%v\n", len(scores), cap(scores), scores)
	fmt.Printf("svip切片的長度:[%d],容量:[%d],資料:%v\n", len(svip), cap(svip), svip)
}

三.切片的遍歷

1.基於for迴圈遍歷

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支援基於索引遍歷
	for i := 0; i < len(s); i++ {
		fmt.Printf("第[%d]個索引儲存的資料是: [%c]\n", i, s[i])
	}
}

2.基於for-range迴圈遍歷

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支援基for-range迴圈
	for index, value := range s {
		fmt.Printf("第[%d]個索引對應的資料為:%c\n", index, value)
	}
}

四.切片的擴容

1.透過append函式擴容切片

package main

import "fmt"

func main() {
	var (
		// 1.定義陣列

		intArr [5]int = [5]int{1, 3, 5, 7, 9}
		// 2.定義切片

		s1 []int = intArr[1:4]
	)

	/*
		切片擴容的底層原理:
			- 1.底層追加元素的時候對陣列進行擴容,老陣列擴容為新陣列;
			- 2.建立一個新陣列,將老陣列中的s1("3","5","7")複製到新陣列中,在新陣列中追加"66","88";
			- 3.s2底層陣列指向的是新陣列;
			- 4.往往我們在使用追加的時候起始想要做的效果給s1追加;
			- 5.底層的新陣列不能直接維護,需要透過切片簡潔維護操作;
	*/
	s2 := append(s1, 66, 88)

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))

	s3 := []int{22, 33, 44}

	// append也支援將一個切片直接追加到另一個切片中
	s1 = append(s1, s3...)

	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))

}

2.切片自動擴容

package main

import (
	"fmt"
)

func main() {

	var numSlice []int
	fmt.Printf("未新增任何元素:%p 資料: %v  長度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))

	for i := 0; i < 10; i++ {
		/*
			溫馨提示:
				- 1.每個切片會指向一個底層陣列,這個陣列的容量夠用就新增新增元素。
				- 2.當底層陣列不能容納新增的元素時,切片就會自動按照一定的策略進行“擴容”,此時該切片指向的底層陣列就會更換。
				- 3.“擴容”操作往往發生在append()函式呼叫時,所以我們通常都需要用原變數接收append函式的返回值。
		*/
		numSlice = append(numSlice, i)

		fmt.Printf("記憶體地址指標:%p 資料: %v  長度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))
	}
}

3.切片的擴容策略[瞭解即可]

可以透過檢視$GOROOT/src/runtime/slice.go原始碼,其中擴容相關程式碼如下:
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }


從上面的程式碼可以看出以下內容:
	- 1.首先判斷,如果新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap),否則走else語句繼續判斷;
	
	- 2.如果舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap),否則走else語句繼續判斷;
	
	- 3.如果舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始迴圈增加原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap);
	
	- 5.如果最終容量(cap)計算值溢位,則最終容量(cap)就是新申請容量(cap);



溫馨提示:
	切片擴容還會根據切片中元素的型別不同而做不同的處理,比如int和string型別的處理方式就不一樣。 

五.切片使用注意事項

1.切片使用注意事項

- 1.切片定義後不可以直接使用,需要讓其引用到一個陣列,或者make一個空間供切片來使用;

- 2.切片使用不能越界;

- 3.切片表示式支援簡寫形式
	案例1: 
		"var s1 = arr[0:end]"簡寫為:"var s1 = arr[:end]"
		
	案例2:
		"var s2 = arr[start:len(arr)]"簡寫為"var s2 = arr[start:]"
		
	案例3:
		"var s3 = arr[0:len(arr)]簡寫為"var s3 = arr[:]"
		
- 4.切片可以繼續切片;

- 5.切片可以動態增長,透過"append"函式操作即可;

- 6.切片也支援使用內建函式copy進行複製;

- 7.切片是引用型別,不支援直接比較,切片唯一合法的比較操作是和nil比較

1.切片的賦值複製

package main

import (
	"fmt"
)

func main() {
	s1 := make([]int, 3)

	// 切片是引用型別,將s1直接複製給s2,此時s1和s2底層共用同一個陣列
	s2 := s1

	fmt.Printf("修改前: s1 --->[%v]\n", s1)
	fmt.Printf("修改前: s2 --->[%v]\n", s2)

	// 注意,我修改的是s2,並沒有取修改s1哈
	s2[1] = 100

	// 再次檢視s1和s2時你會發現二者都發生了變化喲
	fmt.Printf("修改後: s1 --->[%v]\n", s1)
	fmt.Printf("修改後: s2 --->[%v]\n", s2)

}

2.切片的複製

package main

import (
	"fmt"
)

func main() {
	s1 := []int{11, 22, 33, 44, 55}
	s2 := s1

	// 切片是引用型別,所以s1和s2其實都指向了同一塊記憶體地址。修改s2的同時s1的值也會發生變化。
	s2[1] = 100
	fmt.Printf("修改前: s1 ---> 記憶體地址: %p [%v], \n", s1, s1)
	fmt.Printf("修改前: s2 ---> 記憶體地址: %p [%v]\n", s2, s2)

	// 注意,使用make函式對切片進行初始化操作,此操作在"go1.19.3"版本中對s1和s2指向的陣列值也有影響喲~
	s3 := make([]int, 3)

	// Go語言內建的copy()函式可以迅速地將一個切片的資料複製到另外一個切片空間中,copy()函式的使用格式如下:
	// 		copy(destSlice, srcSlice []T)
	//
	// 溫馨提示:
	//		srcSlice:
	//			資料來源切片
	// 		destSlice:
	//			目標切片
	copy(s1, s3) // 使用copy()函式將切片s1中的元素複製到切片s3,屬於值複製。

	// 由於s3屬於只是使用了內建函式copy對s1進行了值複製,因此修改s3並不會影響到s1和s2喲~
	s3[2] = 200
	fmt.Printf("s1 ---> 記憶體地址: %p 資料: %v 長度: %d 容量: %d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 ---> 記憶體地址: %p 資料: %v 長度: %d 容量: %d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 ---> 記憶體地址: %p 資料: %v 長度: %d 容量: %d\n", s3, s3, len(s3), cap(s3))
}

3.切片元素刪除

package main

import (
	"fmt"
)

func main() {

	numberList := []int{11, 22, 33, 44, 55}
	fmt.Printf("刪除前 ---> numberList: %v\n", numberList)

	// Go語言中並沒有刪除切片元素的專用方法,我們可以使用切片本身的特性來刪除元素。
	// 要從切片a中刪除索引為index的元素,操作方法是a = append(a[:index], a[index+1:]...)
	numberList = append(numberList[:3], numberList[4:]...) // 刪除索引為3的元素

	fmt.Printf("刪除後 ---> numberList: %v\n", numberList)
}

4.切片不能直接比較

package main

import (
	"fmt"
)

func main() {
	var a = []bool{false, true}
	var b = []bool{false, true}

	fmt.Printf("a ---> %v\n", a)
	fmt.Printf("b ---> %v\n", b)

	// 切片是引用型別,不支援直接比較,切片唯一合法的比較操作是和nil比較。
	// fmt.Println(a == b) //  報錯: invalid operation: a == b (slice can only be compared to nil)

}

5.判斷切片是否為空

package main

import (
	"fmt"
)

func main() {
	var s1 []int
	s2 := []int{}
	s3 := make([]int, 0)
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))

	// 溫馨提示:
	// 		(1)一個nil值的切片並沒有底層陣列,一個nil值的切片的長度和容量一定都是0;
	// 		(2)我們不能說一個長度和容量都是0的切片一定是nil;
	fmt.Printf("s1是否為空: %t\n", s1 == nil)
	fmt.Printf("s2是否為空: %t\n", s2 == nil)
	fmt.Printf("s3是否為空: %t\n", s3 == nil)

	// 綜上所述,要判斷一個切片是否是空的,要是用len(s) == 0來判斷,不應該使用s == nil來判斷。
	fmt.Println(len(s1) == 0)
	fmt.Println(len(s2) == 0)
	fmt.Println(len(s3) == 0)

}

六.練習題

1.觀看程式碼手寫執行結果

package main

import (
	"fmt"
)

func main() {
	var a = make([]string, 5, 10)
	fmt.Printf("資料: %v 長度: %d 容量: %d\n", a, len(a), cap(a))

	for i := 0; i < 10; i++ {
		a = append(a, fmt.Sprintf("%v", i))
	}

	fmt.Printf("資料: %v 長度: %d 容量: %d\n", a, len(a), cap(a))
}

2.使用sort包對陣列進行排序

package main

import (
	"fmt"
	"sort"
)

func main() {

	var a = [...]int{3, 7, 8, 9, 1}
	fmt.Printf("排序前: %v\n", a)

	sort.Ints(a[:]) // 對切片進行排序,該切片底層對應的就是上面的可變陣列喲~
	fmt.Printf("排序後: %v\n", a)
}

相關文章