1. 程式人生 > 其它 >Go語言基礎四:複雜資料型別之陣列和切片

Go語言基礎四:複雜資料型別之陣列和切片

Go語言複雜資料型別

陣列

Go語言提供了陣列型別的資料結構。

陣列是同一資料型別元素的集合。這裡的資料型別可以是整型、字串等任意原始的資料型別。陣列中不允許混合不同型別的元素。(當然,如果是interface{}型別陣列,那麼一個數組元素可以包含任意資料型別。)

陣列的每一個元素都是型別相同、長度固定且已經編號的,所以陣列元素可以通過索引來讀取(或者修改)。索引從0開始,第一個元素的索引為0,第二個元素的索引為1,以此類推。

陣列的宣告

Go語言陣列的宣告需要指定元素型別以及元素的個數。陣列是一定要宣告這個size的。語法格式如下:

var array_name [Size] array_type

以上為一維 陣列的定義方式。例如下面定義了陣列a的長度為10型別為int

var a [10] int

陣列的初始化

//宣告並初始化
var a = [3]int{1,2,3}
//也可以通過 := 在宣告陣列時快速初始化陣列
a := [3]int{1,2,3}

如果陣列的長度不確定,你可以使用...來代替陣列的長度,編譯器會自動根據你的宣告來推斷陣列的長度。但是要記住,使用...代替長度,必須在宣告陣列的同時初始化陣列。例如

var a = [3]int{1,2,3}
//或者
a := [...]int{1,2,3}

如果陣列已經宣告過了,可以根據陣列的索引給上述陣列進行初始化。例如

var a [3]int //int array with length 3
a[0] = 12    // array index starts at 0
a[1] = 13
a[2] = 14

在你沒有給陣列賦值或者只給個別元素賦值時,陣列中所有沒有被賦值的元素都被自動賦值為陣列型別的零值。**上述陣列中,a是一個整型陣列,所以a的每一個元素都賦值為0。

package main

import "fmt"

func array() {
	var a [3]int //int array with length 3
	a[0] = 12    // array index starts at 0

	fmt.Println(a)
}

上述程式碼的輸出結果為 [12 0 0]

額外說明一下,陣列的大小是型別的一部分。因此[5]int[25]int 是不同型別。並且陣列是不能調整大小的。

陣列是值型別

Go語言中,陣列是值型別而不是引用型別。這意味著將陣列賦值給另一個新的變數時,該變數會得到一個原始陣列的副本。如果對變數進行更改,是不會影響到原始陣列。

package main

import "fmt"

func main() {
    a := [...]string{"hello", "world"}
    b := a 
    b[0] = "你好"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

上述程式碼中對新陣列b進行了修改,這個修改不會出現在陣列a中。程式的輸出結果為

a is  [hello world]
b is  [你好 world]

陣列的長度

通過將陣列作為引數傳遞給len()函式,可以得到陣列的長度。

package main

import "fmt"

func main() {
    a := [...]int{5, 89, 21, 78}
    fmt.Println("length of a is",len(a))
}

上述程式的輸出為length of a is 4

陣列的迭代

for迴圈

使用for迴圈可以用來遍歷陣列的元素。

package main
import "fmt"
func array() {
	a := [...]int{5, 89, 21, 78}
	for i := 0; i < len(a); i++ {
	fmt.Printf("陣列的第%d個元素為%d ", i, a[i])
	}
}

上述程式碼通過for迴圈遍歷陣列的元素,從索引0到到len(a)-1(因為索引從0開始麼)。

程式的輸出結果為陣列的第0個元素為5 陣列的第1個元素為89 陣列的第2個元素為21 陣列的第3個元素為78

range迭代陣列

Go語言中提供了一種更好、更簡潔的寫法,就是通過for循換的range來遍歷陣列。

range關鍵字用於for迴圈中迭代陣列、切片、通道、或者集合。在陣列和切片中,它返回元素的索引和對應的值,在集合中返回key-value對的ket值。

package main
import "fmt"
func array() {

	for i, v := range a {//range返回元素對應的索引和值
		fmt.Printf("陣列的第 %d 個元素為 %d ", i, v)
	}
}

如果只希望得到元素的值並忽略索引,可以通過用 _ 空白識別符號,替換原本用來接收索引值的變數,以此來忽略

for _, v := range a { // 忽略了索引
}

切片

Go的陣列長度不可以改變,在某些特定的場景中就不太適用了。對於這種情況Go語言提供了一種由陣列建立的、更加靈活方便且功能強大的包裝(Wapper),也就是切片。與陣列相比切片的長度不是固定的,可以追加元素。

切片本身不擁有任何資料,它們只是對現有陣列的引用。

切片的定義

可以宣告一個未指定大小的陣列來定義切片:

var slice_name []type
//type用來指定切片內元素的資料型別

切片不需要宣告長度。還可以使用make()函式來建立切片:

var slice_name []type = make([]type, len)
//len是切片的長度
slice_name := make([]type,len)
//make()函式定義時,也可以指定切片的容量,其中capacity為可選引數
make([]type, length, capacity)

切片的初始化

直接初始化

直接初始化一個切片,[]表示是切片型別,{1,2,3}初始化值依次是1,2,3,對於下面這個切片,初始長度為3容量為3

slice_name := []int {1,2,3}
引用陣列或切片
//初始化切片slice,是陣列array的引用
slice := array[:]
//將array中從下標startIndex到下標endIndex-1下的元素建立為一個新的切片
slice := array[startIndex:endIndex]
//endIndex為空時,從索引為的startIndex元素到最後一個元素
slice := array[startIndex:]
//startIndex空時,從第一個元素(索引為0)到索引為endIndex-1的元素
slice := array[:endIndex]
//通過切片s初始化切片s1
s1 := s[startIndex:endIndex] 
make()函式

使用make()函式建立切片時,預設情況下切片的元素的值為0。

切片的修改

切片它自己不擁有任何資料,他只是對底層陣列的一種表示,對切片所做的任何修改都會反映在底層的陣列當中。

當多個切片共用相同的底層陣列時,每個切片的修改都將會反映在這個陣列中。

package main
import (
	"fmt"
)
func main() {
    arr := [3]int {1,2,3}
    slice_1 := arr[:]
    slice_2 := arr[:]
    fmt.Println("原始陣列",arr)
    slice_1[1] = 10
    fmt.Println("修改slice_1之後的陣列",arr)
    slice_2[2] = 20
    fmt.Println("修改slice_2之後的陣列",arr)
}

執行結果

原始陣列 [1 2 3]
修改slice_1之後的陣列 [1 10 3]
修改slice_2之後的陣列 [1 10 20]

從輸出中可以清晰的看出,對切片的修改會反映在底層陣列當中;當多個切片共享一個底層陣列時,每個切片所作出的修改都會反映在陣列中。

切片的長度和容量

切片的長度是切片中的元素數。切片的容量是從建立切片的索引開始的底層陣列中的元素數。

可以內建函式len()測量切片的長度,內建函式cap()獲取切片的容量。

package main
import (
	"fmt"
)
func main() {
    arr := [...]string{"A","B","C","D","E","F"}
    s := arr[1:3]
    fmt.Printf("切片的長度為%d,切片的容量為%d",len(s),cap(s))
    s = s[:1]
    fmt.Printf("切片的長度為%d,切片的容量為%d",len(s),cap(s))

}

輸出結果為:

切片的長度為2,切片的容量為5

在上述程式中,s是由從arr的索引1到2建立的一個切片。因此切片s的長度為2

切片s是從arr[1]開始,即索引從1開始建立的。根據切片容量的定義,可以得知,s的容量為從此索引開始的底層陣列arr中的元素數也就是5。

所以切片的長度為2,切片的容量為5。

追加和移除切片元素

追加元素

正如我們已經知道的,陣列的長度是固定的,它的長度是不能增加。切片是動態的,使用append()函式可以將新元素追加到切片上。append() 函式的定義是 func append(s[]T,x ... T)[]T。此函式只能將元素追加到切片末尾。

x ... T在函式的定義中表示該函式接受引數x的個數是可變的。這些型別的函式被稱為可變函式

append()函式的第二引數可以是一個數字,也可以是一組陣列。例

append(slice,1)或append(slice,1,2,3)或append(slice,slice2...)`

正如我們所瞭解的,切片的對陣列的一種抽象、一種包裝,但是陣列本身的長度是固定的,那麼切片是如何具有動態長度的?而且切片的容量是由他所引用的陣列的長度決定的,那麼當新的元素新增到切片當中,使得切片的長度變大以後超過了這個切片的容量,這個切片和它引用的陣列發生了什麼變化呢?

當新的元素新增到切片中時,系統會建立一個新的陣列,將現有的陣列元素複製到這個新的陣列當中,並且返回這個新陣列的新切片引用。並且新的切片容量為舊切片的兩倍。

package main
import(
	"fmt"
)
func main() {
    arr := [...]string{"A","B","C","D","E","F"}
    s1 := arr[:]
    fmt.Println("s1",s1,"長度為",len(s1),"容量為",cap(s1))
    s1 = append(s1,"G")
    fmt.Println("新增一個元素後s1",s1,"長度為",len(s1),"容量為",cap(s1))
	s1[0] = "1"
	fmt.Println(arr,s1)
}

輸出結果

s1 [A B C D E F] 長度為 6 容量為 6
新增一個元素後s1 [A B C D E F G] 長度為 7 容量為 12
[A B C D E F] [1 B C D E F G]

在上述程式當中,s1是由arr宣告的切片,其初始長度為6,初始容量為6。當向其中新增一個新的元素,這時系統會在建立一個新的陣列,並返回一個新的切片給s1,這時s1的長度為7,容量為12。可以看到容量翻了一倍。此時切片s1所表示的陣列已經不是arr,而是系統新建立的陣列。

追加元素到切片頭部

如果想把元素追加到切片的開頭,Go沒有提供原生的函式。可以通過append()函式變相實現,這種方法實際上是合併兩個切片。

nums := []int {1,2,3}
//... 三個點表示將切片元素展開傳遞給函式
nums = append([]int{4},nums...)
fmt.Println(nums)
//輸出
[4,1,2,3]
移除元素

使用切片子集和append()函式變相實現。

nums := []int {1,2,3,4,5}
nums = append(nums[:2],nums[3:]...)
fmt.Println(nums)
//輸出
[1,2,4,5]

切片的拷貝

從上面我們可以瞭解到切片是對一個數組的引用,因此不能向陣列那樣直接賦值給一個新的變數就會產生拷貝。

陣列是值型別,對陣列而言,將一個數組賦值給另一個新的陣列,那麼這裡會產生一次拷貝。直白地說,賦值之後兩個陣列是不一樣的他們指向地地址不同,一個數組發生變化,另一個數組不會發生變化。

切片是一個結構體,是對底層陣列地一個引用。將切片的值賦值給另一個新的切片,這兩個切片都是引用的同一個陣列。對切片地修改會對映引用的底層陣列上,那麼修個一個切片的值,另一個切片的值也會發生變化。

如果希望兩個切片引用的不是同一個陣列,就需要用到copy()函式完成對切片的拷貝。

copy(dst, src []Type)第一個引數為目標切片,第二個引數為源切片。

package main
import(
	"fmt"
)
func main() {
	arr := [...]int{1,2,3}
    s1 := arr[:]
    s2 := s1
    s1_copy := make([]int,3)
    copy(s1_copy,s1)
    s1[0] = 10
    fmt.Println("s1值為:",s1,",s2值為:",s2,",s1_copy值為:",s1_copy)
    
}

輸出結果

s1值為: [10 2 3] ,s2值為: [10 2 3] ,s1_copy值為: [1 2 3]

上述程式碼中s1是對arr引用過的一個切片;s2是由s1賦值而來的切片;s1_copy是由s1拷貝而來的切片。由輸出結果可知,賦值會將兩個切片指向同一個底層陣列,使用copy()函式會將源切片的值複製到新的切片當中。

數傳遞

我們可以認為,切片在內部可以由一個結構體型別表示。這是它的表現形式。

type slice struct {
	Length	int
	Capactiy	int
	ZerothElement *byte
}

切片包含長度、容量和指向陣列第零個元素的指標。當切片傳遞給函式時,即使他通過值傳遞,指標變數也將引用相同的底層陣列。因此,當切片作為引數傳遞給函式時,函式內做的更改,也會在函式外可見。

多維切片

多維切片類似與多維陣列,唯一的不同在於多維切片沒有指明長度。

package main
import(
    "fmt"
)
func main() {
    nums := [][]int{
        {1,2,3},
        {10,20,30},
        {100,200,300}
    }
    for _,v1 := range nums {
        for _,v2 := range v1 {
            fmt.Printf("%d,",v2)
        }
        fmt.Printf("\n")
    }
}

執行結果

1,2,3,
10,20,30,
100,200,300,

tips

  1. 切片是對底層陣列的引用。只要切片還在記憶體中,這個被引用的底層陣列就不能被垃圾回收。如果我們需要對很大的一個數組的一個小片段進行處理,那麼我們可以由這個陣列建立一個切片,並開始處理切片。但是同時這個很大的輸入仍然在記憶體中,一種解決方法就是使用copy()函式來生成一個切片副本,這樣我們就可以使用新的切片,原始的陣列就會被垃圾回收
  2. *切片的長度是當前切片可以訪問元素的數量,而切片的容量則是當前切片拓展後能訪問的元素的個數。當然這些可以訪問的元素都是底層陣列中的元素
  3. 切片是一個結構體,它由一個指向陣列中元素的指標、長度、容量來組成的。因此切片這個結構體型別同時具有引用型別的特徵和值型別的特徵
  4. 使用append()函式為切片新增元素時,如果使切片的長度超過了容量時,會返回一個新的切片,這個新切片的容量不一定為舊切片容量的2倍,具體參考這裡