日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

go?slice?數組和切片使用區別示例解析_Golang

作者:eleven26 ? 更新時間: 2023-02-09 編程語言

正文

slice(切片)是 go 里面非常常用的一種數據結構,它代表了一個變長的序列,序列中的每個元素都有相同的數據類型。 一個 slice 類型一般寫作 []T,其中 T 代表 slice 中元素的類型;slice 的語法和數組很像,但是 slice 沒有固定長度。

數組和切片的區別

數組有確定的長度,而切片的長度不固定,并且可以自動擴容。

數組的定義

go 中定義數組的方式有如下兩種:

  • 指定長度:
arr := [3]int{1, 2, 3}
  • 不指定長度,由編譯器推導出數組的長度:
arr := [...]{1, 2, 3}

上面這兩種定義方式都定義了一個長度為 3 的數組。正如我們所見,長度是數組的一部分,定義數組的時候長度已經確定下來了。

切片的定義

切片的定義方式跟數組很像,只不過定義切片的時候不用指定長度

s := []int{1, 2, 3}

在上面定義切片的代碼中,我們可以看到其實跟數組唯一的區別就是少了個長度。 那其實我們可以把切片看作是一個無限長度的數組。 當然,實際上它并不是無限的,它只是在切片容納不下新的元素的時候,會自動進行擴容,從而可以容納更多的元素。

數組和切片的相似之處

正如我們上面看到的那樣,數組和切片兩者其實非常相似,在實際使用中,它們也是有些類似的。

比如,通過下標來訪問元素:

arr := [3]int{1, 2, 3}
// 通過下標訪問
fmt.Println(arr[1]) // 2
s := []int{1, 2, 3}
// 通過下標訪問
fmt.Println(s[1]) // 2

數組的局限

我們知道了,數組的長度是固定的,這也就意味著如果我們想往數組里面增加一個元素會比較麻煩, 我們需要新建一個更大的數組,然后將舊的數據復制過去,然后將新的元素寫進去,如:

// 往數組 arr 增加一個元素:4
arr := [3]int{1, 2, 3}
// 新建一個更大容量的數組
var arr1 [4]int
// 復制舊數組的數據
for i := 0; i < len(arr); i++ {
    arr1[i] = arr[i]
}
// 加入新的元素:4
arr1[3] = 4
fmt.Println(arr1)

這樣一來就非常的繁瑣,如果我們使用切片,就可以省去這些步驟:

// 定義一個長度為 3 的數組
arr := [3]int{1, 2, 3}
// 從數組創建一個切片
s := arr[:]
// 增加一個元素
s = append(s, 4)
fmt.Println(s)

因為數組固定長度的缺點,實際使用中切片會使用得更加普遍。

重新理解 slice

在開始之前,我們來看看 slice 這個單詞的意思:作為名詞,slice 的意思有 片;部分;(切下的食物)薄片;,作為動詞,slice 的意思有 切;把…切成(薄)片; 的意思。 從這個角度出發,我們可以把 slice 理解為從某個數組上 切下來的一部分(從這個角度看,slice 這個命名非常的形象)。我們可以看看下圖:

在這個圖中,A 是一個保存了數字 1~7sliceB 是從 A切下來的一部分,而 B 只包含了 A 中的一部分數據。 我們可以把 B 理解為 A 的一個 視圖,B 中的數據是 A 中的數據的一個 引用,而不是 A 中數據的一個 拷貝 (也就是說,我們修改 B 的時候,A 中的數據也會被修改,當然會有例外,那就是 B 發生擴容的時候,再去修改 B 的話就影響不了 A 了)。

slice 的內存布局

現在假設我們有如下代碼:

// 創建一個切片,長度為 3,容量為 7
var s = make([]int, 3, 7)
s[0] = 1
s[1] = 2
s[2] = 3
fmt.Println(s)

對應的內存布局如下:

說明:

  • slice 底層其實也是數組,但是除了數組之外,還有兩個字段記錄切片的長度和容量,分別是 lencap。
  • 上圖中,slice 中的 array 就是切片的底層數組,因為它的長度不是固定的,所以使用了指針來保存,指向了另外一片內存區域。
  • len 表明了切片的長度,切片的長度也就是我們可以操作的下標,上面的切片長度為 3,這也就意味著我們切片可以操作的下標范圍是 0~2。超出這個范圍的下標會報錯。
  • cap 表明了切片的容量,也就是切片擴容之前可以容納的元素個數

切片容量存在的意義

對于我們日常開發來說,slice 的容量其實大多數時候不是我們需要關注的點,而且由于容量的存在,也給開發者帶來了一定的困惑。 那么容量存在的意義是什么呢?意義就在于避免內存的頻繁分配帶來的性能下降(容量也就是提前分配的內存大小)。

比如,假如我們有一個切片,然后我們知道需要往它里面存放 1w 個元素, 如果我們不指定容量的話,那么切片就會在它存放不下新的元素的時候進行擴容, 這樣一來,可能在我們存放這 1w 個元素的時候需要進行多次擴容, 這也就意味著需要進行多次的內存分配。這樣就會影響應用的性能。

我們可以通過下面的例子來簡單了解一下:

// Benchmark1-20       100000000          11.68 ns/op
func Benchmark1(b *testing.B) {
   var s []int
   for i := 0; i < b.N; i++ {
      s = append(s, 1)
   }
}
// Benchmark2-20       134283985           7.482 ns/op
func Benchmark2(b *testing.B) {
   var s []int = make([]int, 10, 100000000)
   for i := 0; i < b.N; i++ {
      s = append(s, 1)
   }
}

在第一個例子中,沒有給 slice 設置容量,這樣它就只會在切片容納不下新元素的時候才會進行擴容,這樣就會需要進行多次擴容。 而第二個例子中,我們先給 slice 設置了一個足夠大的容量,那么它就不需要進行頻繁擴容了。

最終我們發現,在給切片提前設置容量的情況下,會有一定的性能提升。

切片常用操作

創建切片

我們可以從數組或切片生成新的切片:

注意:生成的切片不包含 end。

target[start:end]

說明:

  • target 表示目標數組或者切片
  • start 對應目標對象的起始索引(包含)
  • end 對應目標對象的結束索引(不包含)

如:

s := []int{1, 2, 3}
s1 := s[1:2]    // 包含下標 1,不包含下標 2
fmt.Println(s1) // [2]
arr := [3]int{1, 2, 3}
s2 := arr[1:2]
fmt.Println(s2) // [2]

在這種初始化方式中,我們可以省略 start

arr := [3]int{1, 2, 3}
fmt.Println(arr[:2]) // [1, 2]

省略 start 的情況下,就是從 target 的第一個元素開始。

我們也可以省略 end

arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]

省略 end 的情況下,就是從 start 索引處的元素開始直到 target 的最后一個元素處。

除此之外,我們還可以指定新的切片的容量,通過如下這種方式:

target[start:end:cap]

例子:

arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s := arr[1:4:5]
fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4

往切片中添加元素

我們前面說過了,如果我們想往數組里面增加元素,那么我們必須開辟新的內存,將舊的數組復制過去,然后才能將新的元素加入進去。

但是切片就相對簡單,我們可以使用 append 這個內置函數來往切片中加入新的元素:

var a []int
a = append(a, 1) // 追加1個元素
a = append(a, 1, 2, 3) // 追加多個元素
a = append(a, []int{1,2,3}...) // 追加一個切片

切片復制

go 有一個內置函數 copy 可以將一個切片的內容復制到另外一個切片中:

copy(dst, src []int)

第一個參數 dst 是目標切片,第二個參數 src 是源切片,調用 copy 的時候會把 src 的內容復制到 dst 中。

示例:

var a []int
var b []int = []int{1, 2, 3}
// a 的容量為 0,容納不下任何元素
copy(a, b)
fmt.Println(a) // []
a = make([]int, 3, 3) // 給 a 分配內存
copy(a, b)
fmt.Println(a) // [1 2 3]

需要注意的是,如果 dst 的長度比 src 的長度小,那么只會截取 src 的前面一部分。

從切片刪除元素

雖然我們往切片追加元素的操作挺方便的,但是要從切片刪除元素就相對麻煩一些了。go 語言本身沒有提供從切片刪除元素的方法。 如果我們要刪除切片中的元素,只有構建出一個新的切片:

對應代碼:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
fmt.Println(a) // [1 2 3 4 5 6 7]
var b []int
b = append(b, a[:2]...) // [1 2]
b = append(b, a[5:]...) // [1 2 6 7]
fmt.Println(b) // [1 2 6 7]

在這個例子中,我們想從 a 中刪除 3、4、5 這三個元素,也就是下標 2~4 的元素, 我們的做法是,新建了一個新的切片,然后將 3 前面的元素加入到這個新的切片中, 再將 5 后面的元素加入到這個新切片中。

最終得到的切片就是刪除了 3、4、5 三個元素之后的切片了。

切片的容量到底是多少?

假設我們有如下代碼:

var a = make([]int, 7, 7)
for i := 0; i &lt; 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3] 3 7
fmt.Println(s1, len(s1), cap(s1))
s2 := a[4:6]
// [5 6] 2 3
fmt.Println(s2, len(s2), cap(s2))

s1s2 可以用下圖表示:

  • s1 只能訪問 array 的前三個元素,s2 只能訪問 56 這兩個元素。
  • s1 的容量是 7(底層數組的長度)
  • s2 的容量是 3,從 5 所在的索引處直到底層數組的末尾。

對于 s1s2,我們都沒有指定它的容量,但是我們打印發現它們都有容量, 其實在切片中,我們從切片中生成一個新的切片的時候,如果我們不指定容量, 那新切片的容量就是 s[start:end] 中的 start 直到底層數組的最后一個元素的長度。

切片可以共享底層數組

切片最需要注意的點是,當我們從一個切片中創建新的切片的時候,兩者會共享同一個底層數組, 如上圖的那樣,s1s2 都引用了同一個底層的數組不同的索引, s1 引用了底層數組的 0~2 下標范圍,s2 引用了底層數組 4~5 下標范圍。

這意味著,當我們修改 s1s2 的時候,原來的切片 a 也會發生改變:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3]
fmt.Println(s1)
s1[1] = 100
// [1 100 3 4 5 6 7]
fmt.Println(a)
// [1 100 3]
fmt.Println(s1)

在上面的例子中,s1 這個切片引用了和 a 一樣的底層數組, 然后在我們修改 s1 的時候,a 也發生了改變。

切片擴容不會影響原切片

上一小節我們說了,切片可以共享底層數組。但是如果切片擴容的話,那就是一個全新的切片了

var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
// a 容納不下新的元素了,會進行擴容
b := append(a, 4)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 2 3]
fmt.Println(a)
// [1 100 3 4]
fmt.Println(b)

在上面這個例子中,a 是一個長度和容量都是 3 的切片,這也就意味著,這個切片已經滿了。 在這種情況下,我們再往其中追加元素的時候,就會進行擴容,生成一個新的切片。 因此,我們可以看到,我們修改了 b 的時候,并沒有影響到 a

下面的例子就不一樣了:

// 長度為 2,容量為 3
var a = make([]int, 2, 3)
a[0] = 1
a[1] = 2
// [1 2] 2 3
fmt.Println(a, len(a), cap(a))
// a 還可以容納新的元素,不用擴容
b := append(a, 4)
// [1 2 4] 3 3
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 100]
fmt.Println(a)
// [1 100 4]
fmt.Println(b)

在后面這個例子中,我們只是簡單地改了一下 a 初始化的方式,改成了只放入兩個元素,但是容量還是 3, 在這種情況下,a 可以再容納一個元素,這樣在 b := append(a, 4) 的時候,創建的 b 底層的數組其實跟 a 的底層數組依然是一樣的。

所以,我們需要尤其注意代碼中作為切片的函數參數,如果我們希望在被調函數中修改了切片之后,在 caller 里面也能看到效果的話,最好是傳遞指針。

func test1(s []int) {
   s = append(s, 4)
}
func test2(s *[]int) {
   *s = append(*s, 4)
}
func TestSlice(t *testing.T) {
   var a = []int{1, 2, 3}
   // [1 2 3] 3 3
   fmt.Println(a, len(a), cap(a))
   test1(a)
   // [1 2 3] 3 3
   fmt.Println(a, len(a), cap(a))
   var b = []int{1, 2, 3}
   // [1 2 3] 3 3
   fmt.Println(b, len(b), cap(b))
   test2(&amp;b)
   // [1 2 3 4] 4 6
   fmt.Println(b, len(b), cap(b))
}

在上面的例子中,test1 接收的是值參數,所以在 test1 中切片發生擴容的時候,TestSlice 里面的 a 還是沒有發生改變。 而 test2 接收的是指針參數,所以在 test2 中發生切片擴容的時候,TestSlice 里面的 b 也發生了改變。

總結

  • 數組跟切片的使用上有點類似,但是數組代表的是有固定長度的數據序列,而切片代表的是沒有固定長度的數據序列。
  • 數組的長度是類型的一部分,有兩種定義數組的方式:[2]int{1, 2}[...]int{1, 2}
  • 數組跟切片都可以通過下標來訪問其中的元素,可以訪問的下標范圍都是 0 ~ len(x)-1,x 表示的是數組或者切片。
  • 數組無法追加新的元素,切片可以追加任意數量的元素。
  • slice 的數據結構里面包含了:array 底層數組指針、len 切片長度、cap 切片容量。
  • 創建切片的時候,指定一個合適的容量可以減少內存分配的次數,從而在一定程度上提高程序性能。
  • 我們可以從數組或者切片創建一個新的切片:array[1:3] 或者 slice[1:3]。
  • 使用 append 內置函數可以往切片中添加新的元素。
  • 使用 copy 內置函數可以將一個切片的內容復制到另外一個切片中。
  • 切片刪除元素沒有好的辦法,只能截取被刪除元素前后的數據,然后復制到一個新的切片中。
  • 假設我們通過 slice[start:end] 的方式從切片中創建一個新的切片,那么這個新的切片的容量是 cap(slice) - start,也就是,從 start 到底層數組最后一個元素的長度。
  • 使用切片的時候需要注意:切片之間會共享底層數組,其中一個切片修改了切片的元素的時候,也會反映到其他切片上。
  • 函數調用的時候,如果被調函數內發生擴容,調用者是無法知道的。如果我們不想錯過在被調函數內切片的變化,我們可以傳遞指針作為參數。

原文鏈接:https://juejin.cn/post/7179159979193008188

欄目分類
最近更新