Golang基礎之slice篇

下面代碼中,會輸出什麼?

func Assign1(s []int) {
    s = []int{6, 6, 6}
}

func Reverse0(s [5]int) {
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse1(s []int) {
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse2(s []int) {
    s = append(s, 999)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse3(s []int) {
    s = append(s, 999, 1000, 1001)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func main() {
    s := []int{1, 2, 3, 4, 5, 6}
    Assign1(s)
    fmt.Println(s) // (1)

    array := [5]int{1, 2, 3, 4, 5}
    Reverse0(array)
    fmt.Println(array) // (2)

    s = []int{1, 2, 3}
    Reverse2(s)
    fmt.Println(s) // (3)

    var a []int
    for i := 1; i <= 3; i++ {
        a = append(a, i)
    }
    Reverse2(a)
    fmt.Println(a) // (4)

    var b []int
    for i := 1; i <= 3; i++ {
        b = append(b, i)
    }
    Reverse3(b)
    fmt.Println(b) // (5)
    
    c := [3]int{1, 2, 3}
    d := c
    c[0] = 999
    fmt.Println(d) // (6)
}

    上面的這幾道題,也是Go編程中比較容易讓人感到迷惑的地方,但如果懂slice的底層原理,你就能避開這些坑且能輕鬆的答對上面幾道題。

    

array底層

    Go的數組array底層和C的數組一樣,是一段連續的內存空間,通過下標訪問數組中的元素。array只有長度len屬性而且是固定長度的。

    array的賦值是值拷貝的,看以下代碼:

func main() {
    c := [3]int{1, 2, 3}
    d := c
    c[0] = 999
    fmt.Println(d) // 輸出[1, 2, 3]
}

    因爲是值拷貝的原因,c的修改並沒有影響到d。 

 

slice底層

    掌握Go的slice,底層結構必須要了解。

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

    slice的底層結構由一個指向數組的指針ptr和長度len,容量cap構成,也就是說slice的數據存在數組當中。

 

slice的重要知識點

1. slice的底層是數組指針。

2. 當append後,slice長度不超過容量cap,新增的元素將直接加在數組中。

3. 當append後,slice長度超過容量cap,將會返回一個新的slice。

 

關於知識點1,看以下代碼:

func main() {
    s := []int{1, 2, 3} // len=3, cap=3
    a := s
    s[0] = 888
    s = append(s, 4)

    fmt.Println(a, len(a), cap(a)) // 輸出:[888 2 3] 3 3
    fmt.Println(s, len(s), cap(s)) // 輸出:[888 2 3 4] 4 6
}

    因爲slice的底層是數組指針,所以slice as指向的是同一個底層數組,所以當修改s[0]時,a也會被修改。

    當s進行append時,因爲長度len和容量cap是int值類型,所以不會影響到a。

 

關於知識點2,看以下代碼:

func main() {
    s := make([]int, 0, 4)
    s = append(s, 1, 2, 3)
    fmt.Println(s, len(s), cap(s)) // 輸出:[1, 2, 3] 3 4
    s = append(s, 4)
    fmt.Println(s, len(s), cap(s)) // 輸出:[1, 2, 3] 4 4
}

    當s進行append後,長度沒有超過容量,所以底層數組的指向並沒有發生變化,只是將值添加到數組中。

 

關於知識點3,看以下代碼:

func main() {
    s := []int{1, 2, 3}
    fmt.Println(s, len(s), cap(s)) // 輸出:[1, 2, 3] 3 3
    a := s

    s = append(s, 4) // 超過了原來數組的容量
    s[0] = 999
    fmt.Println(s, len(s), cap(s)) // 輸出:[1, 2, 3] 4 6
    fmt.Println(a,len(s),cap(s)) // 輸出:[1, 2, 3] 3 3
}

    上面代碼中,當對s進行append後,它的長度和容量都發生了變化,最重要的是它的底層數組指針指向了一個新的數組,然後將舊數組的值複製到了新的數組當中。

    a沒有被影響是因爲進行s[0] = 999賦值,是因爲s的底層數組指針已經指向了一個新的數組。

    我們通過觀察容量cap的變化,可以知道slice的底層數組是否發生了變化。cap的增長算法並不是每次都將容量擴大一倍的,感興趣的讀者可以看下slice的擴容算法。

 

使用array還是slice?

    一個很重要的知識點是:Go的函數傳參,都是以值的形式傳參。而且Go是沒有引用的,可以看下這篇文章

    如果要給函數傳遞一個有100w個元素的array時,直接使用array傳遞的效率是非常低的,因爲array是值拷貝,100w個元素都複製一遍是非常可怕的;這時就應該使用slice作爲參數,就相當於傳遞了一個指針。

    如果元素數量比較少,使用array還是slice作爲參數,效率差別並不大。


題目解析

package main

import "fmt"

func Assign1(s []int) {
    s = []int{6, 6, 6}
}

func Reverse0(s [5]int) {
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse1(s []int) {
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse2(s []int) {
    s = append(s, 999)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func Reverse3(s []int) {
    s = append(s, 999, 1000, 1001)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

func main() {
    s := []int{1, 2, 3, 4, 5, 6}
    Assign1(s)
    fmt.Println(s)
    // (1) 輸出[1, 2, 3, 4, 5, 6]
    // 因爲是值拷貝傳遞,Assign1裏的s和main裏的s是不同的兩個指針

    array := [5]int{1, 2, 3, 4, 5}
    Reverse0(array)
    fmt.Println(array)
    // (2) 輸出[1, 2, 3, 4, 5]
    // 傳遞時對array進行了一次值拷貝,不會影響原來的array

    s = []int{1, 2, 3}
    Reverse2(s)
    fmt.Println(s)
    // (3) 輸出[1, 2, 3]
    // 在沒有對s進行append時,len(s)=3,cap(s)=3
    // append之後超過了容量,返回了一個新的slice
    // 相當於只改變了新的slice,舊的slice沒影響

    var a []int
    for i := 1; i <= 3; i++ {
        a = append(a, i)
    }
    Reverse2(a)
    fmt.Println(a)
    // (4) 輸出[999, 3, 2]
    // 在沒有對a進行append時,len(a)=3,cap(a)=4
    // append後沒有超過容量,所以元素直接加在了數組上
    // 雖然函數Reverse2裏將a的len加1了,但它只是一個值拷貝
    // 不會影響main裏的a,所以main裏的len(a)=3

    var b []int
    for i := 1; i <= 3; i++ {
        b = append(b, i)
    }
    Reverse3(b)
    fmt.Println(b)
    // (5) 輸出[1, 2, 3]
    // 原理同(3)

    c := [3]int{1, 2, 3}
    d := c
    c[0] = 999
    fmt.Println(d)
    // (6) 輸出[1, 2, 3]
    // 數組賦值是值拷貝,所以不會影響原來的數組
}

 

總結

1. 謹記slice的底層結構是指針數組,並且lencap是值類型。

2. 使用cap觀察append後是否分配了新的數組。

3. Go的函數傳參都是值拷貝傳遞。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章