這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
之前講到了Go的基礎資料類型,除此之外,Go還支援很多複合類型的資料結構。
數組(array)
數組就是指一系列同一類型資料 的集合。
Go語言中,類型 [n]T 表示擁有 n 個 T 類型的值的數組。如:
var a [3]int
表示變數 a 聲明為擁有有 3個整數的數組。聲明文法上與java的區別是[]是寫在類型前面的。
當然,也可以讓編譯器統計數組字面值中元素的數目:
a := [...]int{1, 2,3}
這兩種寫法, a 都是對應長度為3的int數組。
數組的長度是其類型的一部分,因此數組不能改變大小。 可以用內建函數len()取得數組長度。
package mainimport "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 mainimport "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)=5b := 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 mainimport "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 mainimport "fmt"func main() { var s []int fmt.Println(s, len(s), cap(s)) if s == nil { fmt.Println("nil!") }}
可以看出,切片s長度和容量都是0,值是nil。即切片是空的。
切片的內幕
一個切片是一個數組片段的描述。它包含了指向數組的指標,片段的長度, 和容量(片段的最大長度)。
切片操作並不複製切片指向的元素。它建立一個新的切片並複用原來切片的底層數組。 這使得切片操作和數組索引一樣高效。因此,通過一個新切片修改元素會影響到原始切片的對應元素。
package mainimport "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 mainimport "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 mainimport "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 mainimport "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中使用的比數組要多。