Go 常見錯誤集錦之 append 操作 slice 時的副作用

yudotyang發表於2021-10-08

大家好,我是 Go 學堂的漁夫子。本文是對《100 Go Mistackes:How to Avoid Them》一書的翻譯。因翻譯水平有限,難免存在翻譯準確性問題,敬請諒解。

我們知道,對 slice 的切分實際上是作用在 slice 的底層陣列上的操作。對一個已存在的 slice 進行切分操作會建立一個新的 slice,但都會指向相同的底層陣列。因此,如果一個索引值對兩個 slice 都是可見的,那麼使用索引更新一個 slice 的時候(例如 s1[1] = 10),同時該更新也會影響另外一個 slice。

本文將介紹使用 append 時的一種常見的錯誤,該操作在某些場景下會導致副作用。

首先,我們有以下示例:初始化一個切片 s1,然後通過切分 s1 的方式建立切片 s2,再然後通過在 s2 上進行 append 操作建立切片 s3:

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3: = append(s3, 10)

通過以上程式碼可知,s1 包含 3 個元素。對 s1 進行切分操作來建立 s2。然後對 s2 進行 append 操作建立 s3。那麼,最後這 3 個切片的狀態是什麼呢?

下圖是 s1 和 s2 在記憶體中的狀態示例圖:

s1 是長度為 3,容量為 3 的切片結構,而 s2 是長度為 1,容量為 2 的切片結構,s1 和 s2 的都指向相同的底層陣列。

當使用 append 給切片新增元素的時候 會檢查切片是否已滿:切片的長度等於切片容量時判定為元素已滿。如果沒有滿,還有空間,那麼 append 函式則將元素新增到原底層資料的空閒空間中,並返回一個新的結構體。

在該示例中,s2 還沒有滿,還能接收一個元素。因此,下圖是 3 個切片最終的狀態,如圖:

由圖可看出,3 個切片共享一個底層資料,資料的最後一個元素被更新為 10。那麼,如果我們列印這 3 個切片,則會有以下輸出:

s1=[1 2 10], s2=[2], s3=[2 10]

可見,即使我們沒有修改 s1[2],也沒修改 s1[1],但 s1 的內容被修改了。因此,我們應該牢記該規則,以避免造成意外的錯誤。

我們再來看下另外一個影響:當將通過切分得到的新切片作為函式引數傳遞時的影響。

我們看下面的示例程式碼:

func main() {
    s := []int{1, 2, 3}

    f(s[:2])
    // Use s
}

func f(s []int) {
    // Update s
}

這種實現非常危險。實際上,函式 f 會對輸入的切片產生副作用。例如,如果函式 f 呼叫 append(s, 10),那麼 main 函式中的 s 的內容就不再是 [1 2 3],而是 [1 2 10]。

我們該如何解決上述問題呢?

方案一:拷貝切片 我們可以通過對原切片進行拷貝,然後構建一個新的切片變數,如下程式碼所示:

func main() {
    s := []int{1, 2, 3}
    sCopy := make([]int, 2)
    copy(sCopy, s) 

    f(sCopy)
    result := append(sCopy, s[2]) 
    // Use result
}

func f(s []int) {
    // Update s
}

① 將 s 的前兩個元素拷貝到 sCopy 中

② 通過 append 函式將 s[2] 增加到 sCopy 中構建一個新的結果切片

因為我們在函式 f 中傳遞了一個拷貝,即使在函式中呼叫了 append,也不會對該切片造成副作用。該方案的缺點就是需要對已存在的切片進行一次拷貝,如果切片很大,那拷貝時儲存和效能就會成為問題

方案二:限制切片容量 該方案是通過限制切片容量,在對切片進行操作時自動產生一個新的底層資料的方式來避免對原有切片副作用的產生。該方案就是所謂的滿切片表示式:s[low:high:max]。這種滿切片表示式和 s[low:high] 的區別在於 s[low:high:max] 的切片的容量是 max-low,而 s[low:high] 的容量是 s 中底層資料的最大容量減去 low。

func main() {
    s := []int{1, 2, 3}
    f(s[:2:2]) 
    // Use s
}

func f(s []int) {
    // Update s
}

① 使用滿切片表示式傳遞一個子切片

上面程式碼中傳遞給 f 函式的切片不是 s[:2],而是 s[:2:2]。因此,切片的容量是 2 - 0 = 2,如下所示:

這種解決方案既共享了切片的底層陣列,又通過限制容量避免了副作用。

我們必須時刻注意,從一個切片切分成子切片時,在這兩個切片之間有可能會產生資料副作用。當直接修改一個元素或使用 append 函式的時候,這種副作用就會產生。如果我們想解決這種副作用,可以通過滿切片表示式的方式來解決。這種方式避免了額外的拷貝,還算是比較高效的。

更多原創文章乾貨分享,請關注公眾號
  • Go 常見錯誤集錦之 append 操作 slice 時的副作用
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章