Go 1.19.4 切片與子切片-Day 05

1. 切片

1.1 介紹

切片在Go中是一個引用類型,它包含三個組成部分:指向底層數組的指針(pointer)、切片的長度(length)以及切片的容量(capacity),這些信息共同構成了切片的“頭(header)”。

切片是一個非常奇怪的集合體,它底層用的是數組,但它又能把數組值復制這個問題規避掉。
為啥底層是數組呢?因為它需要使用順序表,因為使用索引訪問,在順序表中是最快的。

1.2 特點

它的特點如下:
(1)長度可以變,容量可變,長度和容量可以不一樣,首次定義時,長度和容量相同。

長度:表示當前元素的數量
容量:表示最多可以定義多少個元素。
如切片長度3,容量5,含義為我切片中最多可以放5個元素,但當前只用了3個,還剩2個元素可以放置。
我把它理解為k8s中的request和limit。

(2)引用類型
切片之間引用(復制)的是header,并不是直接引用內存地址。

(3)底層基于數組

1.3 定義方式

1.3.1 方式一:字面量賦值定義

該方式適合小批量的定義,如果切片元素過多,就不太適合了。

package mainimport "fmt"func main() {// 錯誤的聲明方式// var s0 = []int// 這就是定義一個切片,如果在[]中加上數字或者...,那就是一個數組// 這里的int可以是go中支持的任意數據類型,但元素類型必須一致var s0 = []int{1, 2, 3} // 該切片長度為3,容量為3fmt.Printf("%v\n%[1]T", s0)
}
=========調試結果=========
[1 2 3] // 光從輸出結果來看,是無法分辨數組和切片
[]int // 打印值類型就可以,[]中為空,就表示切片

1.3.2 方式二:聲明空切片(不推薦)

package mainimport "fmt"func main() {// 定義一個長度為0,容量為0的切片var s1 []intfmt.Printf("%T %[1]v %d %d", s1, len(s1), cap(s1))
}
=========調試結果=========
[]int [] 0 0

1.3.3 方式三:make(推薦)

make可以給內建容器開辟內存空間,比較適合用于多元素定義的場景。
并且make還能指定初始容量大小,減少頻繁擴容。
但是注意,不同的數據類型使用make,參數含義是不一樣的。

package mainimport "fmt"func main() {// 0,表示長度為0,目前由于沒有元素,所以容量也為0。// 切片使用make,()中的第二個參數表示長度var s3 = make([]int, 0)fmt.Println(s3, len(s3), cap(s3))// 切片使用make,()中的第二個參數0表示長度,第三個參數5表示容量s4 := make([]string, 0, 5)fmt.Println(s4, len(s4), cap(s4))
}
=========調試結果=========
[] 0 0 // 長度為0,容量為0
[] 0 5 // 長度為0,容量為5

1.4 切片內存模型

切片的內存模型大致如下,還能稱為切片的herdedr:
(1)pointer
存放的指向底層數組的指針。
這個指針指向切片實際引用的數組元素的起始位置。通過這個指針,切片能夠訪問和操作底層數組中的元素。

(2)len
存放當前切片的長度,這個長度決定了切片可以訪問的底層數組元素的范圍。

(3)cap
存放當前切片的容量,容量反映了切片可以增長元素的最大范圍,即在不需要重新分配底層數組的情況下,可以向切片追加的元素數量。

由于切片需要使用順序表,所以它的底層其實還是依賴數組的。
但是數組一旦定死它的長度是不可變的,而切片的長度和容量都可變,那數組的長度不夠咋辦呢?
切換底層數組,當切片需要擴容,但底層數組長度又不夠的時候,go會廢棄這個老的底層數組,再創建一個新的滿足切片擴容長度的底層數組。
在這里插入圖片描述

1.4.1 切片元素內存地址理解

package mainimport "fmt"func main() {var s0 = []int{1, 2, 3}fmt.Printf("%p %p\n", &s0, &s0[0])// &s0,表示的是當前這個結構體(切片)的內存地址(header地址)。// &s0[0],表示的是當前這個切片底層數組的第一個元素的內存地址,也是底層數組的首地址。
}
=========調試結果=========
0xc000008078 0xc000010168

1.4.2 追加內容到切片(append)

append內置函數,用于在切片的尾部追加元素,并且不會修改當前切片的header,因為它總是會返回一個新的header(至于header內容是否改變,取決于操作的切片是新還是舊)。
如果是基于老切片新增元素給新切片,則header可能會發生變化,也就是說pointer、len、cap都有可能會發生變化。
增加元素后,有可能超過當前切片容量,導致切片擴容(切片擴容容量為擴容前已存在元素的倍數)。
注意append只能用于切片。

package mainimport "fmt"func main() {var s0 = []int{1, 2, 3}fmt.Printf("%p %p\n", &s0, &s0[0])// append(s0, 11),表示對s0進行尾部元素追加,追加完畢后又寫入到s0s0 = append(s0, 11)fmt.Println(s0, &s0[0])
}
=========調試結果=========
0xc000008078 0xc000010168
// 11就是追加的內容,并且追加后,底層數組的首地址也發生了改變
// 這是符合上面的推斷的
[1 2 3 11] 0xc00000e3c0
1.4.2.1 切片長度與容量
package mainimport "fmt"func main() {// 切片長度為3,容量為5var s0 = make([]int, 3, 5)fmt.Printf("切片內存地址:%p\n底層數組首地址:%p\n切片元素數量:%d\n切片容量:%v\n切片元素:%v", &s0, &s0[0], len(s0), cap(s0), s0)
}
=========調試結果=========
切片內存地址:0xc000116060
底層數組首地址:0xc000142030
切片元素數量:3
切片容量:5
切片元素:[0 0 0]

基于老切片追加元素到新切片,觀察新老切片的變化。

// 上面s0切片還是3個0值,下面我給他調整一下
package mainimport "fmt"func main() {var s0 = make([]int, 3, 5)fmt.Printf("切片內存地址:%p\n底層數組首地址:%p\n切片元素數量:%d\n切片容量:%v\n切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)fmt.Println("----------------------------------")// 向s0追加兩個元素,得到新的切片s1s1 := append(s0, 1, 2)fmt.Println(s0, len(s0), cap(s0))fmt.Println(s1, len(s1), cap(s1))
}
=========調試結果=========
切片內存地址:0xc0000aa060
底層數組首地址:0xc0000d8030
切片元素數量:3
切片容量:5
切片元素:[0 0 0]
----------------------------------
// 看這部分
[0 0 0] 3 5 // 這是s0
[0 0 0 1 2] 5 5 // 這是s1

為什么s0的長度和容量與s1不一樣?
這就不得不再說下切片的herdedr了,首先最開始用make定義切片的時候,var s0 = make([]int, 3, 5),這個切片中只存儲了3個0元素,但由于容量為5,實際上還能增加2個元素。
所以追加兩個元素后(??s0??原本長度為3,追加后長度為5),總長度并沒有超過原切片的容量(5),所以??append??操作是在原切片??s0??的底層數組上進行的,并且??s1??和??s0??共享同一個底層數組。但是,??s1??和??s0??是兩個不同的切片頭(header),因為它們有不同的長度。

那這里思考一個問題,s0和s1的底層數組是否相同?
看下面的代碼:

package mainimport "fmt"func main() {// 定義一個長度為3,容量為5的切片var s0 = make([]int, 3, 5)fmt.Printf("切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)fmt.Println("----------------------------------")// 向s0追加兩個元素,得到新的切片s1s1 := append(s0, 1, 2)// fmt.Println(s0, len(s0), cap(s0))// fmt.Println(s1, len(s1), cap(s1))fmt.Printf("切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========調試結果=========
切片內存地址:0xc000080048 底層數組首地址:0xc0000aa030 切片元素數量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
切片內存地址:0xc000080078 底層數組首地址:0xc0000aa030 切片元素數量:5 切片容量:5 切片元素:[0 0 0 1 2]

通過上面的返回可以看到,s0切片和s1切片的header(內存地址)不同,但底層數組地址完全一樣,究其原因就是因為底層數組的長度是滿足元素新增的,所以實際上兩個切片都是引用的同一個數組(數據是存在同一個內存空間中的)。

既然底層是同一個數組,為什么s0和s1顯示的內容不同?
可以把切片的長度當成一個窗簾,底層數組實際上就是存儲著00012,但由于s0受到長度3的限制,所以我們是看不到超過長度3的內容的。

為啥兩個切片的header不同呢?
因為兩個切片的元素數量不同,所以s1 := append(s0, 1, 2)插入元素后返回值給s1時,header中的len被更新了,所以header看著不一樣,其實簡單理解,s0和s1都是一個獨立的切片,所以header肯定不一樣,雖然它們底層引用的都是相同的數組。

1.4.2.2 切片容量溢出

這里主要講一下,切片容量溢出后,底層到底是怎么做的。
主要看下面新增的s3切片:

package mainimport "fmt"func main() {// 定義一個長度為3,容量為5的切片var s0 = make([]int, 3, 5)fmt.Printf("s0 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)fmt.Println("----------------------------------")// 向s0追加兩個元素,得到新的切片s1。s1 := append(s0, 1, 2)fmt.Printf("s1 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)fmt.Println("----------------------------------")s2 := append(s0, -1)fmt.Printf("s2 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)fmt.Println("----------------------------------")// 向s2追加三個元素,得到新的切片s3s3 := append(s2, 3, 4, 5)fmt.Printf("s3 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
=========調試結果=========
s0 切片內存地址:0xc000008078 底層數組首地址:0xc00000e3c0 切片元素數量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
s1 切片內存地址:0xc0000080a8 底層數組首地址:0xc00000e3c0 切片元素數量:5 切片容量:5 切片元素:[0 0 0 1 2]
----------------------------------
s2 切片內存地址:0xc0000080d8 底層數組首地址:0xc00000e3c0 切片元素數量:4 切片容量:5 切片元素:[0 0 0 -1]
----------------------------------
s3 切片內存地址:0xc000008108 底層數組首地址:0xc000012230 切片元素數量:7 切片容量:10 切片元素:[0 0 0 -1 3 4 5]

上述代碼中,通過向s2追加三個元素,得到新的切片s3。
具體的實現邏輯大概是這樣:
s2底層數組容量為5,長度為4,append要新增3個,超了2個,觸發擴容,于是向系統申請一塊新的連續(順序表)的內存空間,然后將s2底層數組中已有的數據復制過來,再把要追加的元素寫入,最終得到一個新的底層數組,并且append還會返回一個全新的header給到s3,其中pointer指向新的底層數組、切片長度為7、切片容量為10(系統會自動冗余一些空間,后續講擴容策略)。

1.5 切片的擴容機制

官方文檔:??https://go.dev/src/runtime/slice.go??

(老版本)實際上,當擴容后的cap<1024時,擴容翻倍,容量變成之前的2倍;當cap>=1024時,變成之前的1.25倍(擴容前已存在元素的倍數)。
(新版本1.18+)閾值變成了256,當擴容后的cap<256時,擴容翻倍,容量變成之前的2倍(擴容前已存在元素的倍數);當cap>=256時, newcap += (newcap + 3*threshold) / 4 計算后就是 newcap = newcap +
newcap/4 + 192 ,即1.25倍后再加192。

擴容是創建新的底層數組,把原內存數據拷貝到新內存空間,然后在新內存空間上執行元素追加操作。

切片頻繁擴容成本非常高(元素越多,復制時間越長),所以盡量早估算出使用的大小,一次性給夠,建議使用make。常用make([]int, 0, 100) 。

header復制也會消耗資源,但是很少。
如:var s1 = s0,這種就是header結構體復制

思考一下:如果 s1 := make([]int, 3, 100) ,然后對s1進行append元素,會怎么樣?
當追加的元素不超過切片容量時,只有切片長度會變,其他不變。
如果超過了容量,那么就會觸發擴容。
在這里插入圖片描述

1.6 引用類型

在Go語言中,引用類型(Reference Types)是指那些在賦值、作為函數參數傳遞或作為函數返回值時,傳遞的是指針(即內存地址)的類型,而不是值本身。
這意味著,當操作引用類型的變量時,實際上是在操作其指向的內存位置上的數據。
但嚴格意義上來說,復制的是header。
Go語言中的引用類型包括切片(slices)、映射(maps)、通道(channels)、接口(interfaces)、函數類型以及指向它們的指針。

1.6.1 思考以下代碼切片之間是否發生了復制

package mainimport "fmt"func main() {var s0 = []int{1, 3, 5}fmt.Printf("s0 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)s1 := s0fmt.Printf("s1 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========調試結果=========
s0 切片內存地址:0xc000008078 底層數組首地址:0xc000010168 切片元素數量:3 切片容量:3 切片元素:[1 3 5]
s1 切片內存地址:0xc0000080a8 底層數組首地址:0xc000010168 切片元素數量:3 切片容量:3 切片元素:[1 3 5]

通過返回結果可以得出,只是把切片賦值給另一個新切片,只有header地址會改變,header中的pointer、len、cap都不會變。

這說明什么?說明s0和s1之間,只復制了header結構體,但header中的pointer、len、cap都沒變。

如果把s1切片的元素修改,s0切片會改變嗎?

package mainimport "fmt"func main() {var s0 = []int{1, 3, 5}// fmt.Printf("s0 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)s1 := s0// fmt.Printf("s1 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)s1[0] = 100fmt.Println(s0, s1)
}
=========調試結果=========
[100 3 5] [100 3 5]

表面上看,操作s1就好像在操作s0,有點類似復制了切片的內存地址,通過地址操作兩個切片一起變,但實際上還是因為兩個切片共用同一個底層數組。

1.6.2 使用函數傳參是否會發生復制

package mainimport "fmt"func showAddr(s2 []int) { // 新增函數fmt.Printf("s2 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)
}func main() {var s0 = []int{1, 3, 5}fmt.Printf("s0 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)s1 := s0fmt.Printf("s1 切片內存地址:%p 底層數組首地址:%p 切片元素數量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)s1[0] = 100// fmt.Println(s0, s1)showAddr(s0) // 函數傳參
}
=========調試結果=========
s0 切片內存地址:0xc000008078 底層數組首地址:0xc000010168 切片元素數量:3 切片容量:3 切片元素:[1 3 5]
s1 切片內存地址:0xc0000080a8 底層數組首地址:0xc000010168 切片元素數量:3 切片容量:3 切片元素:[1 3 5]
s2 切片內存地址:0xc0000080d8 底層數組首地址:0xc000010168 切片元素數量:3 切片容量:3 切片元素:[100 3 5]

通過結果得出,只有header結構體發生了復制,但header中存儲的pointer、len、cap不變。

1.7 總結

Go語言中全都是值拷貝(復制),如整型、數組這樣的類型的值是完全復制,slice、map、channel、interface、function這樣的引用類型也是值拷貝,不過復制的是標頭值。

2 . 子切片

2.1 介紹

切片可以通過指定索引區間獲得一個子切片,格式為slice[start:end],規則就是前包后不包,對應元素的索引。

2.2 子切片特點

子切片(slice)是基于底層數組的一個視圖或者窗口。
當從一個已有的切片中創建子切片時,實際上是在共享同一個底層數組,而不是創建一個新的、獨立的數組。因此,子切片的創建本身不會導致底層數組的擴容。
但是,如果使用append追加,則是有可能觸發擴容的。

2.3 子切片語法

slice[start:end]
start:不寫默認為0。
end:不寫話,默認為切片長度。
注意:指定start和end時,不能超過切片的容量。

2.4 子切片示例

2.4.1 示例一:完全復制header

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的內存地址:%p|s1的底層數組首地址:%p|s1的長度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 把s1切片賦值給s2s2 := s1 // 本質上就是在復制headerfmt.Printf("s2的內存地址:%p|s2的底層數組首地址:%p|s2的長度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)// 開始子切片s3 := s1[:] //構建一個新的header,但不會新建數組fmt.Printf("s3的內存地址:%p|s3的底層數組首地址:%p|s3的長度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========調試結果===========
s1的內存地址:0xc0000aa060|s1的底層數組首地址:0xc0000d8030|s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的內存地址:0xc0000aa090|s2的底層數組首地址:0xc0000d8030|s2的長度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的內存地址:0xc0000aa0c0|s3的底層數組首地址:0xc0000d8030|s3的長度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

通過上面的代碼,可以看到s3子切片后,結果和之前的相同,說明了什么?
子切片和原來的切片使用的底層數組也是同一個。

2.4.2 示例二:偏移切片

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的內存地址:%p|s1的底層數組首地址:%p|s1的長度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 首地址發生變化,切偏移一個元素,最終的長度和容量都-1s4 := s1[1:]fmt.Printf("s4的內存地址:%p|s4的底層數組首地址:%p|s4的長度:%d|s4的容量:%d|s4的元素:%v\n", &s4, &s4[0], len(s4), cap(s4), s4)}
===========調試結果===========
s1的內存地址:0xc000008078|s1的底層數組首地址:0xc00000e3c0|s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s4的內存地址:0xc0000080a8|s4的底層數組首地址:0xc00000e3c8|s4的長度:4|s4的容量:4|s4的元素:[30 50 70 90]

看結果:
s1的底層數組首地址:0xc00000e3c0
s4的底層數組首地址:0xc00000e3c8
是不是以為底層數組變了?錯,子切片過程中,只要沒有append操作,底層數組依然還是同一個。
之所以一個首地址是3c0,一個是3c8,是因為int類型就占用8個字節。
并且s4 := s1[1:],意思是偏移了一個元素(把第一個元素擋住了,看不到了),所以此時的首地址就變成了第二個元素的內存地址。
并且由于偏移了一個元素,所以子切片的容量就為4,長度呢?長度沒有指定,所以就從偏移處直到末尾,為4。

2.4.3 示例三:指定start和end

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的內存地址:%p|s1的底層數組首地址:%p|s1的長度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// s1[1:4],展示元素索引1,2,3的元素。s5 := s1[1:4]fmt.Printf("s5的內存地址:%p|s5的底層數組首地址:%p|s5的長度:%d|s5的容量:%d|s5的元素:%v\n", &s5, &s5[0], len(s5), cap(s5), s5)}
===========調試結果===========
s1的內存地址:0xc00009a060|s1的底層數組首地址:0xc0000c8030|s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s5的內存地址:0xc00009a090|s5的底層數組首地址:0xc0000c8038|s5的長度:3|s5的容量:4|s5的元素:[30 50 70]

s5此處的切片長度為:3
s5此處的切片容量為:4
那這個長度和容量是怎么計算出來的?
子切片長度計算方式:end減去start
子切片容量計算方式:從偏移量(start索引)開始到切片底層數組的最后一個元素。

2.4.4 示例四:start和end相同

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的內存地址:%p|s1的底層數組首地址:%p|s1的長度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 該子切片會復制一個新的header,偏移一個元素,子切片長度為0,容量為4s7 := s1[1:1] // 子切片元素超界了,這里是不能顯示的fmt.Printf("s7的內存地址:%p|s7的底層數組首地址:%p|s7的長度:%d|s7的容量:%d|s7的元素:%v\n", &s7, &s7[0], len(s7), cap(s7), s7)}

注意看s1[1:1],這里實際上已經超界了,長度為0,容量為4,如下圖,并且執行的時候會報錯。
在這里插入圖片描述

然后基于現在的代碼,對s7進行append操作,看看會發生什么。

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的長度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)s7 := s1[1:1]fmt.Printf("s7的長度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)s7 = append(s7, 300, 400)fmt.Printf("s1的長度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)fmt.Printf("s7的長度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)}
===========調試結果===========
s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s7的長度:0|s7的容量:4|s7的元素:[]
s1的長度:5|s1的容量:5|s1的元素:[10 300 400 70 90]
s7的長度:2|s7的容量:4|s7的元素:[300 400]

可以看到,最開始s7長度為0(啥也看不到了),容量為4,append后長度變成了2,容量不變。
并且由于s7和s1共享同一個底層數組,所以對應s1切片中索引1和2的元素也被改變了。
為什么是索引1和2?
因為最開始s7 := s1[1:1],這里start是從1開始的,對應的就是s1切片元素中的索引1。
在這里插入圖片描述

再來看一個特殊示例

package mainimport "fmt"func main() {// 聲明并初始化一個長度和容量都為5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范圍[0,4] 0到4fmt.Printf("s1的長度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)s9 := s1[5:5] //長度為0,容量為0,類似[]int{}定義方式fmt.Printf("s9的長度:%d|s9的容量:%d|s9的元素:%v\n", len(s9), cap(s9), s9)
}
===========調試結果===========
s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s9的長度:0|s9的容量:0|s9的元素:[]

為什么還能寫成s9 := s1[5:5]?按索引來算不是超界了嗎?
注意:指定start和end時,除了能使用元素對應的索引,還能夠使用的最大值是切片的容量,s1切片的容量是5。
在這里插入圖片描述

2.4.5 子切片總結

可以看出,上面所有示例操作都是從同一個底層數組上取的段,所以子切片和原始切片共用同一個底層數組。

  • start默認為0,end默認為len(slice)即切片長度,明確定義時可以使用的最大值為切片的容量。
  • 通過指針(切片內存地址)確定底層數組從哪里開始共享。
  • 切片長度計算方法是end - start。
  • 切片容量計算方式是底層數組從偏移的元素(start)到結尾還有幾個元素。

2.5 切片總結

  1. 使用slice[start:end]表示切片,切片長度為end-start,前包后不包。
  2. start缺省(不寫),表示從索引0開始。
  3. end缺省(不寫),表示直接取到末尾,包含最后一個元素,特別注意這個值是len(slice)即切片長度,不是容量,如a1[5:]相當于a1[5:len(a1)]
  4. start和end都缺省,表示從頭到尾。
  5. start和end同時給出,要求end >= start。
  6. start、end最大都不可以超過容量值。
  7. 假設當前容量是8,長度為5,有以下情況:
    a1[:8],可以,end最多寫成8(因為后不包),a1[:9]不可以。
    a1[8:],不可以,end缺省為5,等價于a1[8:5]。
    a1[8:8],可以,但這個切片容量和長度都為0了。
    a1[7:7],可以,但這個切片長度為0,容量為1。
    a1[0:0],可以,但這個切片長度為0,容量為8。
    a1[:8],可以,這個切片長度為8,容量為8,這8個元素都是原序列的。
    a1[1:5],可以,這個切片長度為4,容量為7,相當于跳過了原序列第一個元素。
  8. 切片剛產生時,和原序列(數組、切片)開始共用同一個底層數組,但是每一個切片都自己獨立保存著指針、cap和len。
  9. 一旦一個切片擴容,就和原來共用一個底層數組的序列分道揚鑣,從此陌路。

3. 對數組進行切片

數組也可以切片,但是會生成新的切片

package mainimport "fmt"func main() {// 在[]中加個5,就變成了長度和容量都為5的數組s1 := [5]int{10, 30, 50, 70, 90}fmt.Printf("s1的內存地址:%p|s1的底層數組地址:%p|s1的長度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 數組拷貝,多一個副本出來,元素完全相同s2 := s1fmt.Printf("s2的內存地址:%p|s2的底層數組地址:%p|s2的長度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)s3 := s1[:]//這個切片操作,會產生一個新的底層數組嗎?fmt.Printf("s3的內存地址:%p|s3的底層數組地址:%p|s3的長度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========調試結果===========
s1的內存地址:0xc0000d8030|s1的底層數組地址:0xc0000d8030|s1的長度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的內存地址:0xc0000d80c0|s2的底層數組地址:0xc0000d80c0|s2的長度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的內存地址:0xc0000aa060|s3的底層數組地址:0xc0000d8030|s3的長度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

可與看到,對數組進行切片后,切片的底層數組其實就是s1數組,說明對數組切片,不會誕生一個新的底層數組。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/23313.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/23313.shtml
英文地址,請注明出處:http://en.pswp.cn/web/23313.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

單片機排水泵高壓方案

靈動微多顆算力高、高可靠性的通用系列和電機專用系列MCU&#xff0c;配合成熟的控制算法&#xff0c;覆蓋了包括洗衣機在內的各種大小家電市場。 RAMSUN提供的MM32 MCU種類較多&#xff0c;例如洗衣機內部的排水泵系統&#xff0c;排水泵控制首選電控高性價比產品MM32SPIN023…

JavaWeb_SpringBootWeb案例

環境搭建&#xff1a; 開發規范 接口風格-Restful&#xff1a; 統一響應結果-Result&#xff1a; 開發流程&#xff1a; 第一步應該根據需求定義表結構和定義接口文檔 注意&#xff1a; 本文代碼從上往下一直添加功能&#xff0c;后面的模塊下的代碼包括前面的模塊&#xff0c…

Xmind Pro 2024 專業版激活碼(附下載鏈接)

說到思維導圖&#xff0c;就不能不提 Xmind。這是一款優秀的思維導圖工具&#xff0c;擁有著豐富的導圖模板&#xff0c;漂亮的界面和配色&#xff0c;以及各種各樣的創意工具。 新架構速度更快 采用全新 Snowdancer 引擎&#xff0c;一種堪稱「黑科技」的先進圖形渲染技術。…

翹首以盼的抗鋸齒

Antialiasing 實際的圖形學中是怎么實現反走樣的呢&#xff1f; 我們不希望實際產出的圖形有鋸齒效果&#xff0c;那怎么辦呢&#xff1f; 從采樣的理論開始談起吧 Simpling theory 照片也是一種采樣&#xff0c;把景象打散成像素放到屏幕上的過程&#xff1a; 還可以在不…

14、企業數據資源相關會計處理暫行規定

為規范企業數據資源相關會計處理, 強化相關會計信息披露, 根據《中華人民共和國會計法》 和企業會計準則等相關規定, 現對企業數據資源的相關會計處理規定如下: 一、 關于適用范圍 本規定適用于企業按照企業會計準則相關規定確認為無形資產或存貨等資產類別的數據資源,以…

21 - 即時食物配送 II(高頻 SQL 50 題基礎版)

21 - 即時食物配送 II -- sum(if(order_datecustomer_pref_delivery_date,1,0))/count(*)sum(order_datecustomer_pref_delivery_date)/count(*) -- count(*),表示數據的行數&#xff0c;如果有分組&#xff0c;為分組后數據的行數select round(100*sum(if(order_datecustomer_…

【名詞解釋】Unity的Button組件及其使用示例

Unity的Button組件是Unity引擎中UI系統的一部分&#xff0c;它允許用戶創建可交互的按鈕&#xff0c;用戶可以點擊這些按鈕來觸發事件。Button組件通常用于游戲界面中&#xff0c;比如開始游戲、暫停游戲、選擇選項等。 Button組件的主要屬性包括&#xff1a; interactable: …

原來Stable Diffusion是這樣工作的

stable diffusion是一種潛在擴散模型&#xff0c;可以從文本生成人工智能圖像。為什么叫做潛在擴散模型呢&#xff1f;這是因為與在高維圖像空間中操作不同&#xff0c;它首先將圖像壓縮到潛在空間中&#xff0c;然后再進行操作。 在這篇文章中&#xff0c;我們將深入了解它到…

達摩院重大“遺產”!fluxonium量子比特初始化300納秒且保真度超過99%

通用量子計算機開發的主要挑戰之一是制備量子比特。十多年來&#xff0c;研究人員在構建量子計算機的過程中主要使用了transmon量子比特&#xff0c;這也是迄今為止商業上最成功的超導量子比特。 但與業界多數選擇transmon量子比特不同&#xff0c;&#xff08;前&#xff09;…

npm運行報錯:無法加載文件 C:\Program Files\nodejs\npm.ps1,因為在此系統上禁止運行腳本問題解決

問題其實已經顯而易見了 系統禁止運行腳本 以管理員身份運行 PowerShell&#xff1a; 右鍵點擊“開始”按鈕或按 Win X&#xff0c;然后選擇“Windows PowerShell(管理員)”。 查看當前執行策略&#xff1a; 在 PowerShell 中輸入以下命令來查看當前的執行策略&#xff1a; G…

Python文本處理利器:jieba庫全解析

文章目錄 Python文本處理利器&#xff1a;jieba庫全解析第一部分&#xff1a;背景和功能介紹第二部分&#xff1a;庫的概述第三部分&#xff1a;安裝方法第四部分&#xff1a;常用庫函數介紹1. 精確模式分詞2. 全模式分詞3. 搜索引擎模式分詞4. 添加自定義詞典5. 關鍵詞提取 第…

服務器遭遇UDP攻擊時的應對與解決方案

UDP攻擊作為分布式拒絕服務(DDoS)攻擊的一種常見形式&#xff0c;通過發送大量的UDP數據包淹沒目標服務器&#xff0c;導致網絡擁塞、服務中斷。本文旨在提供一套實用的策略與技術手段&#xff0c;幫助您識別、緩解乃至防御UDP攻擊&#xff0c;確保服務器穩定運行。我們將探討監…

最新PHP眾籌網站源碼 支持報名眾籌+商品眾籌+公益眾籌等多種眾籌模式 含完整代碼包和部署教程

在當今互聯網飛速發展的時代&#xff0c;眾籌模式逐漸成為了創新項目、商品銷售和公益活動融資的重要渠道。分享一款最新版的PHP眾籌網站源碼&#xff0c;支持報名眾籌、商品眾籌和公益眾籌等多種眾籌模式。該源碼包含了完整的代碼包和詳細的部署教程&#xff0c;讓新手也可以輕…

利用醫學Twitter進行病理圖像分析的視覺-語言基礎模型| 文獻速遞-視覺通用模型與疾病診斷

Title 題目 A visual–language foundation model for pathology image analysis using medical Twitter 利用醫學Twitter進行病理圖像分析的視覺-語言基礎模型 01 文獻速遞介紹 缺乏公開可用的醫學圖像標注是計算研究和教育創新的一個重要障礙。同時&#xff0c;許多醫生…

自動化測試-Selenium(一),簡介

自動化測試-Selenium 1. 什么是自動化測試 1.1 自動化測試介紹 自動化測試是一種通過自動化工具執行測試用例來驗證軟件功能和性能的過程。與手動測試不同&#xff0c;自動化測試使用腳本和軟件來自動執行測試步驟&#xff0c;記錄結果&#xff0c;并比較預期輸出和實際輸出…

【Python報錯】已解決ModuleNotFoundError: No module named ‘timm’

成功解決“ModuleNotFoundError: No module named ‘timm’”錯誤的全面指南 一、引言 在Python編程中&#xff0c;經常會遇到各種導入模塊的錯誤&#xff0c;其中“ModuleNotFoundError: No module named ‘timm’”就是一個典型的例子。這個錯誤意味著你的Python環境中沒有安…

Navicate 導入導出數據庫

導出數據庫 找地方存在來&#xff0c;別忘了放在那里。 新建一個數據庫&#xff0c;記得要和導出數據庫的 字符集與排序規則 相同 打開數據庫后&#xff0c;我們選擇它&#xff08;就是單擊它&#xff09;然后右鍵打開菜單-運行sql文件 找到剛才存儲的位置&#xff0c;開始 &a…

大中小面積紫外光老化加速試驗機裝置

高低溫試驗箱,振動試驗臺,紫外老化試驗箱,氙燈老化試驗箱,沙塵試驗箱,箱式淋雨試驗箱,臭氧老化試驗箱,換氣老化試驗箱,電熱鼓風干燥箱,真空干燥箱&#xff0c;超聲波清洗機&#xff0c;鹽霧試驗箱 一、產品用途 紫外光加速老化試驗裝置采用熒光紫外燈為光源,通過模擬自然陽光中…

oracle報錯ORA-01940: cannot drop a user that is currently connected解決方法

目錄 一.原因 二.解決方法 1.查詢活動會話 2.記下SID和SERIAL# 3.斷開會話 4.刪除用戶 一.原因 ORA-01940代表你正在刪除一個有活動會話的用戶 二.解決方法 1.查詢活動會話 SQL> SELECT sid, serial#, username, programFROM v$sessionWHERE username 你要刪除的u…

重寫mybatisPlus自定義ID生成策略

1.項目中需要引入mybatisplus核心組件 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mp.version}</version></dependency> 2.新建一個類實現IdentifierGenera…