Golang 字符串切片與 Python 列表的不同
最近在粉絲交流群里面看到不少學 Python 的同學都在學習 Golang,那么今天我們來看一個非常基礎的數據結構:Python中的列表和 Golang 中的切片(Slice)。
這兩個數據結構從形式上來說,非常相似。我們今天來對比一個只包含字符串的列表和一個字符串切片。
相同點
在 Python 里面,我們定義一個有初始值的字符串列表:
- a = ['kingname', 'pm', 'xxx']
在 Golang 里面,我們定義一個有初始值的字符串切片:
- a := []string{"kingname", "pm", "xxx"}
接下來,我們分別往字符串列表和字符串切片末尾增加幾個元素:
- a.append("address")
- a.append("shanghai")
在 Golang 里面:
- a = append(a, "address")
- a = append(a, "shanghai")
我們也可以賦值給其他的變量,看看修改一個,另一個是否會發生修改:
- b = a
- a[0] = 'superman'
- print(b)
運行效果如下圖所示:
我們再來看看在 Golang 的效果:
- b := a
- a[0] = "superman"
- fmt.Println(b)
運行效果如下圖所示:
那么,我們是不是可以說,Golang 的切片就相當于 Python 里面元素數據類型相同的列表?
不同點
現在,我們再往列表和字符串切片里面各加一個元素,來看看運行效果:
在 Python 里面,運行效果如下圖所示:
進一步實驗你會發現,a 和 b 兩個列表是完全一樣的,只要修改任何一個列表,另一個都會隨之發生變化。
但是 Golang 里面并不是這樣,如下圖所示:
你修改任何一個切片,另一個切片都不會改變。
看到這里,你可能會覺得 Golang 里面,是不是append添加新的數據,每次都會生成新的切片,所以才導致添加數據以后兩個切片就不一樣了。
但實際上并不是這樣,我們用另外一種初始化切片的方式來做一個測試:
在這個例子里面,我生成了一個長度為5,容量為20的字符串切片。根據第15-19行的運行結果可以看到,此時,無論是根據索引修改里面的元素,還是使用 append 添加新的元素,兩個切片的變化都相同。如果我們把切片的容量調小,調整到6,再看看效果:
從這里可以看到,b 跟著 a 變了半截。a 新增的test字符串同時也能在 b 里面找到。但是 a里面新增的abcde卻沒有出現在 b 中。并且對a[0]的修改,也沒有出現在 b 中。
原因
Golang 的切片之所以會出現這個現象,這需要從數組與切片的區別來說起。在 Golang 里面,字符串數組和字符串切片非常像,但他們有一個根本的區別,就是數組是需要一開始就聲明長度的,并且不能擴容。而切片不需要聲明長度,所以:
- [5]string{"xx", "yy"} // 這是長度為5的字符串數組
- []string{"xx", "yy"} // 字符串切片
而切片底層依然是數組,切片有一個容量的概念,指的就是它底層的數組的長度。如果切片中的數據數量等于了切片的容量,那么下一次再添加一個新的數據的時候,切片底層就會創建一個原來長度2倍(數據量小于1024的時候是2倍,大于1024的時候是1.25倍)的數組,然后把已有數據按順序拷貝進去,接著再插入新的數據。
所以,回到上面的代碼。當我們使用a := make([]string, 5, 6)創建一個容量為6的字符串切片的時候,它底層會初始化一個長度為6的字符串數組。當代碼執行到b := a[0: 6]的時候,雖然這里的 b 是另外一個切片,它跟 a 擁有不同的內存地址,但他們共用了同一個底層數組。只要數據小于6,那么對其中一個切片的數據進行修改,本質上就是對它底層數組的修改,而另一個切片也使用這個數組,所以也能看到這個修改。
但是當a數據容量超過6以后,a 切片底層會重新生成一個長度為12的數組,并把原有的老數據都拷貝到新的數組里面,接下來的所有修改都是對這個新的數組進行修改。而此時 b 切片底層還是老的長度為6的數組,所以此時對 a 切片的修改就不會反映到 b 上面了。