首頁 > 軟體

Go陣列與切片輕鬆掌握

2022-11-21 14:01:46

在 Go 中,陣列和切片的功能其實是類似的,都是用來儲存一種型別元素的集合。陣列是固定長度的,而切片的長度是可以調整的

陣列(array)

我們在宣告一個陣列的時候據必須要定義它的長度,並且不能修改。

陣列的長度是其型別的一部分:比如,[2]int 和 [4]int 是兩個不同的陣列型別。

初始化陣列

// 1. 建立一維陣列
// 元素都是預設值
var arr1 [3]int
// 指定長度並設定初始值
var arr2 = [3]int{1, 2, 3}
var arr3 [3]int = [3]int{1, 2, 3}
// 自動推導陣列長度
var arr4 = [...]int{1, 2, 3}
// 指定特定下標的元素的值,其他的為預設值
var arr5 = [3]int{1: 9}
// 2. 建立多維陣列 與一維陣列類似,不再贅述
var arr6 = [3][2]int{{1, 2}, {3, 4}, {5, 6}}
fmt.Println(arr1)
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Println(arr5)
fmt.Println(arr6)

------結果----------------------------
[0 0 0]
[1 2 3]
[1 2 3]
[1 2 3]
[0 9 0]
[[1 2] [3 4] [5 6]]

陣列賦值

var arr = [3]int{1, 2, 3}
fmt.Println(arr)
arr[2] = 9
fmt.Println(arr)

------結果----------------------------
[1 2 3]
[1 2 9]

遍歷陣列

方法一:for 迴圈遍歷

var arr = [3]int{1, 2, 3}
for i := 0; i < len(arr); i++ {
	fmt.Println(arr[i])
}

------結果----------------------------
1
2
3

方法二:for range 迴圈遍歷

使用 index 和 value 分別接收每次迴圈到的位置的下標和值

var arr = [3]int{1, 2, 3}
for index, value := range arr {
	fmt.Printf("index:%d value:%dn", index, value)
}

------結果----------------------------
index:0 value:1
index:1 value:2
index:2 value:3

陣列對比

陣列比較的方法比較簡單,使用 == 符號即可

var arr = [3]int{1, 2, 3}
var arr2 = [3]int{1, 2, 3}
fmt.Println(arr == arr2)
var arr3 = [...]int{1, 2, 3}
fmt.Println(arr == arr3)
var arr4 = [...]int{1, 2, 4}
fmt.Println(arr == arr4)

------結果----------------------------
true
true
false

不能比較長度不同的陣列型別,否則編譯器會報錯,如下:

var arr = [3]int{1, 2, 3}
var arr5 = [...]int{1, 2}
fmt.Println(arr == arr5)

切片(slice)

切片的性質

切片型別的定義

type slice struct {
    array unsafe.Pointer //指向陣列的指標
    len int //切片的長度,可以理解為切片表示的元素的個數
    cap int //容量,指標所指向的陣列長度(從指標位置向後)
}

切片的特性

  • 切片是一個參照型別,是對陣列的一個連續片段的參照
  • 切片本身是一個結構體,通過值拷貝傳遞
  • 切片的 cap 一定是大於等於 len 的

切片初始化

//直接宣告並賦值
s0 := []int{1, 2, 3, 4, 5}
//通過陣列或者切片獲取
arr := [...]int{1, 2, 3, 4, 5}
s1 := s0[:] // 切片 s0 中的全部元素
s2 := s0[:2] // 切片 s0 第一個元素到第二個元素
s3 := arr[3:] // 陣列 arr 從第四個元素開始向後的所有元素
s4 := arr[0:0] // 建立一個空切片
//通過 make(t Type, size ...IntegerType) 初始化,
//接受的第一個 int 表示切片長度,第二個表示容量大小。如果只有一個int引數則預設長度和容量是相同的
s5 := make([]int, 5) //建立一個長度為 5 切片,
s6 := make([]int, 5, 8) //建立一個長度為 5 容量為 8 的int型切片(長度為5的部分會被初始化為預設值)
fmt.Println(s0, s1, s2, s3, s4, s5 ,s6)

-------結果-----------------------------------
[1 2 3 4 5] [1 2 3 4 5] [1 2] [4 5] [] [0 0 0 0 0] [0 0 0 0 0]

切片賦值

和陣列相同根據 index 賦值

//直接宣告並賦值
s0 := []int{1, 2, 3, 4, 5}
fmt.Println(s0)
s0[0] = 999
fmt.Println(s0)

-------結果-----------------------------------
[1 2 3 4 5]
[999 2 3 4 5]

切片的容量

我們可以通過 len(slice) 獲取一個切片的長度,可以通過 cap(slice) 獲取一個切片的容量。

容量:指標所指向的陣列長度(從指標位置向後),如何理解 從指標位置向後 這個意思,通過程式碼觀察:

s0 := []int{1, 2, 3, 4, 5}
s1 := s0[1:3] //第二個元素到第三個元素
fmt.Printf("len: %dn", len(s1))
fmt.Printf("cap: %dn", cap(s1))
fmt.Println(s1 )

------結果---------------
len: 2
cap: 4
[2 3]

如上,s1 實際指向的陣列是 s0 的陣列的一個連續片段。

所有我們可以使用 cap 把切片 s1 指向的陣列(指標向後,包含指標)的去拿不元素都獲取到:

s0 := []int{1, 2, 3, 4, 5}
s1 := s0[1:3]
s2 := s1[:cap(s1)]
fmt.Printf("len: %dn", len(s2))
fmt.Printf("cap: %dn", cap(s2))
fmt.Println(s2)

-------結果------------
len: 4
cap: 4
[2 3 4 5]

append以及擴容

append 可以動態地向切片中追加元素

s0 := []int{1, 2, 3, 4, 5}
s0 = append(s0, 6, 7, 8, 9, 10) //追加元素
fmt.Printf("len: %dn", len(s0))
fmt.Printf("cap: %dn", cap(s0))
fmt.Println(s0)
s1 := []int{11, 12, 13, 14, 15}
s0 = append(s0, s1...) //追加切片,切片需要解包
fmt.Printf("len: %dn", len(s0))
fmt.Printf("cap: %dn", cap(s0))
fmt.Println(s0)

len: 10

cap: 10

[1 2 3 4 5 6 7 8 9 10]

len: 15

cap: 20

[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]

我們可以發現,在第二次和第三次追加元素的時候,切片的容量發生了變化,兩次都是擴充為之前容量的兩倍。

但是一定都是兩倍擴容嗎?事實上不是的,如以下程式碼:

s0 := make([]int, 1000)
fmt.Printf("len: %d, cap: %dn", len(s0), cap(s0))
s0 = append(s0, make([]int, 200)...)
fmt.Printf("len: %d, cap: %dn", len(s0), cap(s0))
s0 = append(s0, make([]int, 400)...)
fmt.Printf("len: %d, cap: %dn", len(s0), cap(s0))

-----結果--------------------------
len: 1000, cap: 1000
len: 1200, cap: 1536
len: 1600, cap: 2304

可以發現第一次擴容後,容量變為 1536,第二次擴容後容量又變成了 2304,並不是什麼兩倍的關係。

通過檢視 append 原始碼中的容量計算部分

func growslice(et *_type, old slice, cap int) slice {
	...
newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap //小容量直接擴容到兩倍容量
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				//大容量取消了 1.25 倍擴容,選擇了一個更為平滑的擴容方案
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	switch {
	case et.size == 1:
		lenmem = uintptr(old.len)
		newlenmem = uintptr(cap)
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.size == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
		} else {
			shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
		}
		lenmem = uintptr(old.len) << shift
		newlenmem = uintptr(cap) << shift
		capmem = roundupsize(uintptr(newcap) << shift)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
	default:
		lenmem = uintptr(old.len) * et.size
		newlenmem = uintptr(cap) * et.size
		capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
		capmem = roundupsize(capmem)
		newcap = int(capmem / et.size)
	}
	...
    return slice{p, old.len, newcap}
}

從原始碼中可以得知:

  • 當需要的容量大於兩倍舊切片的容量時,需要的容量
  • 就是新容量當需要的容量小於兩倍舊切片的容量時, 判斷是否舊切片的長度, 如果小於 256 , 那麼新的容量就是兩倍舊的容量,當大於等於 256 時, 會選擇一個過度演演算法 newcap += (newcap + 3*256) / 4 不斷增加,直至大於等於需要的容量
  • 特殊的一點是,後面的 capmem = roundupsize(uintptr(newcap) * et.size) 這個方法,做了記憶體對齊,導致最後算出的容量大於等於推算出來的容量,至於記憶體對齊都做了哪些操作,還有待研究。

到此這篇關於Golang陣列與切片輕鬆掌握的文章就介紹到這了,更多相關Golang陣列和切片內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com