網站首頁 編程語言 正文
1.數組和切片有什么區別
Go語言中數組是固定長度的,不能動態擴容,在編譯期就會確定大小,聲明方式如下:
var buffer [255]int buffer := [255]int{0}
切片是對數組的抽象,因為數組的長度是不可變的,在某些場景下使用起來就不是很方便,所以Go語言提供了一種靈活,功能強悍的內置類型切片(“動態數組”),與數組相比切片的長度是不固定的,可以追加元素。切片是一種數據結構,切片不是數組,切片描述的是一塊數組,切片結構如下:
我們可以直接聲明一個未指定大小的數組來定義切片,也可以使用make()函數來創建切片,聲明方式如下:
var slice []int // 直接聲明 slice := []int{1,2,3,4,5} // 字面量方式 slice := make([]int, 5, 10) // make創建 slice := array[1:5] // 截取下標的方式 slice := *new([]int) // new一個
切片可以使用append追加元素,當cap不足時進行動態擴容。
2.拷貝大切片一定比拷貝小切片代價大嗎
這道題本質是考察對切片本質的理解,Go語言中只有值傳遞,所以我們以傳遞切片為例子:
func main() { param1 := make([]int, 100) param2 := make([]int, 100000000) smallSlice(param1) largeSlice(param2) } func smallSlice(params []int) { // .... } func largeSlice(params []int) { // .... }
切片param2要比param1大1000000個數量級,在進行值拷貝的時候,是否需要更昂貴的操作呢?
實際上不會,因為切片本質內部結構如下:
type SliceHeader struct { Data uintptr Len int Cap int }
切片中的第一個字是指向切片底層數組的指針,這是切片的存儲空間,第二個字段是切片的長度,第三個字段是容量。將一個切片變量分配給另一個變量只會復制三個機器字,大切片跟小切片的區別無非就是 Len 和 Cap的值比小切片的這兩個值大一些,如果發生拷貝,本質上就是拷貝上面的三個字段。
3.切片的深淺拷貝
深淺拷貝都是進行復制,區別在于復制出來的新對象與原來的對象在它們發生改變時,是否會相互影響,本質區別就是復制出來的對象與原對象是否會指向同一個地址。在Go語言,切片拷貝有三種方式:
1.使用=操作符拷貝切片,這種就是淺拷貝
2.使用[:]下標的方式復制切片,這種也是淺拷貝
3.使用Go語言的內置函數copy()進行切片拷貝,這種就是深拷貝
4.零切片 空切片 nil切片是什么
4.1零切片
我們把切片內部數組的元素都是零值或者底層數組的內容就全是 nil的切片叫做零切片,使用make創建的、長度、容量都不為0的切片就是零值切片:
slice := make([]int,5) // 0 0 0 0 0 slice := make([]*int,5) // nil nil nil nil nil
4.2nil切片
nil切片的長度和容量都為0,并且和nil比較的結果為true,采用直接創建切片的方式、new創建切片的方式都可以創建nil切片:
var slice []int var slice = *new([]int)
4.3空切片
空切片的長度和容量也都為0,但是和nil的比較結果為false,因為所有的空切片的數據指針都指向同一個地址 0xc42003bda0;使用字面量、make可以創建空切片:
var slice = []int{} var slice = make([]int, 0)
空切片指向的 zerobase 內存地址是一個神奇的地址,從 Go 語言的源代碼中可以看到它的定義:
// base address for all 0-byte allocations var zerobase uintptr // 分配對象內存 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size == 0 { return unsafe.Pointer(&zerobase) } ... }
5.切片的擴容策略
這個問題是一個高頻考點,我們通過源碼來解析一下切片的擴容策略,切片的擴容都是調用growslice方法,截取部分重要源代碼:
1.17之前
代碼的擴容策略可以簡述為以下三個規則:
1.當期望容量 > 兩倍的舊容量時,直接使用期望容量作為新切片的容量
2.如果舊容量 < 1024(注意這里單位是元素個數),那么直接翻倍舊容量
3.如果舊容量 > 1024,那么會進入一個循環,每次增加25%直到大于期望容量
可以看到,原來的go對于切片擴容后的容量判斷有一個明顯的magic number:1024,在1024之前,增長的系數是2,而1024之后則變為1.25。關于為什么會這么設計,社區的相關討論1給出了幾點理由:
1.如果只選擇翻倍的擴容策略,那么對于較大的切片來說,現有的方法可以更好的節省內存。
2.如果只選擇每次系數為1.25的擴容策略,那么對于較小的切片來說擴容會很低效。
3.之所以選擇一個小于2的系數,在擴容時被釋放的內存塊會在下一次擴容時更容易被重新利用
// runtime/slice.go // et:表示slice的一個元素;old:表示舊的slice;cap:表示新切片需要的容量; func growslice(et *_type, old slice, cap int) slice { if cap < old.cap { panic(errorString("growslice: cap out of range")) } if et.size == 0 { // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap // 兩倍擴容 doublecap := newcap + newcap // 新切片需要的容量大于兩倍擴容的容量,則直接按照新切片需要的容量擴容 if cap > doublecap { newcap = cap } else { // 原 slice 容量小于 1024 的時候,新 slice 容量按2倍擴容 if old.cap < 1024 { newcap = doublecap } else { // 原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。 // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } // 后半部分還對 newcap 作了一個內存對齊,這個和內存分配策略相關。進行內存對齊之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。 var overflow bool var lenmem, newlenmem, capmem uintptr // Specialize for common values of et.size. // For 1 we don't need any division/multiplication. // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant. // For powers of 2, use a variable shift. switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } }
1.18之后
到了Go1.18時,又改成不和1024比較了,而是和256比較;并且擴容的增量也有所變化,不再是每次擴容1/4,如下代碼所示:
//1.18 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
在1.18中,優化了切片擴容的策略2,讓底層數組大小的增長更加平滑:
通過減小閾值并固定增加一個常數,使得優化后的擴容的系數在閾值前后不再會出現從2到1.25的突變,該commit作者給出了幾種原始容量下對應的“擴容系數”:
6. 參數傳遞切片和切片指針有什么區別
我們都知道切片底層就是一個結構體,里面有三個元素:
分別表示切片底層數據的地址,切片長度,切片容量。
type SliceHeader struct { Data uintptr Len int Cap int }
當切片作為參數傳遞時,其實就是一個結構體的傳遞,因為Go語言參數傳遞只有值傳遞,傳遞一個切片就會淺拷貝原切片,但因為底層數據的地址沒有變,所以在函數內對切片的修改,也將會影響到函數外的切片,舉例:
func modifySlice(s []string) { s[0] = "song" s[1] = "Golang" fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang夢工廠"} modifySlice(s) fmt.Println("inner slice: ", s) }
// 運行結果
out slice: ?[song Golang]
inner slice: ?[song Golang]
不過這也有一個特例,先看一個例子:
func appendSlice(s []string) { s = append(s, "快關注!!") fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang夢工廠"} appendSlice(s) fmt.Println("inner slice: ", s) }
// 運行結果
out slice: ?[asong Golang夢工廠 快關注!!]
inner slice: ?[asong Golang夢工廠]
因為切片發生了擴容,函數外的切片指向了一個新的底層數組,所以函數內外不會相互影響,因此可以得出一個結論,當參數直接傳遞切片時,如果指向底層數組的指針被覆蓋或者修改(重分配、append觸發擴容),此時函數內部對數據的修改將不再影響到外部的切片,代表長度的len和容量cap也均不會被修改
參數傳遞切片指針就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底層數組,則應該按指針傳遞。
7.range遍歷切片有什么要注意的
Go語言提供了range關鍵字用于for 循環中迭代數組(array)、切片(slice)、通道(channel)或集合(map)的元素,有兩種使用方式:
for k,v := range _ { } for k := range _ { }
第一種是遍歷下標和對應值,第二種是只遍歷下標,使用range遍歷切片時會先拷貝一份,然后在遍歷拷貝數據:
s := []int{1, 2} for k, v := range s { } 會被編譯器認為是 for_temp := s len_temp := len(for_temp) for index_temp := 0; index_temp < len_temp; index_temp++ { value_temp := for_temp[index_temp] k := index_temp v := value_temp }
不知道這個知識點的情況下很容易踩坑,例如下面這個例子:
package main import ( "fmt" ) type user struct { name string age uint64 } func main() { u := []user{ {"asong",23}, {"song",19}, {"asong2020",18}, } for _,v := range u{ if v.age != 18{ v.age = 20 } } fmt.Println(u) }
// 運行結果
[{asong 23} {song 19} {asong2020 18}]
因為使用range遍歷切片u,變量v是拷貝切片中的數據,修改拷貝數據不會對原切片有影響。
原文鏈接:https://blog.csdn.net/qq_53267860/article/details/126772653
相關推薦
- 2022-08-18 R語言ComplexHeatmap繪制復雜熱圖heatmap_R語言
- 2022-07-02 less與sass(scss)的區別
- 2022-03-19 C語言數據的存儲詳解_C 語言
- 2022-07-22 mybatis源碼之集成spring原理詳解
- 2022-09-02 Pytorch-LSTM輸入輸出參數方式_python
- 2022-10-08 ASP.NET堆和棧四之對托管和非托管資源的垃圾回收和內存分配_實用技巧
- 2022-05-28 Python實現孤立隨機森林算法的示例代碼_python
- 2022-04-09 iOS浮點類型精度問題的原因與解決辦法_IOS
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支