數組是不可以調整大小的,所以數組使用起來不夠靈活。因此GO語言使用了Slice(切片),切片是數組建立的一種方便、靈活且功能強大的包裝。
正因爲數組不可以調整大小,使用不夠靈活這個缺點,所以在GO語言開發中,用的更多的是Silce,下面來進入Slice的學習。
Slice的定義
先來看一下slice的一個簡單的定義
func main() {
arr := [...]int{0,1,2,3,4,5,6}
s := arr[2:6]
fmt.Println(s)
}
看一下輸出結果
[2 3 4 5]
這邊解釋一下,切片賦值的那一句話,[2:6]的意思是,從arr數組的下標2開始,到arr數組的下標(6-1)也就是5結束,注意這裏是一個半開半閉的區間,左邊是閉,右邊是開,所以打印slice的輸出結果也就是2,3,4,5。
下面來看一下一些靈活的定義
func main() {
arr := [...]int{0,1,2,3,4,5,6}
s := arr[2:6] //從下標2到下標5
s1 := arr[2:] //從下標2一直到最後,也就是下標6
s2 := arr[:6] //從下標0到下標5
s3 := arr[:] //等價於整個數組
fmt.Println(s)
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
}
輸出結果
[2 3 4 5]
[2 3 4 5 6]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]
另外,在Slice之上還可以創建Slice
func main() {
arr := []int{0,1,2,3,4,5,6}
s1 := arr[:]
fmt.Println(s1)
s1 = s1[2:]
fmt.Println(s1)
s1 = arr[:4]
fmt.Println(s1)
}
輸出結果
[0 1 2 3 4 5 6]
[2 3 4 5 6]
[0 1 2 3]
另一種創建方式
func main() {
s1 := []int{0,1,2,3,4,5,6}
fmt.Println(s1)
}
上面的創建方式相當於下面這樣,s1其實就是整個數組的映射,只是在創建數組的同時,返回的是對整個數組的Slice
func main() {
arr := []int{0,1,2,3,4,5,6}
s1 := arr[:]
fmt.Println(s1)
}
Slice的原理
Slice本身其實不擁有任何數據,它其實相當於一個視圖,映射了數組的一部分,對視圖(Slice)的修改會直接映射到原來的數組上。
所以說,要有Slice,必須得先有一個數組,因爲Slice只是數組的一個反映而已.
來看一個簡單的例子
//注意函數參數寫成[],就表示這個參數是一個Slice,而不是數組
func modifySlice(s []int){
s[0] = 100
}
func main() {
arr := [...]int{0,1,2,3,4,5,6}
s := arr[:6] //從下標2到下標5
fmt.Println(s)
modifySlice(s)
fmt.Print("After modifySlice:")
fmt.Println(s)
fmt.Println(arr)
}
輸出結果
[0 1 2 3 4 5]
After modifySlice:[100 1 2 3 4 5]
[100 1 2 3 4 5 6]
我們在函數內部將Slice修改了,修改了之後,因爲Slice是數組的一層視圖,因此實際上也就是改了數組本身,我們在最後一行打印了這個數組,發現數組本身也被改變了,另外數組改了之後,Slice也就隨之改變了。
所以,如果函數的參數傳的是一個Slice,那麼這就相當於一個引用傳遞,對Slice的修改會改變數組的本身。這樣我們就不用像之前那樣,傳一個數組的指針進去了。
當一個數組的多個Slice同時修改該數組時,會怎麼樣?
func main() {
arr := [...]int{0,1,2,3,4,5,6}
s1 := arr[:]
s2 := arr[:]
s1[0] = 100
fmt.Println(arr)
s2[1] = 200
fmt.Println(arr)
}
輸出結果
[100 1 2 3 4 5 6]
[100 200 2 3 4 5 6]
所以,當多個Slice共享同一個數組的時候,每個Slice所做的修改都會映射在原數組中。
Slice的越界問題
先上代碼
func main() {
arr := []int{0,1,2,3,4,5,6}
s1 := arr[2:6]
s2 := s1[3:5]
fmt.Println(s1)
fmt.Println(s2)
}
s1我們知道,是[2,3,4,5],但是s2究竟是什麼呢?
它從s1的下標3開始,也就是5,到下標4結束,但是s1中沒有下標爲4的元素,那麼s2會不會報錯呢?如果不報錯,那麼輸出的是什麼呢?
輸出結果
[2 3 4 5]
[5 6]
注意,這裏我們就可以發現了。雖然在s1中沒有下標爲4的元素,但是這麼說,原數組中其實還是有這個元素的存在的。還是畫個圖來理解一下吧。
看上圖,s1雖然只是映射了[2,3,4,5]這四個元素,但是其實後面的6那個元素在數組底層還是存在的。因爲本身s1和s2都只是視圖而已,但不包含實際的數據,所以s2雖然超出了s1的界限,但是還是沒有超過原數組的界限。正因爲如此,這也是Slice的一個靈活之處,只要沒有超過原數組的界限,這就是可以的,不會報錯。
但是一旦超過了原數組的界限,就會報錯了,比如改一下數據
func main() {
arr := []int{0,1,2,3,4,5,6}
s1 := arr[2:6]
s2 := s1[3:6] //這裏從[3:5]改成了[3:6]
fmt.Println(s1)
fmt.Println(s2)
}
這樣就報錯了
panic: runtime error: slice bounds out of range
Slice的底層實現
有了上面的越界問題,再來看看Slice的底層實現究竟是怎麼樣的。
它內部實現了三個變量:ptr,len,capacity
- ptr表示Slice對應的數組的下標,如在之前的例子中,s1的ptr就是指向下標爲2的元素
- len表示這個Slice的長度,指在Slice定義時候給的大小
- capacity表示這個Slice的容量,也就是數組最後一個元素到ptr指向的元素的距離大小
如果學過C++的人很好理解,這和vector中的三個指針實現起來差不多,begin,end和last,下面來畫一張圖然後結合代碼來看看
我們以s1爲例
如上,ptr就是指向下標爲2的元素,len就是s1[2:6]這個Slice的長度,capacity就是數組最後一個元素的下標減到ptr指向的元素的下標。
對於s1而言,它有四個元素,下標分別爲0,1,2,3,那麼如果訪問s1[4]會出現什麼問題呢?按之前的理解,s1[4]在原數組上其實是指下標爲6的那個元素。
func main() {
arr := []int{0,1,2,3,4,5,6}
s1 := arr[2:6]
s2 := s1[3:5]
s1[4] = 4 //這裏訪問s1[4]
fmt.Println(s1)
fmt.Println(s2)
}
編譯器會報錯
panic: runtime error: index out of range
所以,這裏就知道了,如果要訪問這個Slice,那麼下標最多不可以超過Slice的 len - 1 這個大小,這和我們平時的理解是一樣的,有4個元素,那麼最高下標是3,訪問下標爲4就是越界了。
所以,要將這部分和上面定義s2時做區別,在訪問Slice的時候,下標不可以超過 len - 1;但是用一個Slice定義另一個Slice的時候,Slice是可以進行擴展的,只要不超過原數組的界限,就可以定義成功。
另外,Slice不可以向前擴展,所以用一些Slice定義另一個Slice的時候,新定義的Slice的ptr不可能比原來的Slice的ptr要小。
總結一下Slice使用的注意點
- Slice不可以向前擴展
- s[i]使用時不可以超越len(s)
- Slice向後擴展不可以超過底層數組cap(s)