本文首發於公眾號:Hunter後端
原文鏈接:Golang基礎筆記三之數組和切片
這一篇筆記介紹 Golang 裏的數組和切片,以下是本篇筆記目錄:
- 數組定義和初始化
- 數組屬性和相關操作
- 切片的創建
- 切片的長度和容量
- 切片的擴容
- 切片操作
1、數組定義與初始化
第一篇筆記的時候介紹過數組的定義與初始化,這裏再介紹一下。
數組是具有固定長度的相同類型元素的序列。
這裏有兩個點需要注意,數組的長度是固定的,數組的元素類型是相同的,且在定義的時候就確定好的。
1. 一維數組
我們可以通過下面幾種方式對數組進行定義和賦值:
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println("arr: ", arr)
也可以在定義的時候直接對其賦值:
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("arr: ", arr)
或者定義的時候不指定數量,自動獲取:
var arr = [...]int{1, 2, 3}
fmt.Println("arr: ", arr)
還可以在定義的時候,指定索引位置的值:
var arr = [...]string{0: "Peter", 3: "Tome", 1: "Hunter"}
fmt.Println("arr: ", arr)
2. 多維數組
多維數組一般是二維數組用的較多,示例如下,表示一個兩行三列的二維數組:
var s [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
s[i][j] = 0
}
}
fmt.Println(s)
// [[0 0 0] [0 0 0]]
2、數組屬性和相關操作
1. 獲取數組長度和容量
獲取數組長度和容量分別使用 len() 和 cap() 函數。
arr := [...]int{2, 3, 4}
fmt.Println("len: ", len(arr))
fmt.Println("cap: ", cap(arr))
對於數組而言,數組的長度是固定的,所以其長度和容量都是一樣的。
對於長度和容量的概念,我們在後面介紹切片的時候,再詳細介紹。
2. 數組的複製
我們可以通過 copy() 函數將一個數組複製到另一個數組,其返回值是複製元素的個數:
arr := [3]int{2, 3, 4}
var arr2 [3]int
numCopied := copy(arr2[:], arr[:])
fmt.Printf("複製的元素個數:%d, arr2:%v\n", numCopied, arr2)
3. 數組的排序
我們可以引入 sort 包,使用 sort.Ints() 函數對數組進行排序:
import "sort"
func main() {
arr := [3]int{5, 2, 4}
sort.Ints(arr[:])
fmt.Println(arr) // 2, 4, 5
}
3、切片的創建
切片是對數組的一個連續片段的引用,它本身不存儲數據,而是指向底層數據。
切片由三部分組成:指針,長度,容量。
指針指向引用的數組的起始位置,長度則是切片中元素的數量,容量則是從切片起始位置到底層數據末尾的元素數量。
下面介紹切片創建的幾種方式。
1. 引用數組創建切片
切片本身的定義就是對數組的引用,所以可以通過引用數組的方式來創建切片:
var arr = [3]int{1, 2, 3}
var slice = arr[1:]
fmt.Println(slice) // [2 3]
在這裏,切片 slice 是從 arr 第二個元素開始引用,因此 slice 的內容是 [2, 3]。
注意,這裏 slice 的切片是引用的 arr 數組,所以他們指向的是同一個內存空間,如果修改切片內的元素,會同步影響數組的元素,而修改數組的元素,也會影響切片內容:
var arr = [3]int{1, 2, 3}
var slice = arr[1:]
fmt.Printf("修改前: arr:%v, slice:%v\n", arr, slice)
// 修改前: arr:[1 2 3], slice:[2 3]
arr[1] = 7
fmt.Printf("修改 arr 後,arr:%v, slice:%v\n", arr, slice)
// 修改 arr 後,arr:[1 7 3], slice:[7 3]
slice[1] = 10
fmt.Printf("修改 slice 後,arr:%v, slice:%v\n", arr, slice)
// 修改 slice 後,arr:[1 7 10], slice:[7 10]
2. 創建數組的方式創建切片
使用創建數組的方式不定義其長度,創建的就是一個切片:
slice := []int{1, 2, 3}
3. make 的方式創建切片
使用 make 的方式創建切片,可以指定切片的長度和容量,其格式如下:
var 切片名 []type = make([]type, len, [cap])
make 函數接受三個參數,第一個就是切片類型,第二個是切片長度,第三個是切片容量,其中切片容量是可選參數,如不填寫則默認等於切片長度。
以下是一個創建切片的示例:
slice := make([]int, 3)
fmt.Printf("slice length:%d, cap:%d\n", len(slice), cap(slice)) // 3 3
4、切片的長度和容量
切片的長度和容量分別使用 len() 和 cap() 函數來獲取。
長度的概念很好理解,就是切片的元素個數就是它的長度。
而對於容量,可以理解是這個切片預留的總長度,而如果切片是從數組中引用而來,其定義是 從切片的第一個元素到引用數組的最後一個元素的長度就是切片的容量。
對於下面這個 arr,其長度是 5,兩個切片分別從第三個和第五個元素開始引用:
arr := [5]int{1,2,3,4,5}
slice1 := arr[2:4]
slice2 := arr[4:]
對於 slice1, 它的長度就是 2,因為它引用的元素個數是兩個
slice2 的長度是 1,它是從第五個元素開始引用,直到數組結尾。
但是兩個切片的容量因為其開始引用的下標的不同而不一致,原數組總長度為 5
slice1 是從下標為 2 開始引用,所以它的容量是 5-2=3
slice2 是從下標為 4 開始引用,它的容量是 5-4=1
fmt.Printf("slice1 length:%d, cap:%d\n", len(slice1), cap(slice1))
// slice1 length:2, cap:3
fmt.Printf("slice2 length:%d, cap:%d\n", len(slice2), cap(slice2))
// slice2 length:1, cap:1
5、切片的擴容
當我們向一個切片添加元素,且其長度超出了定義的容量大小,這個就涉及到切片擴容的概念。
首先,我們可以創建一個切片,然後查看其長度和容量:
slice := make([]int, 2, 2)
slice[0] = 1
slice[1] = 2
fmt.Printf("slice length:%d, cap:%d, %p\n", len(slice), cap(slice), slice)
// slice length:2, cap:2, addr:0xc0000120c0
注意,在上面創建 slice 的時候,它的長度和容量如果是一樣的話,可以默認不填寫 cap 參數,這裏為了表示清楚,所以顯式指定其長度和容量。
當我們向 slice 再添加一個元素,就已經超出了其容量大小了,因此切片會自動進行擴容,其容量會變為原來的兩倍,接着可以看到切片地址已經發生了變化:
slice = append(slice, 3)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
// slice length:3, cap:4, addr:0xc000020160
而如果再往其中添加兩個元素,其容量又會擴大,變為原來的兩倍,變成 8:
slice = append(slice, 4)
slice = append(slice, 5)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
// slice length:5, cap:8, addr:0xc00001c180
切片自動擴容規律
關於 Golang 裏切片的自動擴容規律,之前搜索到這樣一個擴容規律:
- 如果新元素追加後所需要的容量超過原容量的兩倍,新容量會直接設為所需的容量
- 當原切片長度小於 1024 時,新容量會是原來容量的兩倍
- 當原切片的大於等於 1024 時,新容量會是原來容量 1.25 倍
但是這個規律並不完全準確,下面是基於 go1.22.6 版本做的相應的測試:
slice := make([]int, 2)
for _ = range 1028 {
slice = append(slice, 1)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), &slice)
}
對於輸出的結果,後面的 cap 的變化趨勢是 32, 64, 128, 256, 512, 848, 1280。
可以看到,在容量為 512 之前,確實是遵循兩倍擴容的規律,但是 512 之後的擴容規律則不再是兩倍,而且 1024 之後的擴容也不是 1.25 倍。
因此,去查詢相關資料和源代碼,發現切片自動擴容的計算分為兩個階段,第一個階段是擴容容量計算階段,第二個階段是內存對齊階段。
1) 擴容容量計算
第一階段進行擴容容量計算的源代碼如下:
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
return newLen
}
const threshold = 256
if oldCap < threshold {
return doublecap
}
for {
newcap += (newcap + 3*threshold) >> 2
if uint(newcap) >= uint(newLen) {
break
}
}
if newcap <= 0 {
return newLen
}
return newcap
}
其大體邏輯為:
- 當新切片的長度大於原容量的兩倍時,直接將新容量設為所需的容量
- 當原容量小於 256 時,新容量為原容量的兩倍
- 當原容量大於等於 256 時,新容量的計算規則為
新容量 = 原容量 + (原容量 + 3 * 256) / 4, 直到這個新容量大於等於新切片的長度
在這個邏輯裏,當原容量的逐步增大,新容量跟原容量的關係會逐漸向 1.25 倍靠攏,這也是之前搜索到的 1.25 倍這個數字的來源。
2) 內存對齊
而在第二個內存對齊階段,會進一步根據切片的元素的類型,使用一個函數來進行計算,由此適配到具體的需要擴容的容量大小。
比如,我們使用字符串切片來進行測試,返回的擴容容量的大小也是不一樣的:
slice := make([]string, 2)
for _ = range 1028 {
slice = append(slice, "hell")
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), &slice)
}
因此,對於切片的自動擴容規律這個問題,我們可以如下回答:
- 當新切片長度大於原容量的兩倍時,新容量會直接設為所需的容量
- 當原容量小於 256 時,新容量為原容量的兩倍
- 當原容量大於等於 256 時,新容量的計算規則為
新容量 = 原容量 + (原容量 + 3 * 256) / 4, 直到這個新容量大於等於新切片的長度
且在上面的計算規則之後,得出的新容量會進一步根據切片元素類型的大小進行內存對齊,通過 roundupsize() 函數得到最終的容量大小。
6、切片操作
1. 增
可以使用 append() 函數對切片尾部增加元素:
slice := []int{1, 2, 3}
fmt.Println("slice: ", slice)
slice = append(slice, 4)
fmt.Println("slice: ", slice)
如果同時增加多個元素,可以如下操作:
slice = append(slice, 5, 6)
fmt.Println("slice: ", slice)
也可以把一個切片中的全部元素添加到原切片中:
slice2 := []int{7, 8}
slice = append(slice, slice2...)
fmt.Println("slice: ", slice)
但是如果想要把元素插入切片中的指定位置,Golang 沒有提供對應的方法,所以只能通過對切片進行拆分然後拼接的方式:
targetIndex := 2
targetValue := 3
slice := []int{1, 2, 4, 5}
slice = append(slice, -1)
copy(slice[targetIndex+1:], slice[targetIndex:])
slice[targetIndex] = targetValue
fmt.Println("slice: ", slice) // [1 2 3 4 5]
2. 刪
同樣,Golang 也沒有提供直接刪除某個元素,或者根據索引下標刪除元素的方法,但我們還是可以通過拼接的方式來實現。
比如想要刪除切片中下標為 2 的元素,可以先將該索引後的元素都往挪一個下標,然後對切片進行截斷:
targetIndex := 2
slice := []int{1, 2, 3, 3, 4, 5}
copy(slice[targetIndex:], slice[targetIndex+1:])
slice = slice[:len(slice)-1]
fmt.Println("slice: ", slice) // slice: [1 2 3 4 5]
同理,如果想要刪除切片中某個指定值的元素,可以先遍歷一遍切片,然後不等於該值的元素組合成一個新的切片。
3. 改
如果想要更改切片中某個下標的值,可以直接通過下標的方式進行更改:
targetIndex := 2
targetValue := 100
slice := []int{1, 2, 3, 3, 4, 5}
slice[targetIndex] = targetValue
fmt.Println("slice: ", slice) // slice: [1 2 100 3 4 5]
4. 查
如果想要查找切片中是否包含某個元素,也只能通過遍歷的方式來進行查找,或者如果數組是有序的,可以通過二分查找等方式自己實現對應的方法。