Go語(yǔ)言中Slice常見(jiàn)陷阱與避免方法詳解
前言
Go
語(yǔ)言提供了很多方便的數(shù)據(jù)類(lèi)型,其中包括 slice
。然而,由于 slice
的特殊性質(zhì),在使用過(guò)程中易犯一些錯(cuò)誤,如果不注意,可能導(dǎo)致程序出現(xiàn)意外行為。本文將詳細(xì)介紹 使用 slice
時(shí)易犯的一些錯(cuò)誤,幫助讀者更好的使用 Go
的 slice
,避免犯錯(cuò)誤。
slice 作為函數(shù) / 方法的參數(shù)進(jìn)行傳遞的陷阱
slice
作為參數(shù)進(jìn)行傳遞,有一些地方需要注意,先說(shuō)結(jié)論:
1、在函數(shù)里修改切片元素的值,原切片的值也會(huì)被改變;
若想修改新切片的值,而不影響原切片的值,可以對(duì)原切片進(jìn)行深拷貝:
通過(guò) copy(dst, src []Type) int
函數(shù)將原切片的元素拷貝到新切片中:此函數(shù)在拷貝時(shí),會(huì)基于兩個(gè)切片中,最小長(zhǎng)度為基礎(chǔ)去拷貝,也就是初始化新切片時(shí),長(zhǎng)度必須大于等于原切片的長(zhǎng)度。
2、在函數(shù)里通過(guò) append
方法,對(duì)切片執(zhí)行追加元素的操作,可能會(huì)引起切片擴(kuò)容,導(dǎo)致內(nèi)存分配的問(wèn)題,可能會(huì)對(duì)程序的性能 造成影響;
為避免切片擴(kuò)容,導(dǎo)致內(nèi)存分配,對(duì)程序的性能造成影響,在初始化切片時(shí),應(yīng)該根據(jù)使用場(chǎng)景,指定一個(gè)合理 cap
參數(shù)。
3、在函數(shù)里通過(guò) append
函數(shù),對(duì)切片執(zhí)行追加元素的操作,原切片里不存在新元素。
若想實(shí)現(xiàn)執(zhí)行 append
函數(shù)之后,原切片也能得到新元素;需將函數(shù)的參數(shù)類(lèi)型由 切片類(lèi)型 改成 切片指針類(lèi)型。
通過(guò)例子來(lái)感受一下上面結(jié)論的由來(lái):
package main import "fmt" func main() { s := []int{0, 2, 3} fmt.Printf("切片的長(zhǎng)度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 3 3 [0, 2, 3] sliceOperation(s) fmt.Printf("切片的長(zhǎng)度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 3 3 [1, 2, 3] } func sliceOperation(s []int) { s[0] = 1 s = append(s, 4) fmt.Printf("切片的長(zhǎng)度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 4 6 [1, 2, 3] }
首先定義并初始化切片 s
,切片里有三個(gè)元素;
調(diào)用 sliceOperation
函數(shù),將切片作為參數(shù)進(jìn)行傳遞;
在函數(shù)里修改切片的第一個(gè)元素的值為 1
,然后通過(guò) append
函數(shù)插入元素 4
,此時(shí)函數(shù)里的切片 由于容量不夠,s
的容量被擴(kuò)大了,變成 原 cap * 2 = 3 * 2 = 6
;
打印結(jié)果已注釋在代碼里,通過(guò)打印結(jié)果可知:
- 在函數(shù)里修改切片的第一個(gè)元素的值,原切片元素的值也會(huì)改變;
- 在函數(shù)里通過(guò)
append
函數(shù),向切片追加元素 4,原切片并沒(méi)有此元素; - 函數(shù)里的切片擴(kuò)容了,原切片卻沒(méi)有。
由于切片是引用類(lèi)型,因此在函數(shù)修改切片元素的值,原切片的元素值也會(huì)改變。
有的人可能會(huì)產(chǎn)生以下兩個(gè)疑問(wèn):
1、既然切片是引用類(lèi)型,為什么通過(guò) append
追加元素,原切片 s
卻沒(méi)有新元素?
2、為什么函數(shù)里的切片擴(kuò)容了,原切片卻沒(méi)有?
在探究這兩個(gè)問(wèn)題之前,我們需要了解切片的數(shù)據(jù)結(jié)構(gòu):
type slice struct { array unsafe.Pointer len int cap int }
切片包含三個(gè)字段:array
(指針類(lèi)型,指向一個(gè)數(shù)組)、len
(切片的長(zhǎng)度)、cap
(切片的容量)。
知道了切片的數(shù)據(jù)結(jié)構(gòu),我們通過(guò)圖片來(lái)直觀地看看切片 s
:
切片 s
沒(méi)有被修改之前,在內(nèi)存中是以上圖所描述的形式存在,array
指針變量指向數(shù)組 [0, 2, 3]
,長(zhǎng)度為 3
,容量為 3
。
在執(zhí)行 sliceOperation
函數(shù)之后,原切片 s
和 sliceOperation
函數(shù)里的切片 s
如上圖所示。
通過(guò)上上圖和上圖對(duì)比可知,底層數(shù)組 [0, 2, 3]
的第一個(gè)元素的值被修改為 1
,然后追加元素 4
,此時(shí)函數(shù)里的切片發(fā)生變化,長(zhǎng)度 3 → 4
,容量 3 → 6
變成原來(lái)的兩倍,底層數(shù)組的長(zhǎng)度也由 3 → 6
。
由于原切片s的長(zhǎng)度為3,array
指針?biāo)赶虻膮^(qū)域只有 [1, 2, 3]
,這也是為什么在函數(shù)里新增了 元素 4
,在原切片 s
里看不到的原因。
第一個(gè)問(wèn)題解決了,我們來(lái)思考第二個(gè)問(wèn)題的原因:
在 Go
中,函數(shù) / 方法的參數(shù)傳遞方式為值傳遞,main
函數(shù)將 s
傳遞過(guò)來(lái),sliceOperation
函數(shù)用 s
去接收,此時(shí)的s為新的切片,只不過(guò)它們所指向的底層數(shù)組為同一個(gè),長(zhǎng)度和容量也是一樣。而擴(kuò)容操作是在新切片上進(jìn)行的,因此原切片不受影響。
slice 通過(guò) make 函數(shù)初始化,后續(xù)操作不當(dāng)所造成的陷阱
使用 make
函數(shù)初始化切片后,如果在后續(xù)操作中沒(méi)有正確處理切片長(zhǎng)度,容易造成以下陷阱:
越界訪問(wèn):如果訪問(wèn)超出切片實(shí)際長(zhǎng)度的索引,則會(huì)導(dǎo)致 index out of range
錯(cuò)誤,例如:
func main() { s := make([]int, 0, 4) s[0] = 1 // panic: runtime error: index out of range [0] with length 0 }
通過(guò) make([]int, 0, 4)
初始化切片,雖說(shuō)容量為 4,但是長(zhǎng)度為 0,如果通過(guò)索引去賦值,會(huì)發(fā)生panic;為避免 panic
,可以通過(guò) s := make([]int, 4)
或 s := make([]int, 4, 4)
對(duì)切片進(jìn)行初始化。
切片初始化不當(dāng),通過(guò) append
函數(shù)追加新元素的位置可能于預(yù)料之外
func main() { s := make([]int, 4) s = append(s, 1) fmt.Println(s[0]) // 0 s2 := make([]int, 0, 4) s2 = append(s2, 1) fmt.Println(s2[0]) // 1 }
通過(guò)打印結(jié)果可知,對(duì)于切片 s
,元素 1
沒(méi)有被放置在第一個(gè)位置,而對(duì)于切片 s2
,元素 1
被放置在切片的第一個(gè)位置。這是因?yàn)橥ㄟ^(guò) make([]int, 4)
和 make([]int, 0, 4)
初始化切片,底層所指向的數(shù)組的值是不一樣的:
- 第一種初始化的方式,切片的長(zhǎng)度和容量都為
4
,底層所指向的數(shù)組長(zhǎng)度也是4
,數(shù)組的值為[0, 0, 0, 0]
,每個(gè)位置的元素被賦值為零值,s = append(s, 1)
執(zhí)行后,s
切片的值為[0, 0, 0, 0, 1]
; - 第二種初始化的方式,切片的長(zhǎng)度為
0
,容量為4
,底層所指向的數(shù)組長(zhǎng)度為0
,數(shù)組的值為[]
,s2 = append(s2, 1)
執(zhí)行后,s2
切片的值為[1]
; - 通過(guò)
append
向切片追加元素,會(huì)執(zhí)行尾插操作。如果我們需要初始化一個(gè)空切片,然后從第一個(gè)位置開(kāi)始插入元素,需要避免make([]int, 4)
這種初始化的方式,否則添加的結(jié)果會(huì)在預(yù)料之外。
性能陷阱
內(nèi)存泄露
內(nèi)存泄露是指程序分配內(nèi)存后不再使用該內(nèi)存,但未將其釋放,導(dǎo)致內(nèi)存資源被浪費(fèi)。
切片引用切片場(chǎng)景:如果一個(gè)切片有大量的元素,而它只有少部分元素被引用,其他元素存在于內(nèi)存中,但是沒(méi)有被使用,則會(huì)造成內(nèi)存泄露。代碼示例如下:
var s []int func main() { sliceOperation() fmt.Println(s) } func sliceOperation() { a := make([]int, 0, 10) a = append(a, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) s = a[0:4] }
上述代碼中,切片 a
的元素有 10
個(gè),而切片 s
是基于 a
創(chuàng)建的,它底層所指向的數(shù)組與 a
所指向的數(shù)組是同一個(gè),只不過(guò)范圍為前四個(gè)元素,而后六個(gè)元素依然存在于內(nèi)存中,卻沒(méi)有被使用,這樣會(huì)造成內(nèi)存泄露。為了避免內(nèi)存泄露,我們可以對(duì)代碼進(jìn)行改造: s = a[0:4]
→ s = append(s, a[0:4]...)
,通過(guò) append
進(jìn)行元素追加,這樣切片 a
底層的數(shù)組沒(méi)有被引用,后面會(huì)被 gc
。
擴(kuò)容
擴(kuò)容陷阱在前面的例子也提到過(guò),通過(guò) append
方法,對(duì)切片執(zhí)行追加元素的操作,可能會(huì)引起切片擴(kuò)容,導(dǎo)致內(nèi)存分配的問(wèn)題。
func main() { s := make([]int, 0, 4) fmt.Printf("切片的長(zhǎng)度:%d, 切片的容量:%d\n", len(s), cap(s)) // 4 4 s = append(s, 1, 2, 3, 4, 5) fmt.Printf("切片的長(zhǎng)度:%d, 切片的容量:%d\n", len(s), cap(s)) // 5 8 }
切片擴(kuò)容,可能會(huì)對(duì)程序的性能 造成影響;為避免此情況的發(fā)生,應(yīng)該根據(jù)使用場(chǎng)景,估算切片的容量,指定一個(gè)合理 cap
參數(shù)。
到此這篇關(guān)于Go語(yǔ)言中Slice常見(jiàn)陷阱與避免方法詳解的文章就介紹到這了,更多相關(guān)Go語(yǔ)言Slice內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go項(xiàng)目配置管理神器之viper的介紹與使用詳解
viper是一個(gè)完整的?Go應(yīng)用程序的配置解決方案,它被設(shè)計(jì)為在應(yīng)用程序中工作,并能處理所有類(lèi)型的配置需求和格式,下面這篇文章主要給大家介紹了關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用,需要的朋友可以參考下2023-02-02Go語(yǔ)言學(xué)習(xí)之JSON編碼解析與使用
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中JSON編碼的解析與使用已經(jīng)JSON與Map、結(jié)構(gòu)體的互相轉(zhuǎn)化,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-02-02Go語(yǔ)言實(shí)現(xiàn)websocket推送程序
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)websocket推送程序,WebSocket是基于TCP的一個(gè)雙向傳輸數(shù)據(jù)的協(xié)議,和HTTP協(xié)議一樣,是在應(yīng)用層的,他的出現(xiàn),是為了解決網(wǎng)頁(yè)進(jìn)行持久雙向傳輸數(shù)據(jù)的問(wèn)題2023-01-01go-micro開(kāi)發(fā)RPC服務(wù)以及運(yùn)行原理介紹
這篇文章介紹了go-micro開(kāi)發(fā)RPC服務(wù)的方法及其運(yùn)行原理,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Go 實(shí)現(xiàn)英尺和米的簡(jiǎn)單單位換算方式
這篇文章主要介紹了Go 實(shí)現(xiàn)英尺和米的簡(jiǎn)單單位換算方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04windows下使用vscode搭建golang環(huán)境并調(diào)試的過(guò)程
這篇文章主要介紹了在windows下使用vscode搭建golang環(huán)境并進(jìn)行調(diào)試,主要包括安裝方法及環(huán)境變量配置技巧,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09Go語(yǔ)言調(diào)用ffmpeg-api實(shí)現(xiàn)音頻重采樣
最近對(duì)golang處理音視頻很感興趣,對(duì)golang音視頻常用庫(kù)goav進(jìn)行了一番研究。自己寫(xiě)了一個(gè)wav轉(zhuǎn)采樣率的功能。給大家分享一下,中間遇到了不少坑,解決的過(guò)程中還是蠻有意思的,希望大家能喜歡2022-12-12