Go語言中slice使用注意事項


Go 語言中的slice類型可以理解為是數組array類型的描述符,包含了三個因素:
  1. 指向底層數組的指針 
  2. slice目前使用到的底層數組的元素個數,即長度
  3. 底層數組的最大長度,即容量
因此當我們定義一個切片變量,s := make([]int, 5, 10),即為指向了一個最大長度為10的底層數組,目前切片s使用到的長度為5。 
 
基於slice的定義,在使用slice時,有以下幾點注意事項:
 
1.對slice進行切分操作
對slice進行切分操作會生成一個新的slice變量,新slice和原來的slice指向同一個底層數組,只不過指向的起始位置可能不同,長度及容量可能也不相同。
  • 當從左邊界有截斷時,會改變新切片容量大小
  • 左邊界默認0,最小為0;右邊界默認slice的長度,最大為slice的容量
  • 當然,因為指向同一個底層數組,對新slice的操作會影響到原來的slice
 
2.slice的賦值及函數間傳遞
a := []int{1, 2, 3, 4, 5}
b := a
如上所示,則a, b指向同一個底層數組,且長度及容量因素相同,對b進行的操作會影響到a。 
 
func main() {
    a := []int{1, 2, 3, 4, 5}
    modifySlice(a)
    //[10 2 3 4 5]
    fmt.Println(a)
}

func modifySlice(s []int) {
    s[0] = 10
}

如上所示,將slice作為參數在函數間傳遞的時候是值傳遞,產生了一個新的slice,只不過新的slice仍然指向原來的底層數組,所以通過新的slice也能改變原來的slice的值 
 
func main() {
    a := []int{1, 2, 3, 4, 5}
    modifySlice(a)
    //[1 2 3 4 5]
    fmt.Println(a)
}

func modifySlice(s []int) {
    s = []int{10, 20, 30, 40, 50}
}

示例代碼 https://play.golang.org/p/LbFovzP-Rj

但是,如上所示,在調用函數的內部,將s整體賦值一個新的slice,並不會改變a的值,因為modifySlice函數內對s重新的整體賦值,讓s指向了一個新的底層數組,而不是傳遞進來之前的a指向的那個數組,之后s的任何操作都不會影響到a了。

 

3.slice的append操作
append操作最容易踩坑,下面詳細說明一下。
append函數定義:   func append(s []T, x ...T) []T
Append基本原則:對於一個slice變量,若slice容量足夠,append直接在原來的底層數組上追加元素到slice上;如果容量不足,append操作會生成一個新的更大容量的底層數組。 
 
第一種情況:
func main() {
    a := make([]int, 2, 4)

    //通常append操作都是將返回值賦值給自己,
    //此處為了方便說明,沒有這樣做
    b := append(a, 1)
    //改變b的前2個元素值 會影響到a
    b[0] = 99

    //a: [99 0]    &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))
    //b: [99 0 1]  &b[0]: 0x10410020  len: 3  cap: 4
    fmt.Println("b:", b, " &b[0]:", &b[0], " len:", len(b), " cap:", cap(b))
}
如上所示,對a進行append操作,若append后的新slice的實際元素個數沒有超出原來指向的底層數組的容量,所以仍然使用原來的底層數組:a, b的第一個值的地址一樣, 改變b的前2個元素也會影響到a。其實這時候a,b指向的同一個底層數組的第3位(索引2)已經變成了數值1,但是對slice而言,除了底層數組,還有長度,容量兩個因素,這時候a的長度仍然是2,所以輸出的a的值沒有變化。 
 
第二種情況:
func main() {

    a := make([]int, 2, 4)

    //通常append操作都是將返回值賦值給自己,
    //此處為了方便說明,沒有這樣做
    b := append(a, 1, 2, 3)
    //改變b的前2個元素值 不會影響到a
    b[0] = 99

    //a: [0 0]         &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))
    //b: [99 0 1 2 3]  &b[0]: 0x10454000  len: 5  cap: 8
    fmt.Println("b:", b, " &b[0]:", &b[0], " len:", len(b), " cap:", cap(b))
}

如上所示,若append后的新slice即b的實際元素個數已經超出了原來的a指向的底層數組的容量,那么就會分配給b一個新的底層數組,可以看到,a,b第一個元素的地址已經不同,改變b的前兩個元素值也不會影響到a,同時容量也發生了變化。
 
第三種情況:
func main() {
    a := make([]int, 2, 4)
    a[0] = 10
    a[1] = 20
    //a: [10 20]  &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))

    //進行append操作
    b := append(a[:1], 1)

    //a: [10 1]   &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))
    
    //b: [10 1]   &b[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("b:", b, " &b[0]:", &b[0], " len:", len(b), " cap:", cap(b))
}

如上所示,若append后的b的實際元素個數沒有超過原來的a指向的底層數組的容量,那么a,b指向同一個底層數組。
注意此時append的操作對象是:對a進行切分之后的切片,只取了a的第一個值,相當於一個新切片,長度為1,和a指向同一個底層數組,我們稱這個切分后的新切片為c吧,那么就相當於b其實是基於c切片進行append的,直接在長度1之后追加元素,所以append之后a的第二個元素變成了1。【所以切分操作和append操作放一起的時候,一定要小心】 
 
第四種情況:
func main() {

    a := make([]int, 2, 4)
    a[0] = 10
    a[1] = 20
    //a: [10 20]       &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))

    //進行append操作
    //append是在第一個元素后開始追加,所以要超過容量,至少要追加4個,而不是之前例子的3個
    b := append(a[:1], 1, 2, 3, 4)

    //a: [10 20]       &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))
    
    //b: [10 1 2 3 4]  &b[0]: 0x10454020  len: 5  cap: 8
    fmt.Println("b:", b, " &b[0]:", &b[0], " len:", len(b), " cap:", cap(b))
}

如上所示,這種情況主要用來與第三種情況對比,如果append的元素數較多,超過了原來的容量,直接采用了新的底層數組,也就不會影響到a了。 
 
上述的四種情況所用例子都比較簡單,所以比較容易看清。要小心如果在函數間傳遞slice,調用函數采用append進行操作,可能會改變原來的值的,如下所示:
func main() {

    a := make([]int, 2, 4)
    a[0] = 10
    a[1] = 20
    //a: [10 20]  &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))

    testAppend(a[:1])

    //a: [10 1]   &a[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("a:", a, " &a[0]:", &a[0], " len:", len(a), " cap:", cap(a))

}

func testAppend(s []int) {
    //進行append操作
    s = append(s, 1)
    //s: [10 1]  &s[0]: 0x10410020  len: 2  cap: 4
    fmt.Println("s:", s, " &s[0]:", &s[0], " len:", len(s), " cap:", cap(s))
}

示例代碼 https://play.golang.org/p/mvUZkmBW8u

 

參考資料 https://blog.golang.org/go-slices-usage-and-internals

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM