1. 程式人生 > >[Golang學習筆記] 07 陣列和切片

[Golang學習筆記] 07 陣列和切片

01-06回顧:

Go語言開發環境配置,

常用原始碼檔案寫法,

程式實體(尤其是變數)及其相關各種概念和程式設計技巧:

  型別推斷,變數重宣告,可重名變數,型別推斷,型別轉換,別名型別和潛在型別

陣列:

陣列型別的值的長度是固定的,在宣告陣列的時候,長度必須給定,並且在之後不會改變,可以說陣列的長度是其型別的。

比如:[1]string和[2]string就是兩個不同的陣列型別。

切片:

切片型別的值是可變長的。切片的型別字面量中只有元素的型別([]int),而沒有長度。切片的長度可以自動地隨著其中元素數量的增長而增加,但是不會隨著元素數量的減少而減小。可以把切片看作是對陣列的一層簡單的封裝,因為在每個切片的底層資料結構中,一定會包含一個數組。後者被叫做前者的底層陣列,前者被看作是對後者的某個連續片段的引用

最大區別:陣列長度是固定的,切片是可變長的。

 

陣列可以叫切片的底層陣列,切片可以看作陣列的連續片段的引用。

 

定義一個切片(三種方式):

①定義一個切片,然後讓切片去引用一個已經建立好的陣列

var arr [5]int = [5]int {1, 2, 3, 4, 5}
var slice = arr[1:3]

②通過make來建立切片。基本語法:var 切片名 []type = make([], len, [cap]);引數說明:type是資料型別、len是大小、cap是切片容量(容量必須>=長度)

var slice []float64 = make([]float64, 5
, 10)
  • 通過make方式建立切片可以指定切片大小和容量
  • 如果沒有給切片的各個元素賦值,那麼就會使用預設值(int、float=>0, strint=>"", bool=>false)
  • 如果make方式建立的切片對應的陣列是由make底層維護,對外不可見,也就是隻能通過slice訪問各個元素

③定義一個切片,直接就指定具體陣列,使用原理類似於make的方式

var slice []string = []string{"zhangsan", "lisi", "wangwu"}

第一種方式是直接引用陣列,這個陣列是事先存在的,程式設計師可見

第二種方式是通過make來建立切片,make也會建立一個數組,是由切片在底層維護,程式設計師不可見

 

切片長度和容量計算公式:

對底層陣列容量是 k 的切片 slice[i:j] 來說:

長度: j - i

容量: k - i

 

擴充套件:

Go語言引用型別:切片型別,字典型別,通道型別,函式型別。

       值型別:基礎資料型別,陣列型別,結構體型別。

 

如果傳遞的值是引用型別的,那麼就是“傳引用”(符號&);

如果傳遞的值是值型別的,那麼就是“傳值”(符號=);

從傳遞成本的角度講,引用型別(&)的值要比值型別的值低很多。

面試題:怎樣正確估算切片的長度和容量?

// demo15.go
package main
 
import (
    "fmt"
)
 
func main() {
    s1:=make([]int, 5)// 指明長度
    fmt.Printf("The value of s1 is: %v\n", s1)
    fmt.Printf("The length of s1 is: %d\n", len(s1))
    fmt.Printf("The capacity of s1 is: %d\n", cap(s1))
    fmt.Printf("The value of s1 is: %d\n", s1)
    s2:=make([]int, 5, 8)// 指明長度和容量
    fmt.Printf("The length of s2:%d\n", len(s2))
    fmt.Printf("The capacity of s2 is:%d\n", cap(s2))
    fmt.Printf("The value of s2 is:%d\n", s2)
}

切片s1和s2的容量分別是5和8。

問題分析:

切片的容量實際上代表它底層陣列的長度。切片的底層陣列長度是不可變的。

想象有一個視窗,通過這個視窗可以看到一個數組,但是不一定能看到該陣列中所有元素,有一部分是被擋住了,有時候只能看到連續的一部分元素。

該例中,這個陣列就是切片s2的底層陣列,而這個視窗就是s2本身。s2的長度就是這個視窗的寬度,決定了你透過s2可以看到其底層陣列中的哪幾個連續的元素。

由於s2的長度是5(定義的),所以你可以看到其底層陣列中的第1到第5個元素,對應的底層陣列的索引範圍是0到4([0,4])。

切片代表的視窗被劃分成一個個的小格子,每個小格子都對應著其底層陣列中的某一個元素。

s2中,視窗最左邊的小格子對應的正好是其底層陣列中的第一個元素,即索引為0的那個元素。因此,s2中的索引從0到4的元素,就是其底層陣列中索引從0到4的那5個元素。

------

s3 := []int{1,2,3,4,5,6,7,8}

s4 := s3[3:6]

長度:

切片s3中有8個元素(整數1到8),s3的長度和容量都是8.

s4從s3中通過切片表示式初始化而來,其中[3:6]表達的就是透過新視窗能看到的,用減法可以計算出長度,6減去3為3.而索引範圍為從3到5(不包括6,可以引申為區間表示法,即[3,6))。

s4中的索引從0到2,指向的元素對應的是s3中索引從3(起始索引)到5(結束索引)的3(長度)個元素。

 

容量:

切片的容量代表了它的底層陣列的長度,但這僅限於用make函式或者切片字面量([]int{1,2,3})初始化切片的情況。

一個切片的容量可以被看作是透過這個視窗最多可以看到的底層陣列中元素的個數。

s4是通過在s3上施加切片操作得來的,所以s3的底層陣列等於s4的底層陣列;而且,在底層陣列不變的情況下,切片代表的視窗可以向右擴充套件,直至其底層陣列的末尾,所以s4的容量就是其底層陣列的長度8減去上述切片表示式([3:6])中的起始索引3,等於5。

 

要注意的是,切片代表的視窗是無法向左擴充套件的,也即無法透過s4看到s3中最左邊的那3個元素。

切片和陣列關係:

一個切片的容量可以被看作是透過這個視窗最多可以看到的底層陣列中元素的個數。

 

擴充套件

問題1: 怎麼樣估算切片容量的增長?

如果一個已定義的切片無法容納更多的元素,Go就會自動為其擴容。但是,擴容不會改變原來的切片,而是會生成一個容量更大的切片,然後把原切片元素和新元素都複製到新切片中。

對於擴容的計算方法,有兩種:

1、一般情況下,新容量是原容量的2倍;

2、當原長度>=(大於或等於)1024時,新容量基準不再是2倍,而是1.25倍。新容量基準會在此基礎上不斷地被調整,直到不小於原長度與要追加的元素數量之和(新長度),最終,新容量往往會比新長度大一些,也有可能相等。

特殊情況:如果一次追加的元素過多,以至於使得新長度比原容量的2倍還要大,那麼新容量就會以新長度為基準,最終的新容量往往比新容量基準要更大一些。

 

擴容append注意點:

append操作可能會導致原本使用同一個底層陣列的兩個Slice變數變為使用不同的底層陣列。

package main

import "fmt"

func main() {

    var array = []int{1, 2, 3, 4, 5}            // len:5,capacity:5
    var newArray = array[1:3]                   // len:2,capacity:4   (已經使用了兩個位置,所以還空兩位置可以append)
    fmt.Printf("array addr:%p\n", array)        //0xc420098000
    fmt.Printf("newArray addr:%p\n", newArray)  //0xc420098008 可以看到newArray的地址指向的是array[1]的地址,即他們底層使用的還是一個數組
    fmt.Printf("array value:%v\n", array)       //[1 2 3 4 5]
    fmt.Printf("newArray value:%v\n", newArray) //[2 3]
    fmt.Println()
    newArray[1] = 9                             //更改後array、newArray都改變了
    fmt.Printf("array value:%v\n", array)       // [1 2 9 4 5]
    fmt.Printf("newArray value:%v\n", newArray) // [2 9]
    fmt.Println()
    newArray = append(newArray, 11, 12)         //append 操作之後,array的len和capacity不變,newArray的len變為4,capacity:4。因為這是對newArray的操作
    fmt.Printf("array value:%v\n", array)       //[1 2 9 11 12] //注意對newArray做append操作之後,array[3],array[4]的值也發生了改變
    fmt.Printf("newArray value:%v\n", newArray) //[2 9 11 12]
    fmt.Println()
    newArray = append(newArray, 13, 14) // 因為newArray的len已經等於capacity,所以再次append就會超過capacity值,
    // 此時,append函式內部會建立一個新的底層陣列(是一個擴容過的陣列),並將array指向的底層陣列拷貝過去,然後在追加新的值。
    fmt.Printf("array addr:%p\n", array)        //0xc420098000
    fmt.Printf("newArray addr:%p\n", newArray)  //0xc4200a0000
    fmt.Printf("array value:%v\n", array)       //[1 2 9 11 12]
    fmt.Printf("newArray value:%v\n", newArray) //[2 9 11 12 13 14]  他兩已經不再是指向同一個底層陣列了
}

問題2:切片的底層陣列什麼時候會被替換?

一個切片的底層陣列永遠不會被替換。

因為在“擴容”的時候,也一定會生成新的底層陣列,同時也生成新的切片,而對原切片及其底層陣列沒有做任何改動。

append函式使用注意:在無需擴容時,返回的是指向原底層陣列的切片;需要擴容時,返回的是指向新底層陣列的新切片。

新增成員時,容量是2的指數遞增的,2,4,8,16,32。而且是在長度要超過容量時,才增加容量。

只要新長度不會超過切片的原容量,那麼使用append函式對其追加元素的時候就不會引起擴容,這隻會使得緊鄰切片視窗(抽象)右邊的元素被新的元素替換掉。

 

思考題:

1、如果有多個切片指向了同一個底層陣列,應該注意什麼?

當若干個切片指向同一個底層資料時,對每一個切片的修改都會反映在底層陣列中。如果覆蓋其他切片已經指向的陣列的值。

可以使用copy函式,重新建立一個切片,不影響源切片。

number5 := make([]int, 2)

copy(number5, numbers[:2])

2、怎樣沿用“擴容”的思想來對切片進行“縮容”?

對切片再次切片,縮小起止範圍,就可以縮容?

 

總結

切片的特點:佔用記憶體少、建立便捷,可以通過“視窗”快速地定位並獲取,並可以修改底層陣列中的元素。

缺點1:刪除切片中的元素就會造成大量元素的移動,注意空出的元素位置要“清空”,否則會造成記憶體洩露。

缺點2:切片頻繁“擴容”,底層陣列不斷產生,記憶體分配的量以及元素複製的次數也會越來越多,影響執行效能。

如果沒有一個合理、有效的切片“縮容”策略(舊底層陣列無法回收,新底層陣列不斷產生),會導致記憶體的過度浪費,降低程式執行效能,使得記憶體溢位,並可能導致程式崩潰。

 

本學習筆記僅為了總結自己學到的Go語言核心知識,方便以後回憶,文中部分內容摘錄自極客時間的《Go語言核心36講》專欄,如有侵權,請聯絡我刪除。