網站首頁 編程語言 正文
正文
slice
(切片)是 go 里面非常常用的一種數據結構,它代表了一個變長的序列,序列中的每個元素都有相同的數據類型。 一個 slice
類型一般寫作 []T
,其中 T
代表 slice
中元素的類型;slice
的語法和數組很像,但是 slice
沒有固定長度。
數組和切片的區別
數組有確定的長度,而切片的長度不固定,并且可以自動擴容。
數組的定義
go 中定義數組的方式有如下兩種:
- 指定長度:
arr := [3]int{1, 2, 3}
- 不指定長度,由編譯器推導出數組的長度:
arr := [...]{1, 2, 3}
上面這兩種定義方式都定義了一個長度為 3 的數組。正如我們所見,長度是數組的一部分,定義數組的時候長度已經確定下來了。
切片的定義
切片的定義方式跟數組很像,只不過定義切片的時候不用指定長度:
s := []int{1, 2, 3}
在上面定義切片的代碼中,我們可以看到其實跟數組唯一的區別就是少了個長度。 那其實我們可以把切片看作是一個無限長度的數組。 當然,實際上它并不是無限的,它只是在切片容納不下新的元素的時候,會自動進行擴容,從而可以容納更多的元素。
數組和切片的相似之處
正如我們上面看到的那樣,數組和切片兩者其實非常相似,在實際使用中,它們也是有些類似的。
比如,通過下標來訪問元素:
arr := [3]int{1, 2, 3} // 通過下標訪問 fmt.Println(arr[1]) // 2 s := []int{1, 2, 3} // 通過下標訪問 fmt.Println(s[1]) // 2
數組的局限
我們知道了,數組的長度是固定的,這也就意味著如果我們想往數組里面增加一個元素會比較麻煩, 我們需要新建一個更大的數組,然后將舊的數據復制過去,然后將新的元素寫進去,如:
// 往數組 arr 增加一個元素:4 arr := [3]int{1, 2, 3} // 新建一個更大容量的數組 var arr1 [4]int // 復制舊數組的數據 for i := 0; i < len(arr); i++ { arr1[i] = arr[i] } // 加入新的元素:4 arr1[3] = 4 fmt.Println(arr1)
這樣一來就非常的繁瑣,如果我們使用切片,就可以省去這些步驟:
// 定義一個長度為 3 的數組 arr := [3]int{1, 2, 3} // 從數組創建一個切片 s := arr[:] // 增加一個元素 s = append(s, 4) fmt.Println(s)
因為數組固定長度的缺點,實際使用中切片會使用得更加普遍。
重新理解 slice
在開始之前,我們來看看 slice
這個單詞的意思:作為名詞,slice
的意思有 片;部分;(切下的食物)薄片;,作為動詞,slice
的意思有 切;把…切成(薄)片; 的意思。 從這個角度出發,我們可以把 slice
理解為從某個數組上 切下來的一部分(從這個角度看,slice
這個命名非常的形象)。我們可以看看下圖:
在這個圖中,A
是一個保存了數字 1~7
的 slice
,B
是從 A
中 切下來的一部分,而 B
只包含了 A
中的一部分數據。 我們可以把 B
理解為 A
的一個 視圖,B
中的數據是 A
中的數據的一個 引用,而不是 A
中數據的一個 拷貝 (也就是說,我們修改 B
的時候,A
中的數據也會被修改,當然會有例外,那就是 B
發生擴容的時候,再去修改 B
的話就影響不了 A
了)。
slice 的內存布局
現在假設我們有如下代碼:
// 創建一個切片,長度為 3,容量為 7 var s = make([]int, 3, 7) s[0] = 1 s[1] = 2 s[2] = 3 fmt.Println(s)
對應的內存布局如下:
說明:
-
slice
底層其實也是數組,但是除了數組之外,還有兩個字段記錄切片的長度和容量,分別是len
和cap
。 - 上圖中,
slice
中的array
就是切片的底層數組,因為它的長度不是固定的,所以使用了指針來保存,指向了另外一片內存區域。 -
len
表明了切片的長度,切片的長度也就是我們可以操作的下標,上面的切片長度為3
,這也就意味著我們切片可以操作的下標范圍是0~2
。超出這個范圍的下標會報錯。 -
cap
表明了切片的容量,也就是切片擴容之前可以容納的元素個數。
切片容量存在的意義
對于我們日常開發來說,slice
的容量其實大多數時候不是我們需要關注的點,而且由于容量的存在,也給開發者帶來了一定的困惑。 那么容量存在的意義是什么呢?意義就在于避免內存的頻繁分配帶來的性能下降(容量也就是提前分配的內存大小)。
比如,假如我們有一個切片,然后我們知道需要往它里面存放 1w 個元素, 如果我們不指定容量的話,那么切片就會在它存放不下新的元素的時候進行擴容, 這樣一來,可能在我們存放這 1w 個元素的時候需要進行多次擴容, 這也就意味著需要進行多次的內存分配。這樣就會影響應用的性能。
我們可以通過下面的例子來簡單了解一下:
// Benchmark1-20 100000000 11.68 ns/op func Benchmark1(b *testing.B) { var s []int for i := 0; i < b.N; i++ { s = append(s, 1) } } // Benchmark2-20 134283985 7.482 ns/op func Benchmark2(b *testing.B) { var s []int = make([]int, 10, 100000000) for i := 0; i < b.N; i++ { s = append(s, 1) } }
在第一個例子中,沒有給 slice
設置容量,這樣它就只會在切片容納不下新元素的時候才會進行擴容,這樣就會需要進行多次擴容。 而第二個例子中,我們先給 slice
設置了一個足夠大的容量,那么它就不需要進行頻繁擴容了。
最終我們發現,在給切片提前設置容量的情況下,會有一定的性能提升。
切片常用操作
創建切片
我們可以從數組或切片生成新的切片:
注意:生成的切片不包含 end
。
target[start:end]
說明:
-
target
表示目標數組或者切片 -
start
對應目標對象的起始索引(包含) -
end
對應目標對象的結束索引(不包含)
如:
s := []int{1, 2, 3} s1 := s[1:2] // 包含下標 1,不包含下標 2 fmt.Println(s1) // [2] arr := [3]int{1, 2, 3} s2 := arr[1:2] fmt.Println(s2) // [2]
在這種初始化方式中,我們可以省略 start
:
arr := [3]int{1, 2, 3} fmt.Println(arr[:2]) // [1, 2]
省略 start
的情況下,就是從 target
的第一個元素開始。
我們也可以省略 end
:
arr := [3]int{1, 2, 3} fmt.Println(arr[1:]) // [2, 3]
省略 end
的情況下,就是從 start
索引處的元素開始直到 target
的最后一個元素處。
除此之外,我們還可以指定新的切片的容量,通過如下這種方式:
target[start:end:cap]
例子:
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} s := arr[1:4:5] fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4
往切片中添加元素
我們前面說過了,如果我們想往數組里面增加元素,那么我們必須開辟新的內存,將舊的數組復制過去,然后才能將新的元素加入進去。
但是切片就相對簡單,我們可以使用 append
這個內置函數來往切片中加入新的元素:
var a []int a = append(a, 1) // 追加1個元素 a = append(a, 1, 2, 3) // 追加多個元素 a = append(a, []int{1,2,3}...) // 追加一個切片
切片復制
go 有一個內置函數 copy
可以將一個切片的內容復制到另外一個切片中:
copy(dst, src []int)
第一個參數 dst
是目標切片,第二個參數 src
是源切片,調用 copy
的時候會把 src
的內容復制到 dst
中。
示例:
var a []int var b []int = []int{1, 2, 3} // a 的容量為 0,容納不下任何元素 copy(a, b) fmt.Println(a) // [] a = make([]int, 3, 3) // 給 a 分配內存 copy(a, b) fmt.Println(a) // [1 2 3]
需要注意的是,如果 dst
的長度比 src
的長度小,那么只會截取 src
的前面一部分。
從切片刪除元素
雖然我們往切片追加元素的操作挺方便的,但是要從切片刪除元素就相對麻煩一些了。go 語言本身沒有提供從切片刪除元素的方法。 如果我們要刪除切片中的元素,只有構建出一個新的切片:
對應代碼:
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } fmt.Println(a) // [1 2 3 4 5 6 7] var b []int b = append(b, a[:2]...) // [1 2] b = append(b, a[5:]...) // [1 2 6 7] fmt.Println(b) // [1 2 6 7]
在這個例子中,我們想從 a
中刪除 3、4、5
這三個元素,也就是下標 2~4
的元素, 我們的做法是,新建了一個新的切片,然后將 3
前面的元素加入到這個新的切片中, 再將 5
后面的元素加入到這個新切片中。
最終得到的切片就是刪除了 3、4、5
三個元素之后的切片了。
切片的容量到底是多少?
假設我們有如下代碼:
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } // [1 2 3 4 5 6 7] fmt.Println(a) s1 := a[:3] // [1 2 3] 3 7 fmt.Println(s1, len(s1), cap(s1)) s2 := a[4:6] // [5 6] 2 3 fmt.Println(s2, len(s2), cap(s2))
s1
和 s2
可以用下圖表示:
-
s1
只能訪問array
的前三個元素,s2
只能訪問5
和6
這兩個元素。 -
s1
的容量是 7(底層數組的長度) -
s2
的容量是 3,從5
所在的索引處直到底層數組的末尾。
對于 s1
和 s2
,我們都沒有指定它的容量,但是我們打印發現它們都有容量, 其實在切片中,我們從切片中生成一個新的切片的時候,如果我們不指定容量, 那新切片的容量就是 s[start:end]
中的 start
直到底層數組的最后一個元素的長度。
切片可以共享底層數組
切片最需要注意的點是,當我們從一個切片中創建新的切片的時候,兩者會共享同一個底層數組, 如上圖的那樣,s1
和 s2
都引用了同一個底層的數組不同的索引, s1
引用了底層數組的 0~2
下標范圍,s2
引用了底層數組 4~5
下標范圍。
這意味著,當我們修改 s1
或 s2
的時候,原來的切片 a
也會發生改變:
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } // [1 2 3 4 5 6 7] fmt.Println(a) s1 := a[:3] // [1 2 3] fmt.Println(s1) s1[1] = 100 // [1 100 3 4 5 6 7] fmt.Println(a) // [1 100 3] fmt.Println(s1)
在上面的例子中,s1
這個切片引用了和 a
一樣的底層數組, 然后在我們修改 s1
的時候,a
也發生了改變。
切片擴容不會影響原切片
上一小節我們說了,切片可以共享底層數組。但是如果切片擴容的話,那就是一個全新的切片了。
var a = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) // a 容納不下新的元素了,會進行擴容 b := append(a, 4) // [1 2 3 4] 4 6 fmt.Println(b, len(b), cap(b)) b[1] = 100 // [1 2 3] fmt.Println(a) // [1 100 3 4] fmt.Println(b)
在上面這個例子中,a
是一個長度和容量都是 3
的切片,這也就意味著,這個切片已經滿了。 在這種情況下,我們再往其中追加元素的時候,就會進行擴容,生成一個新的切片。 因此,我們可以看到,我們修改了 b
的時候,并沒有影響到 a
。
下面的例子就不一樣了:
// 長度為 2,容量為 3 var a = make([]int, 2, 3) a[0] = 1 a[1] = 2 // [1 2] 2 3 fmt.Println(a, len(a), cap(a)) // a 還可以容納新的元素,不用擴容 b := append(a, 4) // [1 2 4] 3 3 fmt.Println(b, len(b), cap(b)) b[1] = 100 // [1 100] fmt.Println(a) // [1 100 4] fmt.Println(b)
在后面這個例子中,我們只是簡單地改了一下 a
初始化的方式,改成了只放入兩個元素,但是容量還是 3
, 在這種情況下,a
可以再容納一個元素,這樣在 b := append(a, 4)
的時候,創建的 b
底層的數組其實跟 a
的底層數組依然是一樣的。
所以,我們需要尤其注意代碼中作為切片的函數參數,如果我們希望在被調函數中修改了切片之后,在 caller 里面也能看到效果的話,最好是傳遞指針。
func test1(s []int) { s = append(s, 4) } func test2(s *[]int) { *s = append(*s, 4) } func TestSlice(t *testing.T) { var a = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) test1(a) // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) var b = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(b, len(b), cap(b)) test2(&b) // [1 2 3 4] 4 6 fmt.Println(b, len(b), cap(b)) }
在上面的例子中,test1
接收的是值參數,所以在 test1
中切片發生擴容的時候,TestSlice
里面的 a
還是沒有發生改變。 而 test2
接收的是指針參數,所以在 test2
中發生切片擴容的時候,TestSlice
里面的 b
也發生了改變。
總結
- 數組跟切片的使用上有點類似,但是數組代表的是有固定長度的數據序列,而切片代表的是沒有固定長度的數據序列。
- 數組的長度是類型的一部分,有兩種定義數組的方式:
[2]int{1, 2}
、[...]int{1, 2}
。 - 數組跟切片都可以通過下標來訪問其中的元素,可以訪問的下標范圍都是
0 ~ len(x)-1
,x
表示的是數組或者切片。 - 數組無法追加新的元素,切片可以追加任意數量的元素。
-
slice
的數據結構里面包含了:array
底層數組指針、len
切片長度、cap
切片容量。 - 創建切片的時候,指定一個合適的容量可以減少內存分配的次數,從而在一定程度上提高程序性能。
- 我們可以從數組或者切片創建一個新的切片:
array[1:3]
或者slice[1:3]
。 - 使用
append
內置函數可以往切片中添加新的元素。 - 使用
copy
內置函數可以將一個切片的內容復制到另外一個切片中。 - 切片刪除元素沒有好的辦法,只能截取被刪除元素前后的數據,然后復制到一個新的切片中。
- 假設我們通過
slice[start:end]
的方式從切片中創建一個新的切片,那么這個新的切片的容量是cap(slice) - start
,也就是,從start
到底層數組最后一個元素的長度。 - 使用切片的時候需要注意:切片之間會共享底層數組,其中一個切片修改了切片的元素的時候,也會反映到其他切片上。
- 函數調用的時候,如果被調函數內發生擴容,調用者是無法知道的。如果我們不想錯過在被調函數內切片的變化,我們可以傳遞指針作為參數。
原文鏈接:https://juejin.cn/post/7179159979193008188
相關推薦
- 2022-11-18 Python實現常見數據格式轉換的方法詳解_python
- 2022-04-28 教你如何在Centos8-stream安裝PostgreSQL13_PostgreSQL
- 2022-04-15 python使用reportlab生成pdf實例_python
- 2022-05-19 nginx中封禁ip和允許內網ip訪問的實現示例_nginx
- 2022-01-30 tortoiseGit推送每次需要輸入密碼,解決方案
- 2022-06-08 Element-UI中selet下拉框無法回顯問題
- 2022-01-29 git 本地,遠程做了不同的修改,同步方法
- 2022-07-24 Golang實現文件夾的創建與刪除的方法詳解_Golang
- 最近更新
-
- 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同步修改后的遠程分支