Golang理解-數組和切片


數組

數組在Go中定義及特點

數組是一個由固定長度的特定類型元素組成的序列,一個數組可以由零個或多個元素組成。

因為數組的長度是固定的,因此在Go語言中很少直接使用數組。

和數組對應的類型是Slice(切片),它是可以增長和收縮動態序列,slice功能也更靈活,但是要理解slice工作原理的話需要先理解數組。

默認情況下,數組的每個元素都被初始化為元素類型對應的零值,對於數字類型來說就是0。我們也可以使用數組字面值語法用一組值來初始化數組:

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

在數組字面值中,如果在數組的長度位置出現的是“...”省略號,則表示數組的長度是根據初始化值的個數來計算。因此,上面q數組的定義可以簡化為

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必須是常量表達式,因為數組的長度需要在編譯階段確定。

我們將會發現,數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提供順序初始化值序列,但是也可以指定一個索引和對應值列表的方式初始化,就像下面這樣:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 歐元
    GBP                 // 英鎊
    RMB                 // 人民幣
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

如果一個數組的元素類型是可以相互比較的,那么數組類型也是可以相互比較的,這時候我們可以直接通過==比較運算符來比較兩個數組,只有當兩個數組的所有元素都是相等的時候數組才是相等的。不相等比較運算符!=遵循同樣的規則。

數組如何在函數參數中傳遞

當調用一個函數的時候,函數的每個調用參數將會被賦值給函數內部的參數變量,所以函數參數變量接收的是一個復制的副本,並不是原始調用的變量。

因為函數參數傳遞的機制導致傳遞大的數組類型將是低效的,並且對數組參數的任何的修改都是發生在復制的數組上,並不能直接修改調用時原始的數組變量

在這個方面,Go語言對待數組的方式和其它很多編程語言不同,其它編程語言可能會隱式地將數組作為引用或指針對象傳入被調用的函數。

注意事項

雖然通過指針來傳遞數組參數是高效的,而且也允許在函數內部修改數組的值,

但是數組依然是僵化的類型,因為數組的類型包含了僵化的長度信息。

而且數組也沒有任何添加或刪除數組元素的方法。由於這些原因,除了一些需要處理特定大小數組的特例外,數組依然很少用作函數參數;相反,我們一般使用slice來替代數組。


Slice

slice在go中的定義及特點

Slice(切片)代表變長的序列,序列中每個元素都有相同的類型。一個slice類型一般寫作[]T,其中T代表slice中元素的類型;slice的語法和數組很像,只是沒有固定長度而已。

一個slice是一個輕量級的數據結構,提供了訪問數組子序列(或者全部)元素的功能,而且slice的底層確實引用一個數組對象。

一個slice由三個部分構成:指針、長度和容量。

  • 指針指向第一個slice元素對應的底層數組元素的地址,要注意的是slice的第一個元素並不一定就是數組的第一個元素
  • 長度對應slice中元素的數目
  • 長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分別返回slice的長度和容量。

內置的len和cap函數分別返回slice的長度和容量。

多個slice之間可以共享底層的數據,並且引用的數組部分區間可能重疊。

img

如果切片操作超出cap(s)的上限將導致一個panic異常,但是超出len(s)則是意味着擴展了slice,因為新slice的長度會變大

因為slice值包含指向第一個slice元素的指針,因此向函數傳遞slice將允許在函數內部修改底層數組的元素

換句話說,復制一個slice只是對底層的數組創建了一個新的slice別名

slice之間不能比較(這和數組不同),因此我們不能使用"=="操作符來判斷兩個slice是否含有全部相等元素。

不過標准庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等([]byte),但是對於其他類型的slice,我們必須自己展開每個元素進行比較:

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
      	// 逐個元素比較
        if x[i] != y[i] {
            return false
        }
    }
    return true
}

為什么slice不支持比較呢?

  1. 一個slice的元素是間接引用的,一個slice甚至可以包含自身。雖然有很多辦法處理這種情形,但是沒有一個是簡單有效的。
  2. 因為slice的元素是間接引用的,一個固定的slice值(譯注:指slice本身的值,不是元素的值)在不同的時刻可能包含不同的元素,因為底層數組的元素可能會被修改. Go語言中map的key只做簡單的淺拷貝,它要求key在整個生命周期內保持不變性(譯注:例如slice擴容,就會導致其本身的值/地址變化)。
  3. 對於像指針或chan之類的引用類型,==相等測試可以判斷兩個是否是引用相同的對象

基於以上原因,我們安全的做法是直接禁止slice之間的比較操作,slice唯一合法的比較操作是和nil比較.

雖然slice是可以和nil進行比較的,但是只其中也有些細節需要注意:

  1. 一個零值的slice等於nil。一個nil值的slice並沒有底層數組。
  2. 一個nil值的slice的長度和容量都是0,但是也有非nil值的slice的長度和容量也是0的,例如[]int{}或make([]int, 3)[3:]
  3. 如果你需要測試一個slice是否是空的,使用len(s) == 0來判斷,而不應該用s == nil來判斷。除了和nil相等比較外,一個nil值的slice的行為和其它任意0長度的slice一樣;

內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略,在這種情況下,容量將等於長度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底層,make創建了一個匿名的數組變量,然后返回一個slice;只有通過返回的slice才能引用底層匿名的數組變量。在第一種語句中,slice是整個數組的view。在第二個語句中,slice只引用了底層數組的前len個元素,但是容量將包含整個的數組。額外的元素是留給未來的增長用的。

slice擴容原則

先來看看slice源碼中是如何寫的,在來分析擴容的原則:

func growslice(et *_type, old slice, cap int) slice {
    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        // 小於1024,*2擴容
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // 大於1024,*1.25
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
  // 下面代碼省略
  ....
}

從上面代碼可以看出, slice的擴容規則是:

  1. 小於1024,每次擴容后的cap = oldCap * 2
  2. 大於1024,每次擴容cap = oldCap * 1.25

我們可以看出,每次擴容會涉及到數組的copy,然后生成新的數組(slice指向新的數組),這樣會給系統帶來額外的開銷,通常我們在創建slice的時候,建議使用make函數,更具業務場景給定一個合適的cap大小,避免slice因為擴容而發生底層數組的copy。

slice的內存使用技巧

案例一:

輸入slice和輸出slice共用一個底層數組,這可以避免分配另一個數組,不過原來的數據將可能會被覆蓋:例如

func nonempty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++
        }
    }
    return strings[:i]
}

// 輸出:
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data)           // `["one" "three" "three"]`

同樣的,使用append也能實現同樣的功能:

func nonempty2(strings []string) []string {
    out := strings[:0] // zero-length slice of original
    for _, s := range strings {
        if s != "" {
            out = append(out, s)
        }
    }
    return out
}

無論如何實現,以這種方式重用一個slice一般都要求最多為每個輸入值產生一個輸出值,事實上很多這類算法都是用來過濾或合並序列中相鄰的元素。

這種slice用法是比較復雜的技巧,雖然使用到了slice的一些技巧,但是對於某些場合是比較清晰和有效的。

案例二:

使用slice來模擬stack操作,入棧即向slice中append元素,出棧則通過收縮slice,彈出棧頂的元素:

// 入棧, push
stack = append(stack, v)

// 出棧, pop
stack = stack[:len(stack)-1]

案例三:

要刪除slice中間的某個元素並保存原有的元素順序,可以通過內置的copy函數將后面的子slice向前依次移動一位完成:

list := []int{1,2,3,4,5,6}
// 刪除元素i,並保留原來的順序,原理就是將i后面的元素按次序copy
copy(list[i:],list[i+1:])

要刪除元素后不用保持原來順序的話,我們可以簡單的用最后一個元素覆蓋被刪除的元素:

func remove(slice []int, i int) []int {
  	// 使用最后一個元素覆蓋要刪除的元素
    slice[i] = slice[len(slice)-1]
  	// 返回新的slice
    return slice[:len(slice)-1]
}


免責聲明!

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



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