Go 語言切片是如何擴容的?以下這篇文章為大家介紹一下Go 語言中切片的擴容機制,希望對大家有幫助!
在 Go 語言中,有一個很常用的資料結構,那就是切片(Slice)。
切片是一個擁有相同類型元素的可變長度的序列,它是基於陣列類型所做的一層封裝。它非常靈活,支援自動擴容。
切片是一種引用類型,它有三個屬性:指標,長度和容量。
底層原始碼定義如下:
type slice struct { array unsafe.Pointer len int cap int }
- #指標: 指向 slice 可以存取到的第一個元素。
- 長度: slice 中元素個數。
- 容量: slice 起始元素到底層陣列最後一個元素間的元素個數。
例如使用make([]byte, 5)
建立一個切片,它看起來是這樣的:
##宣告和初始化
切片的使用還是比較簡單的,這裡舉個例子,直接看程式碼吧。func main() { var nums []int // 声明切片 fmt.Println(len(nums), cap(nums)) // 0 0 nums = append(nums, 1) // 初始化 fmt.Println(len(nums), cap(nums)) // 1 1 nums1 := []int{1,2,3,4} // 声明并初始化 fmt.Println(len(nums1), cap(nums1)) // 4 4 nums2 := make([]int,3,5) // 使用make()函数构造切片 fmt.Println(len(nums2), cap(nums2)) // 3 5 }
擴容時機
當切片的長度超過其容量時,切片會自動擴容。這通常發生在使用append 函數向切片中新增元素時。
擴充功能會指派新陣列並複製元素,因此可能會影響效能。如果你知道要添加多少元素,可以使用 make 函數預先分配足夠大的切片來避免頻繁擴容。
append 函數,簽章如下:
func Append(slice []int, items ...int) []int
append 函數參數長度可變,可以追加多個值,還可以直接追加一個切片。使用起來比較簡單,分別看兩個例子:
追加多個值:
package main import "fmt" func main() { s := []int{1, 2, 3} fmt.Println("初始切片:", s) s = append(s, 4, 5, 6) fmt.Println("追加多个值后的切片:", s) }輸出結果為:
初始切片: [1 2 3] 追加多个值后的切片: [1 2 3 4 5 6]再來看一下直接
追加一個切片:
package main import "fmt" func main() { s1 := []int{1, 2, 3} fmt.Println("初始切片:", s1) s2 := []int{4, 5, 6} s1 = append(s1, s2...) fmt.Println("追加另一个切片后的切片:", s1) }輸出結果為:
初始切片: [1 2 3] 追加另一个切片后的切片: [1 2 3 4 5 6]再來看一個
發生擴容的範例:
package main import "fmt" func main() { s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片 fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s) for i := 1; i <= 5; i++ { s = append(s, i) // 向切片中添加元素 fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s) } }輸出結果為:
初始状态: len=0 cap=3 [] 添加元素1: len=1 cap=3 [1] 添加元素2: len=2 cap=3 [1 2] 添加元素3: len=3 cap=3 [1 2 3] 添加元素4: len=4 cap=6 [1 2 3 4] 添加元素5: len=5 cap=6 [1 2 3 4 5]在這個範例中,我們建立了一個長度為
0,容量為
3 的切片。然後,我們使用
append 函數在切片中新增
5 個元素。
4 個元素時,切片的長度超過了其容量。此時,切片會自動擴容。新的容量是原始容量的兩倍,即
6。
原始碼分析
在 Go 語言的原始碼中,切片擴容通常是在進行切片的append 操作時觸發的。在進行
append 操作時,如果切片容量不足以容納新的元素,就需要對切片進行擴容,此時就會呼叫
growslice 函數進行擴容。
growslice 函數定義在 Go 語言的 runtime 套件中,它的呼叫是在編譯後的程式碼中實現的。具體來說,當執行
append 操作時,編譯器會將其轉換為類似下面的程式碼:
slice = append(slice, elem)在上述程式碼中,如果切片容量不足以容納新的元素,則會呼叫
growslice 函數進行擴容。所以
growslice 函數的呼叫是
由編譯器在產生的機器碼中實現的,而不是在原始碼中明確調用的。
package main import "fmt" func main() { s := make([]int, 0) oldCap := cap(s) for i := 0; i < 2048; i++ { s = append(s, i) newCap := cap(s) if newCap != oldCap { fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }上述程式碼先建立了一個空的 slice,然後在一個迴圈裡不斷往裡面
append 新元素。
1.17 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 1024 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304運行結果(
1.18 版本):
[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 848 [0 -> 847] cap = 848 | after append 848 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1792 [0 -> 1791] cap = 1792 | after append 1792 cap = 2560根據上面的結果還是能看到差別的,具體擴容策略下面邊看源碼邊說明。
go1.17
扩容调用的是 growslice
函数,我复制了其中计算新容量部分的代码。
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 1024 { newcap = doublecap } else { // 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 } } } // ... return slice{p, old.len, newcap} }
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
go1.18
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... 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 } } } // ... return slice{p, old.len, newcap} }
和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4
。
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
- 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是
newcap + 3*threshold
,直到新容量大于期望容量;
内存对齐
分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。
那是为什么呢?
实际上,growslice
的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize
函数,在计算完 newcap
值之后,还会有一个步骤计算最终的容量:
capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize)
这个函数的实现就不在这里深入了,先挖一个坑,以后再来补上。
总结
切片扩容通常是在进行切片的 append
操作时触发的。在进行 append
操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice
函数进行扩容。
切片扩容分两个阶段,分为 go1.18 之前和之后:
一、go1.18 之前:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
二、go1.18 之后:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
- 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是
newcap + 3*threshold
,直到新容量大于期望容量;
以上就是本文的全部内容,如果觉得还不错的话欢迎点赞,转发和关注,感谢支持。
推荐学习:Golang教程
以上是淺析Go語言的切片如何擴容的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Golang適合快速開發和並發編程,而C 更適合需要極致性能和底層控制的項目。 1)Golang的並發模型通過goroutine和channel簡化並發編程。 2)C 的模板編程提供泛型代碼和性能優化。 3)Golang的垃圾回收方便但可能影響性能,C 的內存管理複雜但控制精細。

goimpactsdevelopmentpositationality throughspeed,效率和模擬性。 1)速度:gocompilesquicklyandrunseff,IdealforlargeProjects.2)效率:效率:ITScomprehenSevestAndardArdardArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdEcceSteral Depentencies,增強的Depleflovelmentimency.3)簡單性。

C 更適合需要直接控制硬件資源和高性能優化的場景,而Golang更適合需要快速開發和高並發處理的場景。 1.C 的優勢在於其接近硬件的特性和高度的優化能力,適合遊戲開發等高性能需求。 2.Golang的優勢在於其簡潔的語法和天然的並發支持,適合高並發服務開發。

Golang在实际应用中表现出色,以简洁、高效和并发性著称。1)通过Goroutines和Channels实现并发编程,2)利用接口和多态编写灵活代码,3)使用net/http包简化网络编程,4)构建高效并发爬虫,5)通过工具和最佳实践进行调试和优化。

Go語言的核心特性包括垃圾回收、靜態鏈接和並發支持。 1.Go語言的並發模型通過goroutine和channel實現高效並發編程。 2.接口和多態性通過實現接口方法,使得不同類型可以統一處理。 3.基本用法展示了函數定義和調用的高效性。 4.高級用法中,切片提供了動態調整大小的強大功能。 5.常見錯誤如競態條件可以通過gotest-race檢測並解決。 6.性能優化通過sync.Pool重用對象,減少垃圾回收壓力。

Go語言在構建高效且可擴展的系統中表現出色,其優勢包括:1.高性能:編譯成機器碼,運行速度快;2.並發編程:通過goroutines和channels簡化多任務處理;3.簡潔性:語法簡潔,降低學習和維護成本;4.跨平台:支持跨平台編譯,方便部署。

關於SQL查詢結果排序的疑惑學習SQL的過程中,常常會遇到一些令人困惑的問題。最近,筆者在閱讀《MICK-SQL基礎�...


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Atom編輯器mac版下載
最受歡迎的的開源編輯器

Dreamweaver CS6
視覺化網頁開發工具

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能