Go語言之切片

切片也是一種數據結構,它和數組很是類似,由於他是圍繞動態數組的概念設計的,能夠按需自動改變大小,使用這種結構,能夠更方便地管理和使用數據集合。算法


內部實現數組


切片是基於數組實現的,它的底層是數組,它本身自己很是小,能夠理解爲對底層數組的抽象。由於機遇數組實現,因此它的底層的內存是連續非配的,效率很是高。它還有能夠經過索引得到數據、能夠迭代以及垃圾回收優化的好處。數據結構


切片對象很是小,是由於它是隻有 3 個字段的數據結構:一個是指向底層數組的指針,一個是切片的長度,一個是切片的容量。這 3 個字段,就是Go語言操做底層數組的元數據,有了它們,咱們就能夠任意地操做切片了。app


聲明和初始化ide


切片建立的方式有好幾種,咱們先看下最簡潔的make方式。函數


slice:=make([]int,5)


使用內置的make函數時,須要傳入一個參數,指定切片的長度,例子中咱們使用的時 5 ,這時候切片的容量也是 5 。固然咱們也能夠單獨指定切片的容量。優化


slice:=make([]int,5,10)


這時,咱們建立的切片長度是 5 ,容量是 10 。須要注意的這個容量 10 其實對應的是切片底層數組的。spa


由於切片的底層是數組,因此建立切片時,若是不指定字面值的話,默認值就是數組的元素的零值。這裏咱們因此指定了容量是 10 ,可是咱們只能訪問 5 個元素。由於切片的長度是 5 ,剩下的 5 個元素,須要切片擴充後才能夠訪問。設計


容量必須>=長度,咱們是不能建立長度大於容量的切片的。指針


還有一種建立切片的方式,是使用字面量,就是指定初始化的值。


slice:=[]int{1,2,3,4,5}


有沒有發現,跟建立數組很是像,只不過不用制定[]中的值。這時候切片的長度和容量是相等的,而且會根據咱們指定的字面量推導出來。固然咱們也能夠像數組同樣,只初始化某個索引的值:


slice:=[]int{4:1}


這是指定了第 5 個元素爲 1 ,其餘元素都是默認值 0 。這時候切片的長度和容量也是同樣的。這裏再次強調一下切片和數組的微小差異。


//數組
array:=[5]int{4:1}
//切片
slice:=[]int{4:1}


切片還有nil切片和空切片,它們的長度和容量都是 0 。可是它們指向底層數組的指針不同,nil切片意味着指向底層數組的指針爲nil,而空切片對應的指針是個地址。


//nil切片
var nilSlice []int
//空切片
slice:=[]int{}


nil切片表示不存在的切片,而空切片表示一個空集合,它們各有用處。


切片另一個用處比較多的建立是基於現有的數組或者切片建立。


slice := []int{1, 2, 3, 4, 5}
slice1 := slice[:]
slice2 := slice[0:]
slice3 := slice[:5]

fmt.Println(slice1)
fmt.Println(slice2)
fmt.Println(slice3)


基於現有的切片或者數組建立,使用[i:j]這樣的操做符便可。它表示以i索引開始,到j索引結束,截取原數組或者切片,建立而成的新切片。新切片的值包含原切片的i索引,可是不包含j索引。對比Java的話,發現和String的subString方法很像。


i若是省略,默認是 0 ;j若是省略,默認是原數組或者切片的長度。因此例子中的三個新切片的值是同樣的。這裏注意的是ij都不能超過原切片或者數組的索引。


slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]

newSlice[0] = 10

fmt.Println(slice)
fmt.Println(newSlice)


這個例子證實了,新的切片和原切片共用的是一個底層數組。因此當修改的時候,底層數組的值就會被改變,因此原切片的值也改變了。固然對於基於數組的切片也同樣的。


咱們基於原數組或者切片建立一個新的切片後,那麼新的切片的大小和容量是多少呢?這裏有個公式:


對於底層數組容量是k的切片slice[i:j]來講
長度:j-i
容量:k-i


好比咱們上面的例子slice[1:3],長度就是3-1=2,容量是5-1=4。不過代碼中咱們計算的時候不用這麼麻煩,由於Go語言爲咱們提供了內置的lencap函數來計算切片的長度和容量。


slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]

fmt.Printf("newSlice長度:%d,容量:%d",len(newSlice),cap(newSlice))


以上是基於一個數組或者切片使用 2 個索引建立新切片的方法。此外還有一種 3 個索引的方法,第 3 個用來限定新切片的容量,其用法爲slice[i:j:k]


slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:2:3]


這樣咱們就建立了一個長度爲2-1=1,容量爲3-1=2的新切片,不過第三個索引,不能超過原切片的最大索引值 5 。


使用切片


使用切片,和使用數組同樣,經過索引就能夠獲取切片對應元素的值,一樣也能夠修改對應元素的值。


slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice[2]) //獲取值
slice[2] = 10 //修改值
fmt.Println(slice[2]) //輸出10


切片只能訪問到其長度內的元素,訪問超過長度外的元素,會致使運行時異常,與切片容量關聯的元素只能用於切片增加。


咱們前面講了,切片算是一個動態數組,因此它能夠按需增加,咱們使用內置append函數便可。append函數能夠爲一個切片追加一個元素,至於如何增長、返回的是原切片仍是一個新切片、長度和容量如何改變這些細節,append函數都會幫咱們自動處理。


slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]

newSlice=append(newSlice,10)
fmt.Println(newSlice)
fmt.Println(slice)

//Output
[2 3 10]
[1 2 3 10 5]


例子中,經過append函數爲新建立的切片newSlice,追加了一個元素 10 。咱們發現打印的輸出,原切片slice的第 4 個值也被改變了,變成了 10 。引發這種結果的緣由是由於newSlice有可用的容量,不會建立新的切片來知足追加,因此直接在newSlice後追加了一個元素 10 。由於newSliceslice切片共用一個底層數組,因此切片slice的對應的元素值也被改變了。


這裏newSlice新追加的第 3 個元素,其實對應的是slice的第 4 個元素,因此這裏的追加實際上是把底層數組的第4個元素修改成 10 ,而後把newSlice長度調整爲 3 。


若是切片的底層數組沒有足夠的容量時,就會新建一個底層數組,把原來數組的值複製到新底層數組裏,再追加新值,這時候就不會影響原來的底層數組了。


因此通常咱們在建立新切片的時候,最好要讓新切片的長度和容量同樣,這樣咱們在追加操做的時候就會生成新的底層數組,和原有數組分離,就不會由於共用底層數組而引發奇怪問題,由於共用數組的時候修改內容,會影響多個切片。


append函數會智能地增加底層數組的容量,目前的算法是:容量小於 1000 個時,老是成倍的增加;一旦容量超過 1000 個,增加因子設爲 1.25 ,也就是說每次會增長 25% 的容量。


內置的append也是一個可變參數的函數,因此咱們能夠同時追加好幾個值。


newSlice=append(newSlice,10,20,30)


此外,咱們還能夠經過...操做符,把一個切片追加到另外一個切片裏。


slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:2:3]

newSlice=append(newSlice,slice...)
fmt.Println(newSlice)
fmt.Println(slice)


迭代切片


切片是一個集合,咱們可使用for range循環來迭代它,打印其中的每一個元素以及對應的索引。


    slice := []int{1, 2, 3, 4, 5}
    for i,v:=range slice{
        fmt.Printf("索引:%d,值:%d\n",i,v)
    }


若是咱們不想要索引,可使用_來忽略它。這是Go語言的用法,不少不須要的函數等返回值,均可以忽略。


    slice := []int{1, 2, 3, 4, 5}
    for _,v:=range slice{
        fmt.Printf("值:%d\n",v)
    }


這裏須要說明的是range返回的是切片元素的複製,而不是元素的引用。


除了for range循環外,咱們也可使用傳統的for循環,配合內置的len函數進行迭代。


 
 

    slice := []int{1, 2, 3, 4, 5}
    for i := 0; i < len(slice); i++ {
        fmt.Printf("值:%d\n", slice[i])
    }

在函數間傳遞切片


咱們知道切片是 3 個字段構成的結構類型,因此在函數間以值的方式傳遞的時候,佔用的內存很是小,成本很低。在傳遞複製切片的時候,其底層數組不會被複制,也不會受影響,複製只是複製的切片自己,不涉及底層數組。


func main() {
    slice := []int{1, 2, 3, 4, 5}
    fmt.Printf("%p\n", &slice)
    modify(slice)
    fmt.Println(slice)
}

func modify(slice []int) {
    fmt.Printf("%p\n", &slice)
    slice[1] = 10
}


打印的輸出以下:


0xc420082060
0xc420082080
[1 10 3 4 5]


仔細看,這兩個切片的地址不同,因此能夠確認切片在函數間傳遞是複製的。而咱們修改一個索引的值後,發現原切片的值也被修改了,說明它們共用一個底層數組。


在函數間傳遞切片很是高效,並且不須要傳遞指針和處理複雜的語法,只須要複製切片,而後根據本身的業務修改,最後傳遞迴一個新的切片副本便可。這也是爲何函數間使用切片傳遞參數,而不是數組的緣由。


關於多維切片就不介紹了,還有多維數組,一來它和普通的切片數組同樣,只不過是多個一維組成的多維;二來我壓根不推薦用多維切片和數組,可讀性很差,結構不夠清晰,容易出問題。

相關文章
相關標籤/搜索