崩潰 golang入坑系列


早上(11.30)收到郵件,Vultr東京機房網絡故障。當時搭建SS時,考慮到了機房故障。所以特意分出了日本和香港兩條線路。但千算萬算,忘記數據庫還在東京機房中。 現在網絡故障,SS服務器無法讀取數據庫中的賬號信息。於是乎,主備兩條線同時宕了。哭笑兩聲,沒錢去做異地雙活,訪問量又不大,就這么湊合吧。 我就不信Vultr網絡故障是大概率事件。如果很頻繁的出故障,用戶會用腳去投票的。

鐵路警察各管一段,Vultr的故障讓Vultr的運維去背鍋吧。我們言歸正傳,繼續聊Golang。

在<擼袖子>那節,我們提到了數組。 其中用了一個很少的篇幅說了一下數組的近親-切片。當時說到數組使用起來不方便,Golang提供了一種更方便的數組使用方式,就是切片。這節中,我們就先來說切片。

先來復習數組的概念,就是一組相同數據類型的集合。在說數組的時候,沒有什么動態擴展的方法。只能實現規定好這個數組有多少個元素,然后按照下標進行增刪改查。在真實環境中,有很大的局限性。 切片作為數組的近親,就彌補了這種缺陷。Golang所提供的切片,內置了很多方法來達到數組的動態擴容/縮容。

切片既然是數組的近親,那聲明方式基本上長得很像:

var name []type

name自然是切片名稱,type就是數據類型。僅此而已,就完成了一個切片的聲明。和數組的聲明最大的不同,就在於沒有長度限制。這是最常用的聲明方式,還有一種文縐縐的聲明方式,如下:

name := make([]type, length, capacity)

一瞅就有種學院派的作風。 多了兩個參數,length和capacity。這兩個概念理解不好,這就是一個大坑。Golang為了讓切片有很高的讀效率和又不容易出現指針越界,就創造了length和capacity兩個屬性。

capacity指的是此切片當前指向內存的數據大小。而length指的是當前切片的容量大小,從邏輯上來看,滿足這個條件: 0<=length<=capacity。

為什么說這是一個坑? 如果打算用切片操作目標內存的時候,必須小心別append過頭,否則就操作到新開啟的內存塊去了,也要小心別意外覆蓋了原slice的值。比如下面:

s := []int{10} //創建一個legnth = capacity = 1 的切片,並且初始化為10
s = append(s,11) //容量不夠,翻倍擴容。legnth = capacity = 2,現在是10,11
s = append(s,12) //容量又不夠了,再次擴容。legnth =3, capacity = 4,現在是10,11,12
x := append(s, 13) //容量夠了,不擴容。legnth = capacity = 4,現在是10,11,12,13
坑來了
y := append(s, 14) //容量夠了,不擴容。legnth = capacity = 4,現在是10,11,12,14

但如果你將上面代碼輸出一下,會看到x和y的值是相同的,都是10.11.12.14。這里面包含了切片的本質。 在Golang官方文檔中提及,對切片單獨進行append操作,並不會修改切片的內容(也就是單獨執行append(s,12)),往往需要將append后的數據重新賦值給源切片,也就是s = append(s,12),這是Golang官方所推薦的用法。 上面的例子中,在x和y那兩行,因為s沒有發生變化,length=3.所以后面append的值會直接添加到末尾。而返回的又都是同一塊內存地址,所以x和y其實指的是同一塊內存,因此其內部值也是相同的。 可以來一段代碼,把x,y和s的內存地址都輸出出來,結果就一目了然了。

如果嫌麻煩,那就用最簡單的方式:

var s []int
s = append(s,xxx)

而如果想輸出當前的length和capacity,就直接使用len()和cap()兩個內置函數。

數組允許存在空數據,切片也當然允許存在空切片。當直接聲明一個切片的時候,此時此刻,length = capacity = 0.

var numbers []int
此時此刻 len = 0 cap = 0 slice = []

又該如何判斷切片是否為空呢?可以使用length和capacity屬性,但不如使用nil來的簡單:

numbers == nil

true就表示是空切片,false表示是非空切片。

切片同數組相比,最靈活的方面在於切分子切片。例如可以在代碼中,根據業務需要,隨時將一個大切片取出任意元素組成一個子切片。看下面:

numbers := []int{0,1,2,3,4,5,6,7,8}
number2 := numbers[:2] // 從0到2,但不包括2.所以是0,1
number3 := numbers[2:5] // 從2到5,但不包括5.所以是2,3,4
number4 := numbers[5:] // 從5到末尾,包括末尾。

上面number2, number3和number4都是子切片,在使用時,需要記住這些子切片都是指向了源切片某一塊內存,什么意思?也就是說源切片元素發生了變化,那么子切片也會發生變化。不信? 在上面代碼中聲明子切片后,任意修改numbers的元素,在看看結果。

如果不想受源切片影響怎么辦?使用copy()函數。顧名思義,也就是把重新創建一個切片,自立山頭唄。

number5 := make([]int, 2)
copy(number5, numbers[:2])

輸出地址之后,就可以看到兩者已經完全脫離父子關系,想干嘛就干嘛去吧。

說到最后,需要看一下切片的數據結構了。 我想看到數據結構,上面那些所謂的坑應該就能看明白了。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
src/runtime/slice.go

可以看到slice,包含一個指針,一個len變量和一個cap變量。當需要獲取length和capacity時,是直接讀取的len和cap變量值,不需要再遍歷一遍,所以獲取長度和容量效率非常高。 而array指向了一塊內存,進行append操作時,如果len == cap,則擴容。如果len < cap,那么就是array[len+1]操作。因為golang默認都是值傳遞,雖然len已經變成len+1了,但原始的slice的len仍然沒有變。因此golang才建議,用源切片來接受返回值,這樣源切片的len和cap就會同步發生變化。

說實話,這部分腦子里面清楚,但用文字表述的效果欠佳。所以遇到切片時刻記住,用源切片來接受返回值。如果需要子切片,首要需要考慮,是不是需要用copy來復制生成。

轉載請保留聯系方式 ztao8607@gmail.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM