10Go語言——slice

Go語言——slice

1、切片介紹

切片是一種數據結構,這種數據結構便於使用和管理數據集合。切片是圍繞動態數組的概念構建的,能夠按需自動增加和縮小。切片的動態增加是經過內置函數 append 來實現的。這個函數能夠快速且高效地增加切片。還能夠經過對切片再次切片來縮小一個切片的大小。由於切片的底層內存也是在連續塊中分配的,因此切片還能得到索引、迭代以及爲垃圾回收優化的好處。 算法

切片是引用類型,指向底層數組,切片語法和數組很像,只是沒有長度而已。slice底層是連續內存,動態增長長度其實若是超過底層數組容量,也會從新分配內存。數據庫

  • 指向底層的數組,做爲變長數組的替代方案,能夠關聯底層數組的局部或所有
  • slice 爲引用類型,但自身是結構體,值拷貝傳遞。
  • 若是多個slice指向相同底層數組,其中一個的值改變會影響所有
  • 能夠直接建立或從底層數組獲取生成,通常使用make()建立
  • make([]T, len, cap) ,其中cap能夠省略,則和len的值相同。
  • 屬性 len 表示可用元素數量,讀寫操做不能超過該限制。
  • 屬性 cap 表示最⼤擴張容量,不能超出數組限制。
  • 若是 slice == nil,那麼 len、 cap 結果都等於 0。

2、內部實現和原理

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片是有 3 個字段的數據結構 ,這 3 個字段分別是指向底層數組的指針、切片訪問的元素的個數(即長度)和切片容許增加到的元素個數(即容量)。 express

切片示例

能夠看出,slice是引用類型。時刻注意一點,slice傳遞的時候也是值拷貝,把slice的指針,長度,容量拷貝過去,那麼一樣能夠去操做底層數組數組

3、建立和初始化

一、普通初始化(var或者:= ,字面量)

var slice[]int //只聲明一個slice,len  和 cap 都是0
var slice0 []int=[]int{1,2,3}//完整聲明,字面量初始化
var slcie1=[]int{1,2,3}  //直接var 字面量初始化
slice2:=[]int{1,2,3}//函數內部能夠用:=替代var 初始化

二、使用make() 函數進行初始化

通常使用make()進行建立 make([]T, len, cap) 指定類型,長度,容量,make()會在按照容量分配內存空間,也就是分配的數組數據結構

slice:=make([]string,5)//使用長度聲明一個字符串切片,長度、容量都是5
slice0:=make([]int,3,5)//使用長度和容量聲明int切片,長度爲3,容量爲5

容量小於長度的切片會在編譯時報錯架構

slice := make([]int, 5, 3)//注意,不容許,

注意:使用make([]int,3,5)建立slice,指定的容量cap 其實就是初始化的底層數組的長度。可是此slice 長度爲3,只能操做底層數組的前3個,後兩個是不能操做的,可是能夠經過append函數添加到此slice中。若是基於這個切片建立新的切片,新切片會和原有切片共享底層數組,也能經過後期操做(如append)來訪問多餘容量的元素。app

三、使用索引聲明切片

// 建立字符串切片
// 使用空字符串初始化第 100 個元素
slice := []string{99: ""}

記住,若是在[]運算符裏指定了一個值,那麼建立的就是數組而不是切片。只有不指定值的時候,纔會建立切片 。這裏字面量聲明索引爲99的位置爲空字符串,因此,長度和容量都是 100 個元素,最少開闢了100個內存空間。函數

四、從數組建立slice

(1)經過兩個冒號建立切片,slice[x:y:z]切片實體[x:y]切片長度len = y-x,切片容量cap = z-x性能

data := [...]int{0, 1, 2, 3, 4, 5, 6} //初始化一個數組
slice := data[1:4:5] // [low : high : max]  經過兩個冒號建立切片

使用兩個冒號[1:4:5] 從數組中建立切片,長度爲4-1=3,也就是索引從1到3 的數據(1,2,3),而後,後面是最大是5,即容量是5-1=4,即,建立的切片是長度爲從索引爲 一、二、3 的切片,底層數組爲[ 1,2,3,4] 優化

在這裏插入圖片描述

(2)經過單個冒號,索引建立slice

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

expression slice len cap comment
------------+----------------------+------+-------+---------------------
data[:6:8] [0 1 2 3 4 5]         //6 8 省略 low.
data[5:] [5 6 7 8 9]             //5 5 省略 high、 max。
data[:3] [0 1 2]                 //3 10 省略 low、 max。
data[:] [0 1 2 3 4 5 6 7 8 9]    //10 10 所有省略。

五、建立空的slice

首先說一下,slice和array的區別,其實就是[]中有沒有值,有值就是數組,無值就是slice

// 使用 make 建立空的整型切片
slice := make([]int, 0)
// 使用切片字面量建立空的整型切片
slice := []int{}

空切片在底層數組包含 0 個元素,也沒有分配任何存儲空間。想表示空集合時空切片頗有用.
例如,數據庫查詢返回 0 個查詢結果時

4、切片的使用

一、直接用索引賦值

// 建立一個整型切片
// 其容量和長度都是 5 個元素
slice := []int{10, 20, 30, 40, 50}
// 改變索引爲 1 的元素的值
slice[1] = 25

二、reslice 也就是經過slice建立 slice

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error。 不能超過父slice的容量。
//索引s[2:5]  表示從索引2開始,到索引4結束,包括2可是不包括5

在這裏插入圖片描述

  • s1:=s[2:5] 獲得的是len爲 5-2=3, cap爲10-2=8。 這個公式 能夠計算。 因此,s1指向底層數組的從第二個索引開始,三個長度的內存,2,3,4. 可是他的容量cap爲8,也就是,s1指向內存起始位置是原數組的2,一直到9,都是一塊連續的內存。若是要增長s1的長度,不須要開闢新的內存空間,只須要往s1裏面添加就行,他繼續指向這塊內存地址,直到他的長度超過容量,就會從新開闢內存空間。
  • s2:=s1[2:6:7] ,s2是經過s1建立的新的slice,它一樣指向了和s1,s1同樣的底層數組,只不過是起始的索引位置不一樣。s2表示從s1索引爲2開始到索引爲5的長度爲4個的slice,而且還指定了他的容量爲7,也就是它不使用它起始索引到底層數組末尾的長度做爲容量,而是使用本身指定的容量7. 如圖,s1長度爲3,而s2的長度爲4,而且是從s1索引爲2開始的,可是卻沒有報錯。 是由於,reslice,是根據 子slice相對於父slice的容量建立的,只要子slice的長度沒有超過父slice的容量,那麼就是容許的,由於他們指向的是同一個底層數組。
  • 因此,s3:=s2[3:6] 他報錯了。由於s3是建立 從s2做爲爲3,長度爲3的slice,可是s2的長度爲4,容量爲5,從索引3開始算,s2只能最大建立出長度爲2的 子slice,不能建立出長度爲3的slice
  • 總結下,就是,父slice 和子slice 都是指向了同一塊連續內存,底層是個數組。 只是根據長度和容量的不一樣,建立的slice 容許操做的內存塊是不一樣的。若是子slice建立時不指定本身的容量cap,那麼它的容量默認爲,從他指向的那個底層數組的索引開始,一直到這個數組的最末端。這塊是它的容量,也就是它建立之後,能夠繼續擴展而不改變內存地址。注意,它能夠操做的連續內存是由它的長度決定的。好比,長度爲3,容量爲5,的slice,它只能操做從它指向底層數組的那個首索引開始,日後三個數,另外兩個是不容許操做的。

因爲reslice 的slice指向了同一塊連續內存空間,因此操做是會相互影響的

// 建立一個整型切片
// 其長度和容量都是 5 個元素
slice := []int{10, 20, 30, 40, 50}
// 建立一個新切片
// 其長度是 2 個元素,容量是 4 個元素 {20,30}
newSlice := slice[1:3]    
// 修改 newSlice 索引爲 1 的元素
// 同時也修改了原來的 slice 的索引爲 2 的元素
newSlice[1] = 35      
//最終底層爲   {10,35,30,40,50}

三、append 增加slice

相對於數組而言,使用切片的一個好處是,能夠按需增長切片的容量。 函數 append 老是會增長新切片的長度,而容量有可能會改變,也可能不會改變,這取決於被操做的切片的可用容量。 (也就是,若是容量夠直接加,不夠從新分配內存,拷貝原數組加)

(1)向 slice 尾部添加數據,返回新的 slice 對象。

s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)
s2 := append(s, 1)
fmt.Printf("%p\n", &s2)
fmt.Println(s, s2)
//輸出結果
0xc000004440
0xc000004480
[] [1]

append函數作的事就是更改slice指向底層數組的值

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[:3]
s2 := append(s, 100, 200) // 添加多個值。  
fmt.Println(data)
fmt.Println(s)
fmt.Println(s2)
//輸出----------append函數更改了slice執行底層數組data,增長了s的長度,並將相應位置的值改變
[0 1 2 100 200 5 6 7 8 9]
[0 1 2]
[0 1 2 100 200]

(2) 未超過原slice容量,底層數組不會從新分配

package main

import "fmt"

func main(){
    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s := data[:2:3] //長度爲1,容量爲3
    fmt.Printf("len: %d,cap: %d\n",len(s),cap(s))
    fmt.Printf("原地址對比\n%p\n%p\n",&data[0],&s[0])
    s2 := append(s, 100) // 添加1一個值,未超過容量
    fmt.Printf("len: %d,cap: %d\n",len(s2),cap(s2))
    fmt.Printf("未超過容量時地址對比\n%p\n%p\n",&data[0],&s2[0])
    s3 := append(s2, 200) // 再次添加一個值,超過容量3
    fmt.Printf("len: %d,cap: %d\n",len(s3),cap(s3))
    fmt.Printf("超過容量時地址對比\n%p\n%p\n",&data[0],&s3[0])

}
//輸出
len: 2,cap: 3
原地址對比
0xc00008e000
0xc00008e000
len: 3,cap: 3
未超過容量時地址對比
0xc00008e000
0xc00008e000
len: 4,cap: 6
超過容量時地址對比
0xc00008e000
0xc00007e030

(3)可是,一旦超出原 slice.cap 限制,就會從新分配底層數組,即使原數組並未填滿。

data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
s = append(s, 100, 200) // ⼀次 append 兩個值,超出 s.cap 限制。
fmt.Println(s, data) // 從新分配底層數組,與原數組無關。
fmt.Println(&s[0], &data[0]) // 比對底層數組起始指針。
//輸出
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0xc00007e030 0xc00004a060

上面代碼表示,切片s 的長度爲2,容量爲3,而s經過append一次性添加兩個值,也就是想把s變爲長度爲4的切片,這超過了s的原始容量,因此,這樣append會從新開闢一段內存地址,長度爲4,容量爲4。能夠看大他們的內存地址是不同的。

(4)append超過容量時,由增加因子決定開闢多大新內存

s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 1000000; i++ {
    s = append(s, i)
    if n := cap(s); n > c {
        fmt.Printf("cap: %d -> %d  %.2f\n", c, n,float32(n)/float32(c))
        c = n
    }
}
//輸出
cap: 1 -> 2  2.00
cap: 2 -> 4  2.00
cap: 4 -> 8  2.00
cap: 8 -> 16  2.00
cap: 16 -> 32  2.00
cap: 32 -> 64  2.00
cap: 64 -> 128  2.00
cap: 128 -> 256  2.00
cap: 256 -> 512  2.00
cap: 512 -> 1024  2.00
cap: 1024 -> 1280  1.25
cap: 1280 -> 1696  1.33
cap: 1696 -> 2304  1.36
cap: 2304 -> 3072  1.33
cap: 3072 -> 4096  1.33
cap: 4096 -> 5120  1.25
cap: 5120 -> 7168  1.40
cap: 7168 -> 9216  1.29
cap: 9216 -> 12288  1.33
cap: 12288 -> 15360  1.25
cap: 15360 -> 19456  1.27
cap: 19456 -> 24576  1.26
cap: 24576 -> 30720  1.25
cap: 30720 -> 38912  1.27
cap: 38912 -> 49152  1.26
cap: 49152 -> 61440  1.25
cap: 61440 -> 76800  1.25
cap: 76800 -> 96256  1.25
cap: 96256 -> 120832  1.26
cap: 120832 -> 151552  1.25
cap: 151552 -> 189440  1.25
cap: 189440 -> 237568  1.25
cap: 237568 -> 296960  1.25
cap: 296960 -> 371712  1.25
cap: 371712 -> 464896  1.25
cap: 464896 -> 581632  1.25
cap: 581632 -> 727040  1.25
cap: 727040 -> 909312  1.25
cap: 909312 -> 1136640  1.25

從輸出看,容量最開始以2倍的方式進行開闢,數據量越大,增加因子會穩定在1.25左右。

一旦元素個數超過 1000,容量的增加因子會設爲 1.25左右,也就是會每次增長 25%的容量。隨着語言的演化,這種增加算法可能會有所改變。

在大批量添加數據時,建議一次性分配足夠大的空間,以減小內存分配和數據複製開銷。或初始化足夠長的 len 屬性,改用索引號進行操做。及時釋放再也不使用的 slice 對象,避免持有過時數組,形成 GC 沒法回收。

可是若是是要避免多個slice同時操做同一內存出現錯誤時,就須要將將slice建立爲長度=容量,避免append新slice指向同一內存操做數據,具體狀況根據實際靈活選擇。

(5)將一個切片追加到另外一個切片 ,使用...運算符

// 建立兩個切片,並分別用兩個整數進行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 將兩個切片追加在一塊兒,並顯示結果
fmt.Printf("%v\n", append(s1, s2...))   //使用... 就能夠
//輸出:
[1 2 3 4]
//就像經過輸出看到的那樣,切片 s2 裏的全部值都追加到了切片 s1 的後面。使用 Printf時用來顯示 append 函數返回的新切片的值

四、使用for range 迭代slice

// 建立一個整型切片
    // 其長度和容量都是 4 個元素
    slice := []int{10, 20, 30, 40} // 迭代每個元素,並顯示其值
    for i, v := range slice {
        fmt.Printf("Index: %d Value: %d\n", i, v)
    }
    //輸出
    Index: 0 Value: 10
    Index: 1 Value: 20
    Index: 2 Value: 30
    Index: 3 Value: 40

關鍵字 range 會返回兩個值。第一個值是當前迭代到的索引位置,第二個值是該位置對應元素值的一份拷貝

// 建立一個整型切片
// 其長度和容量都是 4 個元素
slice := []int{10, 20, 30, 40}
// 迭代每一個元素,並顯示值和地址
for index, value := range slice {
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
    value, &value, &slice[index])
}
//輸出:-----明顯看出內存地址不一樣,因此這裏的v,是拷貝,其實只是一個內存地址不斷更新存不一樣的拷貝
Value: 10 Value-Addr: C000058058 ElemAddr: C0000560C0
Value: 20 Value-Addr: C000058058 ElemAddr: C0000560C8
Value: 30 Value-Addr: C000058058 ElemAddr: C0000560D0
Value: 40 Value-Addr: C000058058 ElemAddr: C0000560D8

由於迭代返回的變量是一個迭代過程當中根據切片依次賦值的新變量,因此 value 的地址老是相同的。要想獲取每一個元素的地址,可使用切片變量和索引值

迭代返回索引和位置很是有用,若是咱們只想用其中一個,能夠經過佔位符 「_」 操做

slice := []int{10, 20, 30, 40}
for _, value := range slice {// 迭代每一個元素,並顯示其值,經過佔位符,不須要獲得索引
    fmt.Printf("Value: %d\n", value)
}
//輸出:
Value: 10
Value: 20
Value: 30
Value: 40

五、copy 複製slice

函數 copy 在兩個 slice 間複製數據,複製長以 len 小的爲準。兩個 slice 可指向同一底層數組,容許元素區間重疊。

(1)不指定位置

s1 := []int{1, 2, 3, 4, 5, 6}
s2 := []int{7, 8, 9}
fmt.Println(s2)
copy(s2, s1)        //將s1  copy到 s2, 誰的長度小聽誰的,此處只能拷貝前三個
fmt.Println(s2) 
//輸出------長度大的往長度小的複製最大隻能複製以小長度爲準的位數
[7 8 9]
[1 2 3]

(2)指定位置

還能夠指定複製slice 和被複制slice 的位置進行copy,不指定默認從前日後,如上面代碼

s1 := []int{0,1, 2, 3, 4, 5, 6}
s2 := []int{7, 8, 9}
fmt.Println("s1=",s1)
fmt.Println("s2=",s2)
copy(s1[4:6], s2[1:3]) //將s2指定位置複製到s1指定的位置
fmt.Println("s1=",s1)
//輸出
s1= [0 1 2 3 4 5 6]
s2= [7 8 9]
s1= [0 1 2 3 8 9 6]

(3)注意,copy之後的數組,指向的底層數組也會隨之改變

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[8:]
s2 := data[:5]
copy(s2, s) // dst:s2, src:s
fmt.Println(s2)
fmt.Println(data)
//輸出
[8 9 2 3 4]
[8 9 2 3 4 5 6 7 8 9]

六、切片的傳遞

切片的傳遞,都是值傳遞,是切片的拷貝

在 64 位架構的機器上,一個切片須要 24 字節的內存:指針字段須要 8 字節,長度字段須要8字節,容量字段須要 8 字節。因爲與切片關聯的數據包含在底層數組裏,不屬於切片自己,因此將切片複製到任意函數的時候,對底層數組大小都不會有影響。複製時只會複製切片自己,不會涉及底層數組 。因此無論多大的slice,值傳遞都不會影響性能。

5、多維切片

多維切片和多維數組差很少,只不過[]沒有值

// 建立一個整型切片的切片
slice := [][]int{{10}, {100, 200}}

在這裏插入圖片描述

使用append 給slice[0]進行增加,查看內存變化

// 建立一個整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 爲第一個切片追加值爲 20 的元素
slice[0] = append(slice[0], 20)

以上代碼會先增加切片,會爲新的整型切片分配新的底層數組,而後將切片複製到外層切片的索引爲 0 的元素
在這裏插入圖片描述

相關文章
相關標籤/搜索