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中使用的比數組要多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章