1. 程式人生 > >Go起步:4、複合型別1--陣列array和切片slice

Go起步:4、複合型別1--陣列array和切片slice

之前講到了Go的基礎資料型別,除此之外,Go還支援很多複合型別的資料結構。

陣列(array)

陣列就是指一系列同一型別資料 的集合。
Go語言中,型別 [n]T 表示擁有 n 個 T 型別的值的陣列。如:

var a [3]int

表示變數 a 宣告為擁有有 3個整數的陣列。宣告語法上與java的區別是[]是寫在型別前面的。
當然,也可以讓編譯器統計陣列字面值中元素的數目:

a := [...]int{1, 23}

這兩種寫法, a 都是對應長度為3的int陣列。
陣列的長度是其型別的一部分,因此陣列不能改變大小。 可以用內建函式len()取得陣列長度。

package main

import "fmt"
func main() { var a [2]string //定義一個長度為2的字串陣列 a[0] = "Hello" //下標1賦值為Hello a[1] = "World" fmt.Println(a[0], a[1]) //按下標取值 fmt.Println(a) //列印陣列 primes := [...]int{2, 3, 5, 7, 11, 13} //定義一個長度為6的int陣列,並初始化 for i := 0; i < len(primes); i++ { fmt.Println(primes[i]) } }

這裡寫圖片描述
從上面可以看出,陣列訪問和賦值可以用下標的方式,下標從0開始,這點和其他大部分程式語言一致。
Go的陣列也支援多維陣列。定義方式如下:

var arrayName [ x ][ y ] variable_type
package main

import "fmt"

func main() {
    a := [3][4]int{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}
    fmt.Println(a)
    for i := 0; i < 3; i++ {
        for j := 0; j < 4; j++ {
            fmt.Printf("a[%d
][%d] = %d\n"
, i, j, a[i][j]) } } }

這裡寫圖片描述
上面展示了二維陣列的定義初始化和取值。
特別需要說明的一點是,初始化時的最後兩個引號不能分行寫,否則編譯會不過,Go編譯器不知為何做這種限制。如下寫法是錯誤的。

a := [3][4]int{{0, 1, 2, 3}, {4, 5, 6, 7}, 
    {8, 9, 10, 11}
}

切片(slice)

前面說過,陣列的長度是不可變的,這在操作上帶來了很大不便,但是Go給出了很好的解決方案,就是切片(slice)。
Go的切片是對陣列的抽象。Go陣列的長度不可改變,在特定場景中這樣的集合就不太適用,Go中提供了一種靈活,功能強悍的內建型別切片(“動態陣列”),與陣列相比切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。

定義

可以通過宣告一個未指定大小的陣列來定義切片,型別 []T 表示一個元素型別為 T 的切片。從這個角度來說,切片可以視為動態大小的陣列。
但是,切片並不儲存任何資料, 它只是描述了底層陣列中的一段。更改切片的元素會修改其底層陣列中對應的元素。與它共享底層陣列的切片都會觀測到這些修改

var s []type

除此之外,可以使用make()函式來建立切片:

var slice1 []type = make([]type, length ,capacity)

其中type是切片的型別,length是切片的初始化長度,capacity是可選引數,指切片容量。
make 函式會分配一個元素為零值的陣列並返回一個引用了它的切片。

a := make([]int, 5)     // len(a)=5, cap(a)=5
b := make([]int, 0, 5)  // len(b)=0, cap(b)=5

len()函式可以返回切片的長度,cap()函式返回切片的容量。

初始化

切片初始化是很靈活的,方法也有很多種。
1、直接初始化切片,[]表示是切片型別,{1,2,3}初始化值依次是1,2,3.其cap=len=3

s :=[] int {1,2,3 } 

2、初始化切片s,是陣列arr的引用

s := arr[:] 

3、將arr中從下標startIndex到endIndex-1 下的元素建立為一個新的切片,arr可以是陣列也可以是一個切片,這是定義的切片就是切片的切片。

s := arr[startIndex:endIndex] 

4、預設endIndex時將表示一直到arr的最後一個元素

s := arr[startIndex:] 

5、預設startIndex時將表示從arr的第一個元素開始

s := arr[:endIndex] 

6、通過內建函式make()初始化切片s,[]int 標識為其元素型別為int的切片

s :=make([]int,len,cap) 
package main

import "fmt"

func main() {
    //1、直接初始化切片
    var s1 = []int{1, 2, 3, 4, 5}
    s11 := []int{1, 2, 3, 4, 5}
    //2、初始化切片s,是陣列arr的引用
    var arr = []int{1, 2, 3, 4, 5}
    s2 := arr[:]
    //3、從下標startIndex到endIndex-1 下的元素建立為一個新的切片
    s3 := arr[1:3]
    s31 := s1[1:3]
    //4、預設endIndex時將表示一直到arr的最後一個元素
    s4 := arr[3:]
    s41 := s1[3:]
    //5、預設startIndex時將表示從arr的第一個元素開始
    s5 := arr[:4]
    s51 := s1[:4]
    //6、通過內建函式make()初始化切片s,[]int 標識為其元素型別為int的切片
    s6 := make([]string, 4, 50)
    s6[0] = "a"
    s6[1] = "b"
    s6[2] = "c"
    s6[3] = "d"

    s61 := make([]string, 4)

    fmt.Println("s1:", s1)
    fmt.Println("s11:", s11)
    fmt.Println("s2:", s2)
    fmt.Println("s3:", s3)
    fmt.Println("s31:", s31)
    fmt.Println("s4:", s4)
    fmt.Println("s41:", s41)
    fmt.Println("s5:", s5)
    fmt.Println("s51:", s51)
    fmt.Println("s6:", s6)
    fmt.Println("len(s6):", len(s6))
    fmt.Println("cap(s6)", cap(s6))
    fmt.Println("s61:", s61)
    fmt.Println("len(s61):", len(s61))
    fmt.Println("cap(s61)", cap(s61))
}

輸出為:
這裡寫圖片描述

空(nil)切片

上面是對於切面的初始化,在一個切片在未初始化之前預設為 nil,長度為 0且沒有底層陣列。。nil是Go的一個關鍵字。

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

這裡寫圖片描述
可以看出,切片s長度和容量都是0,值是nil。即切片是空的。

切片的內幕

一個切片是一個數組片段的描述。它包含了指向陣列的指標,片段的長度, 和容量(片段的最大長度)。
切片操作並不複製切片指向的元素。它建立一個新的切片並複用原來切片的底層陣列。 這使得切片操作和陣列索引一樣高效。因此,通過一個新切片修改元素會影響到原始切片的對應元素。

package main

import "fmt"

func main() {

    s1 := [...]int{1, 2, 3, 4, 5}
    s2 := s1[2:]
    fmt.Println("修改前s1:", s1)
    fmt.Println("修改前s2:", s2)
    s2[2] = 10
    fmt.Println("修改後s2:", s2)
    fmt.Println("修改後s1:", s1)
}

這裡寫圖片描述

切片的增長

前面說過,切片可以看成是動態陣列,所以他的長度是可變的。只要切片不超出底層陣列的限制,它的長度就是可變的,只需將它賦予其自身的切片即可。

package main

import "fmt"

func main() {
    s := make([]int, 5, 10)
    fmt.Println("修改後s:", len(s))
    s = s[:cap(s)]
    fmt.Println("修改後s:", len(s))
}

這裡寫圖片描述
上面就是把切片s的長度修改成他的最大長度。如果超過他的最大長度,則會報錯–“panic: runtime error: slice bounds out of range”。

s = s[:12]

這裡寫圖片描述
如果想增加切片的容量,我們必須建立一個新的更大的切片並把原分片的內容都拷貝過來。整個技術是一些支援動態陣列語言的常見實現。

package main

import "fmt"

func main() {
    s := make([]int, 5, 10)
    t := make([]int, len(s), cap(s)*2) // 擴大s的容量
    for i := range s {
        s[i] = i
        t[i] = s[i]
    }
    fmt.Println("修改前s:", s)
    fmt.Println("修改前len(s):", len(s))
    fmt.Println("修改前cap(s):", cap(s))
    s = t
    fmt.Println("修改後s:", s)
    fmt.Println("修改後len(s):", len(s))
    fmt.Println("修改後cap(s):", cap(s))
}

這裡寫圖片描述
上面把一個切片的容量擴大了2倍。
對於迴圈中複製的操作Go提供了可copy內建函式。copy函式可以將源切片的元素複製到目的切片。copy函式支援不同長度的切片之間的複製(它只複製較短切片的長度個元素)。此外, copy 函式可以正確處理源和目的切片有重疊的情況。
使用copy函式可以直接替換上面的for迴圈。

copy(t, s)

除此之外,Go還提供了一個為切片追加新的元素操作的方法– append()。
append 的第一個引數 s 是一個元素型別為 T 的切片, 其餘型別為 T 的值將會追加到該切片的末尾。append 的結果是一個包含原切片所有元素加上新新增元素的切片。

package main

import "fmt"

func main() {
    var s []int
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)

    s = append(s, 0)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)

    s = append(s, 1)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)

    s = append(s, 2, 3, 4)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)

    s2 := []int{5, 6, 7}
    s = append(s, s2...)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

這裡寫圖片描述
上面的程式,先建立了一個nil切片,然後不斷往裡面新增新的資料。s = append(s, s2…)這個寫法是把後面的s2切片打散傳給append,相當於是s = append(s, 5, 6, 7),這也是Go支援的語法。
可以看出切片的長度和容量是不斷增加的。通過我的觀察,append增加容量是按照如果容量不夠把之前切片的容量乘以2,如果乘以2還不夠就之前容量+1乘以2來遞增的。不過這個以後還得看看原始碼確認下,今天一直沒找到在哪。

通過到目前的瞭解,切片應該在Go中使用的比陣列要多。