天天看點

跟着老貓來搞GO-内建容器slice

跟着老貓來搞go-容器:重點弄清楚數組和slice(切片)

前面的一章主要和大家分享了GO語言的函數的定義,以及GO語言中的指針的簡單用法,那麼本章,老貓就和大家一起來學習一下GO語言中的容器。

說到容器,大家有程式設計經驗的肯定第一個想到的就是數組了,當然也有程式設計經驗的小夥伴會覺得數組并不是容器。但是無論如何,說到數組其實它就是存儲群組織資料的一種方式而已,大家就不要太過糾結叫法了。

咱們直接上數組定義的例子,具體如下:

上面的例子輸出的結果如下

大家可以總結一下,其實數組有這麼幾個特點

在寫法上,其實也是和其他程式設計語言是相反的,其定義的數組的長度寫在變量類型的前面

數組中所存儲的内容必然是同一類型的

那麼我們如何周遊擷取數組中的資料呢?其實看過老貓之前文章的小夥伴應該曉得可以用for循環來周遊擷取,其中一種大家比較容易想到的方式如下(我們以周遊上面的arr3為例)

這種方式呢,我們當然是可以擷取的。接下來老貓其實還想和大家分享另外一種方式,采用range關鍵字的方式

大家覺得上述兩種方式哪種方式會比較優雅?顯而易見是後者了,意義明确而且美觀。

另外和大家同步一點是數組作為參數也是值傳遞。還是沿用之前的我們重新定義一個新的函數如下:

那麼我們在main函數中進行相關調用(為了示範編譯錯誤,老貓這裡用圖檔)

大家根據上面的圖可以很清晰的看到調用printArray(arr2)的時候報了編譯錯誤,其實這就是說明,在go語言中,即使同一個類型的數組,如果不同長度,那麼編譯器還是認為他們是不同類型的。

那麼我們這個時候再對傳入的數組進行值的變更呢,具體如下代碼

大家可以看到,老貓在這裡操作了兩次列印,第一次列印是直接在函數中列印,此時已經更改了第一個值,其函數内部列印的結果為

顯然内部的值是變更了,然而我們再看一下外面的函數的列印的值,如下

其實并沒有發生變更,這其實說明了什麼呢,這其實說明了在調用printArray的時候其實是直接将數組拷貝一份傳入函數的,外面的數組并未被更新,這也直接說明了GO語言是值傳遞的參數傳遞方式。

大家在使用這個數組的時候一定要注意好了,說不準就被坑了。大家可能會覺得這個數組真難用,其實可以告訴大家一個好消息,在GO語言中,一般其實不會直接去使用數組的,咱們用的比較多的還是“切片”

說到切片的話,咱們其實最好是基于上面數組的基礎上去了解切片。咱們先來看一個例子

其實像類似于'[]'這種定義我們就稱呼其為切片,英文成為slice,它表示擁有相同類型元素的可變長度的序列。我們來看一下結果:

其實這麼說會比較好了解,slice咱們可以将其看作為視圖,就拿arr[2:6]來說,我們其實在原來數組的基礎上抽取了從第二個位置到第六個位置的元素作為值重新展現出來,當然我們的取值為左閉右開區間的。

上面我們說了slice相當于是數組的視圖,那麼接下來的例子,咱們來證明上述的說法,詳細看下面的例子

老貓寫了個函數,主要是更新slice第一個位置的值,大家可以先思考一下執行前後所得到的結果是什麼,然後再看下面的答案。

其實最終執行的結果為:

那麼為什麼是這樣的?其實arr[2:6]很容易了解是上面的3456,第二個也比較容易了解,當我們slice的第一個值被更新成了100,是以程式設計了第二種,那麼原始的資料為什麼也會變成100呢?這裡面其實是需要好好品一下,因為我們之前說slice是對原數組的視圖,當我們第二種看到slice其實已經發生了更新變成了100,那麼底層的資料肯定也發生了變更,變成了100了。(這裡要注意的是,并沒有誰說視圖的操作不會反作用于原數組)。這裡還是比較重要的,希望大家細品一下。

說到reslice,說白了就是對原先的slice再做一次slice取值,那麼我們看下面的例子。

以上例子可見s1是對數組的全量切片,然後我們對s1又進行了一次切片處理,很容易地可以推算出來我們第二次所得到的結果為[3,4],像這種行為我們就稱為reslice,這個還是比較好了解的。

接下來咱們在這個基礎上加深一下難度,我們在S2的基礎上再次進行resilce,具體如下:

我們都知道s2所得到的值為[3,4],當我們在次對其進行reslice的時候,由于取的是[1:3],那麼此時我們發現是從第一個位置到第三個位置,第一個位置還是比較好推算出來的,基于[3,4]的話,那麼其第一個位置應該是4,那麼後面呢?結果又是什麼呢?這裡将結果直接告訴大家吧,其實老貓運作之後所得到的結果是

那麼為什麼會有這樣的一個結果?5又是從哪裡來的呢?

咱們來看一下老貓下面整理的一幅示意圖。

arr的一個數組,并且其長度為7,并且裡面存儲了七個數。

接下來s1對其去完全切片,是以我們得到的也是一個完整的7個數。

需要注意的是,這時候我們用的是下标表示,當s2對s1在此切片的時候,咱們發現其本質是對數組的第二個元素開始進行取值,由于是視圖的概念,其實s2還會視圖arr虛幻出另外兩個位置,也就是咱們表示的灰色的3以及4下标。

同樣的我們将s3表示出來,由此我們s3是在s2的基礎上再次切片,理論上有三個下标值,分别是0、1、2下标取值,但是我們發現s2的3号位置訓示虛幻出來的位置,并未真正存在值與之對應,是以,咱們取交集之後與數組arr對應隻能取出兩個,也就是最終的[4,5]。

此處還是比較難了解,希望大家好好了解一下,然後寫代碼自己推演一下,其實這個知識點就是slice的擴充,我們再來看一下下面的slice的底層實作。

其實slice一般包含三個概念,slice的底層其實是空數組結構,ptr為指向數組第一個位置的指針,Len表示具體的slice的可用長度,而cap表示有能力擴充的長度。

其實關于len以及cap我們都有函數直接可以調用擷取,我們看一下上面的例子,然後列印一下其長度以及擴充cap大家就清楚了。具體列印的代碼如下。

上述代碼輸出的結果為

當我們的取值超過cap的時候就會報錯,例如現在s2為s2:=[2:4],現在我們發現其cap為5,如果我們超過5,那麼此時s2可以寫成s2:=[2:8],那麼此時就會報以下異常

再者如果我們這麼取值

此時s3已經超過了len長度,那麼也會報錯,報錯如下

綜上例子,我們其實可以得到這麼幾個結論。

slice可以向後擴充,不可以向前擴充。

s[i]不可以超越len(s),向後擴充不可以超越底層數組cap(s)

以上對slice的擴充其實還是比較讓人頭疼的,比較難了解,不過真正弄清裡面的算法倒是也還好,希望大家也能了解上述的闡釋,老貓已經盡最大努力了,如果還有不太清楚的,也歡迎大家私聊老貓。

向slice添加元素,如何添加呢?看一下老貓的代碼,如下:

如上述所示,我們往切片中添加操作的時候采用的是append函數,大家可以先不看老貓下面的實際結果自己推算一下最終的輸出結果是什麼。結合之前老貓所述的切片操作。結果如下:

上述我們會發現append操作的話會有這樣的一個結論

添加元素的時候如果超過cap,系統會重新配置設定更大的底層數組

由于值傳遞的關系,必須接收append的傳回值

之前老貓和大家分享的slice看起來都是基于arr的,其實slice的底層也确實是基于arry的,那麼我們是不是每次在建立slice的時候都需要去建立一個數組呢?其實不是的,我們slice的建立方式有很多種,我們來看一下下面的建立方式

為什麼要把删除操作單獨拎出來分享,主要是因為上述這些操作都有比較便捷的内建函數來使用,但是删除操作就沒有了。咱們隻能通過切片的特性來求值。如下例子

上述有一個2到6的切片,如果我們要移除其中的4元素,那麼我們就得用這種切片組合的方式去移除裡面的元素,相信大家可以看懂,至于“s1[3:]...”這種形式,其實是go語言的一種寫法,表示取從3号位置剩下的所有的元素。

最終我們得到的結果就得到了

以上就是對slice的所有的知識分享了,花了老貓不少時間整理出來的,老貓也盡量把自己的一些了解說清楚,slice在語言中還是比較重要的。

回顧一下上面的GO語言容器,其實重點和大家分享是slice(切片)的相關定義,操作以及底層的一些原理。弄清楚的話還是比較容易上手的。當然go語言的容器可不止這些,由于篇幅的限制,老貓就不分享其他的容器了,相信在寫下去就沒有耐心看了。後面的容器主要會和大家分享map以及字元和字元串的處理。

我是老貓,更多内容,歡迎大家搜尋關注老貓的公衆号“程式員老貓”。

熱愛技術,熱愛産品,熱愛生活,一個懂技術,懂産品,懂生活的程式員~

更多精彩内容,可以關注公衆号“程式員老貓”。

一起讨論技術,探讨一下點子,研究研究賺錢!