Go起步:4、複合型別1--陣列array和切片slice
之前講到了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 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中使用的比陣列要多。