Go 陣列&切片

雲崖先生發表於2020-09-30

陣列相關

   在Go語言中,陣列是一種容器相關的資料型別,用於存放多種相同型別的資料。

陣列定義

   在定義陣列時,必須定義陣列的型別以及長度,陣列一經定義不可進行改變。

   同時,陣列的長度是按照元素個數進行統計的,並且陣列長度是陣列的一部分。

package main

import (
	"fmt"
)

func main() {
	var arr [10]string  // 定義一個長度為10byte的陣列
	fmt.Println(len(arr)) // 10 陣列長度
	fmt.Println(cap(arr)) // 10 陣列容量
}

定義賦值

   陣列可以在一經定義的時候就進行賦值,賦值的長度不可大於陣列的長度。

   陣列也可以不定義長度,而是使用...語法來動態計算填充的元素長度。

   陣列也可以在定義的時候通過索引對其進行賦值。

   以下是定長的陣列定義賦值

package main

import (
	"fmt"
)

func main() {
	var arr = [10]string{"①", "②", "③", "④","⑤","⑥","⑦","⑧","⑨","⑩"}
	fmt.Printf("陣列長度:%d\n", len(arr)) // 陣列長度:10
	fmt.Printf("陣列容量:%d\n", cap(arr)) // 陣列容量:10
	fmt.Printf("第一個元素:%v\n", arr[0])  // 第一個元素:①
}

   以下是不定長的陣列定義賦值

package main

import (
	"fmt"
)

func main() {
	var arr = [...]string{"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "拾壹"}
	fmt.Printf("陣列長度:%d\n", len(arr)) // 陣列長度:11
	fmt.Printf("陣列容量:%d\n", cap(arr)) // 陣列容量:11
	fmt.Printf("最後一個元素:%v\n", arr[len(arr)-1])  // 最後一個元素:拾壹
}

   以下是定長的陣列索引賦值:

package main

import (
	"fmt"
)

func main() {
	var arr = [10]string{1: "②", 8: "⑨"}
	fmt.Printf("陣列長度:%d\n", len(arr)) // 陣列長度:11
	fmt.Printf("陣列容量:%d\n", cap(arr)) // 陣列容量:11
	fmt.Printf("陣列元素:%v\n", arr) // 陣列元素:[ ②       ⑨ ]
}

預設填充

   當定義了一個陣列並未填充資料時,會進行預設的資料填充:

   布林值是false

   字串是""

   整形和浮點型都是0

多維陣列

   多維陣列只有第一層可以使用...來讓編譯器推導陣列長度,其他層都需要手動指定長度。

   如下定義了一個二維陣列:

package main

import (
	"fmt"
)

func main() {
	// 一維陣列:不定長  二維陣列:定長,2個 在一維陣列中建立了3個二維陣列
	var arr = [...][2]string{
		{"1-1", "1-2"},
		{"2-1", "2-2"},
		{"3-1", "3-2"},
	}
	fmt.Println(arr) // [[1-1 1-2] [2-1 2-2] [3-1 3-2]]
}

值型別

   陣列本身是值型別的,所以當重新複製一份陣列時,不會受到前陣列的影響。

   這相當於深拷貝。

   如下示例,單一的陣列中存入值型別,互不影響。

package main

import (
	"fmt"
)

func main() {
	var arr = [...]int{1, 2, 3}
	// 陣列是值型別
	newArr := arr
	arr[0] = 10
	fmt.Println(arr) // [10 2 3]
	fmt.Println(newArr) // [1 2 3]
}

  

   示例二,二維陣列的改變,也不會引發另一個陣列的改變,故此說是深拷貝。

package main

import (
	"fmt"
)

func main() {
	var arr = [...][1]int{
		{10},
	}
	// 陣列是值型別
	newArr := arr
	arr[0][0] = 100
	fmt.Println(arr) // [[100]]
	fmt.Println(newArr) // [[10]]
}

迴圈遍歷

   可以使用for的索引迴圈,也可以使用range來進行陣列遍歷。

package main

import (
	"fmt"
)

func main() {
	var arr = [10]string{"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"}
	// 索引迴圈
	for index := 0; index < len(arr); index++ {
		fmt.Println(index)
	}
	// range迴圈
	for index,ele := range arr{
		fmt.Println(index)
		fmt.Println(ele)
	}
}

切片相關

   切片是基於陣列的一層封裝,非常靈活且支援擴容。

   你可以將它當作一個更加高階的陣列。

   注意:切片是引用型別,不能直接做比較,只能和nil做比較。

   nil則相當於null

切片宣告

   宣告切片型別的基本語法如下:

var 變數名 []切片型別

   以下是一些示例,對於單純的建立切片來說,它沒有像陣列一樣的容量限制,但是具有型別限制:

package main

import (
	"fmt"
)

func main() {
	var a []string
	var b = []int{}
	var c = []bool{true,false}
	fmt.Println(a)  // []
	fmt.Println(b)  // []
	fmt.Println(c)  // [true false]
	fmt.Println(a == nil) // true
}

切片引用

   如果對一個陣列進行切片取值,那麼切片的元素引用於原陣列中的元素。

package main

import (
	"fmt"
)

func main() {
	var arr = [...]string{"①", "②", "③", "④", "⑤"}
	var arrScile = arr[1:3] // 引用陣列的②③
	arr[1] = "二"
	arr[2] = "三" 
	fmt.Println(arrScile) // [二 三] 跟隨發生改變
}

   這裡是常見的一些切片引用的操作,需要注意的是切片引用顧頭不顧尾。

a[2:]  // 等同於 a[2:len(a)]
a[:3]  // 等同於 a[0:3]
a[:]   // 等同於 a[0:len(a)]

長度容量

   引用切片的len()指的是切出來的元素數量。

   而cap()則是被切片的陣列中,從第一個位置切的地方往後算還剩多少個元素。

   長度就是當前切片或者陣列中存在的元素個數

   容量就是最多可以容納的元素個數

package main

import (
	"fmt"
)

func main() {
	var arr = [...]string{"①", "②", "③", "④", "⑤"}
	var arrSlice = arr[1:3] 
	fmt.Println(len(arrSlice)) // 2
	fmt.Println(cap(arrSlice)) // 4
}

   如何理解?這裡有一幅圖,因為有指標的關係。所以才是引用:

   image-20200929181049361

make切片

   上面的切片都是經過陣列建立出來的,切片的容量不能由我們自己進行控制。

   但是使用make()的話就可以動態的建立出一個切片。

make([]型別, size切片中元素數量, cap切片容量)

   如下示例:

package main

import (
	"fmt"
)

func main() {
	// 建立一個string型別的切片 預設存放3個元素 最大可存放5個
	var slice = make([]string,3,5)
	fmt.Println(slice) // [  ]
}

append

   使用內建函式append(),可以為切片新增元素。

   可以新增一個元素,也可以新增多個元素,甚至可以利用解構語法進行切片合併。

   需要注意的是,當使用append()方法後,應該用一個變數來接收它。

   以下是新增多元素的示例

package main

import (
	"fmt"
)

func main() {
	var slice []string
	slice = append(slice, "一", "二")
	fmt.Println(slice) // [一 二]
}

   以下是合併兩個切片的示例

package main

import (
	"fmt"
)

func main() {
	var (
		slice1   []string
		slice2   []string
	)
	slice1 = append(slice1, "一", "二","三")
	slice2 = append(slice2, "四", "五")
	slice1 = append(slice1,slice2...)
	fmt.Println(slice1)  // [一 二 三 四 五]

}

copy

   切片是引用型別,如果想將其作為值型別的傳遞可以用copy

   它有兩個引數,分別是destSlicesrcSlice

   destSlice:目標切片

   srcSlice:資料來源切片

   注意:目標切片必須是make建立的切片,否則將會失敗

package main

import (
	"fmt"
)

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := make([]int, 5, 5)
	// 目標切片  資料來源切片
	copy(slice2, slice1)
	slice1[0] = 100
	fmt.Println(slice1) // [100 2 3 4 5]
	fmt.Println(slice2) // [1 2 3 4 5]
}

刪除元素

   切片中沒有提供任何刪除元素的方法,但是可以利用append()返回新切片的特性,來達到刪除元素的目的。

package main

import (
	"fmt"
)

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	// 刪除3
	slice1 = append(slice1[:2],slice1[3:]...)
	fmt.Print(slice1) // [1 2 4 5]
}

擴容行為

   切片不需要指定容量,它會自動進行擴容。如下示例:

func main() {
	var a []string

	fmt.Println(cap(a)) // 0 初始值
	a = append(a,"1")
	fmt.Println(cap(a)) // 1  
	a = append(a,"2")
	fmt.Println(cap(a)) // 2
	a = append(a,"3")
	fmt.Println(cap(a)) // 4
	a = append(a,"4")
	fmt.Println(cap(a)) // 4
	a = append(a,"5")
	fmt.Println(cap(a)) // 8
	a = append(a,"6")
	fmt.Println(cap(a)) // 8
	a = append(a,"7")
	fmt.Println(cap(a)) // 8
	a = append(a,"8")
	fmt.Println(cap(a)) // 8
}

   可以通過檢視$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
		}
	}
}

   從上面的程式碼可以看出以下內容:

  • 首先判斷,如果新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)。
  • 否則判斷,如果舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap),
  • 否則判斷,如果舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始迴圈增加原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)
  • 如果最終容量(cap)計算值溢位,則最終容量(cap)就是新申請容量(cap)。

   需要注意的是,切片擴容還會根據切片中元素的型別不同而做不同的處理,比如intstring型別的處理方式就不一樣。

   關於上面的示例,其實只看前兩個判斷就夠了。因為舊的容量沒有超過1024

迴圈遍歷

   可以使用for的索引迴圈,也可以使用range來進行切片遍歷。

package main

import (
	"fmt"
)

func main() {
	var arr = []string{"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"}
	// 索引迴圈
	for index := 0; index < len(arr); index++ {
		fmt.Println(index)
	}
	// range迴圈
	for index,ele := range arr{
		fmt.Println(index)
		fmt.Println(ele)
	}
}

相關文章