網站首頁 編程語言 正文
前言
Go
語言提供了很多方便的數據類型,其中包括 slice
。然而,由于 slice
的特殊性質,在使用過程中易犯一些錯誤,如果不注意,可能導致程序出現意外行為。本文將詳細介紹 使用 slice
時易犯的一些錯誤,幫助讀者更好的使用 Go
的 slice
,避免犯錯誤。
slice 作為函數 / 方法的參數進行傳遞的陷阱
slice
作為參數進行傳遞,有一些地方需要注意,先說結論:
1、在函數里修改切片元素的值,原切片的值也會被改變;
若想修改新切片的值,而不影響原切片的值,可以對原切片進行深拷貝:
通過 copy(dst, src []Type) int
函數將原切片的元素拷貝到新切片中:此函數在拷貝時,會基于兩個切片中,最小長度為基礎去拷貝,也就是初始化新切片時,長度必須大于等于原切片的長度。
2、在函數里通過 append
方法,對切片執行追加元素的操作,可能會引起切片擴容,導致內存分配的問題,可能會對程序的性能 造成影響;
為避免切片擴容,導致內存分配,對程序的性能造成影響,在初始化切片時,應該根據使用場景,指定一個合理 cap
參數。
3、在函數里通過 append
函數,對切片執行追加元素的操作,原切片里不存在新元素。
若想實現執行 append
函數之后,原切片也能得到新元素;需將函數的參數類型由 切片類型 改成 切片指針類型。
通過例子來感受一下上面結論的由來:
package main import "fmt" func main() { s := []int{0, 2, 3} fmt.Printf("切片的長度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 3 3 [0, 2, 3] sliceOperation(s) fmt.Printf("切片的長度:%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("切片的長度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 4 6 [1, 2, 3] }
首先定義并初始化切片 s
,切片里有三個元素;
調用 sliceOperation
函數,將切片作為參數進行傳遞;
在函數里修改切片的第一個元素的值為 1
,然后通過 append
函數插入元素 4
,此時函數里的切片 由于容量不夠,s
的容量被擴大了,變成 原 cap * 2 = 3 * 2 = 6
;
打印結果已注釋在代碼里,通過打印結果可知:
- 在函數里修改切片的第一個元素的值,原切片元素的值也會改變;
- 在函數里通過
append
函數,向切片追加元素 4,原切片并沒有此元素; - 函數里的切片擴容了,原切片卻沒有。
由于切片是引用類型,因此在函數修改切片元素的值,原切片的元素值也會改變。
有的人可能會產生以下兩個疑問:
1、既然切片是引用類型,為什么通過 append
追加元素,原切片 s
卻沒有新元素?
2、為什么函數里的切片擴容了,原切片卻沒有?
在探究這兩個問題之前,我們需要了解切片的數據結構:
type slice struct { array unsafe.Pointer len int cap int }
切片包含三個字段:array
(指針類型,指向一個數組)、len
(切片的長度)、cap
(切片的容量)。
知道了切片的數據結構,我們通過圖片來直觀地看看切片 s
:
切片 s
沒有被修改之前,在內存中是以上圖所描述的形式存在,array
指針變量指向數組 [0, 2, 3]
,長度為 3
,容量為 3
。
在執行 sliceOperation
函數之后,原切片 s
和 sliceOperation
函數里的切片 s
如上圖所示。
通過上上圖和上圖對比可知,底層數組 [0, 2, 3]
的第一個元素的值被修改為 1
,然后追加元素 4
,此時函數里的切片發生變化,長度 3 → 4
,容量 3 → 6
變成原來的兩倍,底層數組的長度也由 3 → 6
。
由于原切片s的長度為3,array
指針所指向的區域只有 [1, 2, 3]
,這也是為什么在函數里新增了 元素 4
,在原切片 s
里看不到的原因。
第一個問題解決了,我們來思考第二個問題的原因:
在 Go
中,函數 / 方法的參數傳遞方式為值傳遞,main
函數將 s
傳遞過來,sliceOperation
函數用 s
去接收,此時的s為新的切片,只不過它們所指向的底層數組為同一個,長度和容量也是一樣。而擴容操作是在新切片上進行的,因此原切片不受影響。
slice 通過 make 函數初始化,后續操作不當所造成的陷阱
使用 make
函數初始化切片后,如果在后續操作中沒有正確處理切片長度,容易造成以下陷阱:
越界訪問:如果訪問超出切片實際長度的索引,則會導致 index out of range
錯誤,例如:
func main() { s := make([]int, 0, 4) s[0] = 1 // panic: runtime error: index out of range [0] with length 0 }
通過 make([]int, 0, 4)
初始化切片,雖說容量為 4,但是長度為 0,如果通過索引去賦值,會發生panic;為避免 panic
,可以通過 s := make([]int, 4)
或 s := make([]int, 4, 4)
對切片進行初始化。
切片初始化不當,通過 append
函數追加新元素的位置可能于預料之外
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 }
通過打印結果可知,對于切片 s
,元素 1
沒有被放置在第一個位置,而對于切片 s2
,元素 1
被放置在切片的第一個位置。這是因為通過 make([]int, 4)
和 make([]int, 0, 4)
初始化切片,底層所指向的數組的值是不一樣的:
- 第一種初始化的方式,切片的長度和容量都為
4
,底層所指向的數組長度也是4
,數組的值為[0, 0, 0, 0]
,每個位置的元素被賦值為零值,s = append(s, 1)
執行后,s
切片的值為[0, 0, 0, 0, 1]
; - 第二種初始化的方式,切片的長度為
0
,容量為4
,底層所指向的數組長度為0
,數組的值為[]
,s2 = append(s2, 1)
執行后,s2
切片的值為[1]
; - 通過
append
向切片追加元素,會執行尾插操作。如果我們需要初始化一個空切片,然后從第一個位置開始插入元素,需要避免make([]int, 4)
這種初始化的方式,否則添加的結果會在預料之外。
性能陷阱
內存泄露
內存泄露是指程序分配內存后不再使用該內存,但未將其釋放,導致內存資源被浪費。
切片引用切片場景:如果一個切片有大量的元素,而它只有少部分元素被引用,其他元素存在于內存中,但是沒有被使用,則會造成內存泄露。代碼示例如下:
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
個,而切片 s
是基于 a
創建的,它底層所指向的數組與 a
所指向的數組是同一個,只不過范圍為前四個元素,而后六個元素依然存在于內存中,卻沒有被使用,這樣會造成內存泄露。為了避免內存泄露,我們可以對代碼進行改造: s = a[0:4]
→ s = append(s, a[0:4]...)
,通過 append
進行元素追加,這樣切片 a
底層的數組沒有被引用,后面會被 gc
。
擴容
擴容陷阱在前面的例子也提到過,通過?append
?方法,對切片執行追加元素的操作,可能會引起切片擴容,導致內存分配的問題。
func main() { s := make([]int, 0, 4) fmt.Printf("切片的長度:%d, 切片的容量:%d\n", len(s), cap(s)) // 4 4 s = append(s, 1, 2, 3, 4, 5) fmt.Printf("切片的長度:%d, 切片的容量:%d\n", len(s), cap(s)) // 5 8 }
切片擴容,可能會對程序的性能 造成影響;為避免此情況的發生,應該根據使用場景,估算切片的容量,指定一個合理?cap
?參數。
原文鏈接:https://juejin.cn/post/7199609052124495932
相關推薦
- 2022-08-03 Python?權限控制模塊?Casbin_python
- 2022-03-16 詳解C語言在STM32中的內存分配問題_C 語言
- 2024-07-18 redisson分布式鎖中waittime的設置
- 2023-04-20 navicat 連接 mongodb 報錯[13][Unauthorized] command li
- 2022-05-10 Element-ui 中的 Select 組件用(深度)選擇器修改默認樣式不生效的問題及如何使用 p
- 2022-05-05 Entity?Framework使用Fluent?API配置案例_實用技巧
- 2022-07-25 Python實現文件及文件夾操作大全_python
- 2022-07-10 同時啟動兩個項目,產生的跨域問題
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支