一文詳解Go語言中切片的底層原理
大家好,我是二條,在上一篇我們學(xué)習(xí)了輕松理解Go中的內(nèi)存逃逸問題,今天接著我們學(xué)習(xí)Go中切片的相關(guān)知識(shí)。本文不會(huì)單獨(dú)去講解切片的基礎(chǔ)語法,只會(huì)對(duì)切片的底層和在開發(fā)中需要注意的事項(xiàng)作分析。
在Go語言中,切片作為一種引用類型數(shù)據(jù),相對(duì)數(shù)組而言是一種動(dòng)態(tài)長度的數(shù)據(jù)類型,使用的場景也是非常多。但在使用切片的過程中,也有許多需要注意的事項(xiàng)。例如切片函數(shù)傳值、切片動(dòng)態(tài)擴(kuò)容、切片對(duì)底層數(shù)組的引用問題等等。今天分享的主題,就是圍繞切片進(jìn)行。
切片的函數(shù)傳值
切片作為一種引用數(shù)據(jù)類型,在作為函數(shù)傳值時(shí),如果函數(shù)內(nèi)部對(duì)切片做了修改,會(huì)影響到原切片上。
package?main
import?"fmt"
func?main()?{
?sl1?:=?make([]int,?10)
?for?i?:=?0;?i?<?10;?i++?{
?}
?fmt.Println("切片sl1的值是",?sl1)
?change(sl1)
?fmt.Println("切片sl2的值是",?sl1)
}
func?change(sl?[]int)?{
?sl[0]?=?100
?fmt.Println("形參sl切片的值是",?sl)
}打印上述代碼:
切片sl1的值是 [1 2 3 4 5 6 7 8 9 10]
形參sl切片的值是 [100 2 3 4 5 6 7 8 9 10]
切片sl2的值是 [100 2 3 4 5 6 7 8 9 10]
通過上面的結(jié)果,不難看出來,在函數(shù)change()中修改了切片,原切片的小標(biāo)0的值也發(fā)生了改變。這是因?yàn)榍衅且环N引用類型數(shù)據(jù),在傳遞到函數(shù)change()時(shí),使用的都是相同的底層數(shù)組(切片底層本質(zhì)仍是一個(gè)數(shù)組)。因此,底層數(shù)組的值改變了,就會(huì)影響到其他指向該數(shù)組的切片上。
針對(duì)上述的問題,有什么解決方案,使得傳遞切片,不會(huì)影響原切片的值呢?可以采用切片復(fù)制的方式,重新創(chuàng)建一個(gè)新的切片當(dāng)做函數(shù)的參數(shù)進(jìn)行傳遞。
package?main
import?"fmt"
func?main()?{
?sl1?:=?make([]int,?10)
?for?i?:=?0;?i?<?10;?i++?{
??sl1[i]?=?i?+?1
?}
?fmt.Println("切片sl1的值是",?sl1)
?//?創(chuàng)建一個(gè)新的切片,當(dāng)做參數(shù)傳遞。
?sl2?:=?make([]int,?10)
?copy(sl2,?sl1)
?change(sl2)
?fmt.Println("切片sl2的值是",?sl1)
}
func?change(sl?[]int)?{
?sl[0]?=?100
?fmt.Println("形參sl切片的值是",?sl)
}打印上述代碼:
切片sl1的值是 [1 2 3 4 5 6 7 8 9 10]
形參sl切片的值是 [100 2 3 4 5 6 7 8 9 10]
切片sl2的值是 [1 2 3 4 5 6 7 8 9 10]
通過上述運(yùn)行結(jié)果,在change函數(shù)中,對(duì)切片下標(biāo)為0做了值修改,對(duì)切片sl1的值沒有影響。
切片動(dòng)態(tài)擴(kuò)容機(jī)制
在Go中,切片是一種動(dòng)態(tài)長度的引用數(shù)據(jù)類型。當(dāng)切片的容量不足以容納新增加的元素時(shí),底層會(huì)實(shí)現(xiàn)自動(dòng)擴(kuò)容用來存儲(chǔ)新添加的元素。先查看下面的一段實(shí)例代碼,證明切片存在動(dòng)態(tài)擴(kuò)容。
package?main
import?"fmt"
func?main()?{
?var?sl1?[]int
?fmt.Println("切片sl1的長度是",?len(sl1),?",容量是",?cap(sl1))
?for?i?:=?0;?i?<?10;?i++?{
??sl1?=?append(sl1,?i)
??fmt.Println("切片sl1的長度是",?len(sl1),?",容量是",?cap(sl1))
?}
}打印上述代碼:
切片sl1的長度是 0 ,容量是 0
切片sl1的長度是 1 ,容量是 1
切片sl1的長度是 2 ,容量是 2
切片sl1的長度是 3 ,容量是 4
切片sl1的長度是 4 ,容量是 4
切片sl1的長度是 5 ,容量是 8
切片sl1的長度是 6 ,容量是 8
切片sl1的長度是 7 ,容量是 8
切片sl1的長度是 8 ,容量是 8
切片sl1的長度是 9 ,容量是 16
切片sl1的長度是 10 ,容量是 16
可以看出,切片的長度是隨著for操作,依次遞增。但切片的容量就不是依次遞增,從明面上看,有點(diǎn)像以2的倍數(shù)在增加。具體增加的規(guī)律是怎么樣的呢?
要弄明白Go中的切片是如何實(shí)現(xiàn)擴(kuò)容的,這就需要關(guān)注一下Go的版本。
在Go的1.18版本以前,是按照如下的規(guī)則來進(jìn)行擴(kuò)容:
1、如果原有切片的長度小于 1024,那么新的切片容量會(huì)直接擴(kuò)展為原來的 2 倍。
2、如果原有切片的長度大于等于 1024,那么新的切片容量會(huì)擴(kuò)展為原來的 1.25 倍,這一過程可能需要執(zhí)行多次才能達(dá)到期望的容量。
3、如果切片屬于第一種情況(長度小于 1024)并且需要擴(kuò)容的容量小于 1024 字節(jié),那么新的切片容量會(huì)直接增加到原來的長度加上需要擴(kuò)容的容量(新容量=原容量+擴(kuò)容容量)。
從Go的1.18版本開始,是按照如下的規(guī)則進(jìn)行擴(kuò)容:
1、當(dāng)原slice容量(oldcap)小于256的時(shí)候,新slice(newcap)容量為原來的2倍。
2、原slice容量超過256,新slice容量newcap = oldcap + (oldcap+3*256) / 4。
使用上面的代碼,將循環(huán)的值調(diào)到非常大,例如10w,甚至更大,你會(huì)發(fā)現(xiàn)切片的容量和長度始終是比較趨近,而不是差距很大。
例如我將循環(huán)設(shè)置到100w,這里就只打印最后幾行結(jié)果,不進(jìn)行全部打印。
package?main
import?"fmt"
func?main()?{
?var?sl1?[]int
?fmt.Println("切片sl1的長度是",?len(sl1),?",容量是",?cap(sl1))
?for?i?:=?0;?i?<?1000000;?i++?{
??sl1?=?append(sl1,?i)
??fmt.Println("切片sl1的長度是",?len(sl1),?",容量是",?cap(sl1))
?}
}打印上述代碼結(jié)果為:
.................
切片sl1的長度是 999990 ,容量是 1055744
切片sl1的長度是 999991 ,容量是 1055744
切片sl1的長度是 999992 ,容量是 1055744
切片sl1的長度是 999993 ,容量是 1055744
切片sl1的長度是 999994 ,容量是 1055744
切片sl1的長度是 999995 ,容量是 1055744
切片sl1的長度是 999996 ,容量是 1055744
切片sl1的長度是 999997 ,容量是 1055744
切片sl1的長度是 999998 ,容量是 1055744
切片sl1的長度是 999999 ,容量是 1055744
切片sl1的長度是 1000000 ,容量是 1055744
上面講到的不同版本之間的規(guī)律,這個(gè)規(guī)律是怎么來的,我們可以直接源代碼。
首先看1.18版本開始的底層代碼,你需要找到Go的源碼文件,路徑為runtime/slice.go,該文件中有一個(gè)名為growslice()函數(shù)。這個(gè)函數(shù)的代碼很長,我們重點(diǎn)關(guān)注下述代碼,其他的代碼除了做一些邏輯處理,還處理了內(nèi)存對(duì)齊問題,關(guān)于內(nèi)存對(duì)齊就不在本篇提及。
//?type切片期望的類型,old舊切片,cap新切片期望最小的容量
func?growslice(et?*_type,?old?slice,?cap?int)?slice?{
??newcap?:=?old.cap//?老切片容量
??doublecap?:=?newcap?+?newcap//?老切片容量的兩倍
??if?cap?>?doublecap?{//?期望最小的容量?>?老切片的兩倍(新切片的容量?=?2?*?老切片的容量)
????newcap?=?cap
??}?else?{
????const?threshold?=?256
????if?old.cap?<?threshold?{
??????newcap?=?doublecap
????}?else?{
??????for?0?<?newcap?&&?newcap?<?cap?{
????????//?在2倍增長以及1.25倍之間尋找一種相對(duì)平衡的規(guī)則
????????newcap?+=?(newcap?+?3*threshold)?/?4
??????}
??????if?newcap?<=?0?{
????????newcap?=?cap
??????}
????}
??}
}接著來看1.18版本之前的源代碼,可以直接通過GitHub上進(jìn)行查看。1.16GitHub源碼地址。
//?type切片期望的類型,old舊切片,cap新切片期望最小的容量
func?growslice(et?*_type,?old?slice,?cap?int)?slice?{
???newcap?:=?old.cap
?doublecap?:=?newcap?+?newcap
?if?cap?>?doublecap?{//?需要兩倍擴(kuò)容時(shí),則直接擴(kuò)容為兩倍
??newcap?=?cap
?}?else?{
??if?old.cap?<?1024?{//?小于1024,直接擴(kuò)容為2倍
???newcap?=?doublecap
??}?else?{
???//?原?slice?容量超過?1024,新?slice?容量變成原來的1.25倍
???for?0?<?newcap?&&?newcap?<?cap?{
????newcap?+=?newcap?/?4
???}
???if?newcap?<=?0?{
????newcap?=?cap
???}
??}
?}
}通過上述的代碼,已經(jīng)總結(jié)出切片擴(kuò)容的規(guī)律。如果你在實(shí)際的案例中,并非按照總結(jié)的規(guī)律進(jìn)行擴(kuò)容,這是因?yàn)榍衅瑪U(kuò)容之后還考慮了內(nèi)存對(duì)齊問題,也就是上述growslic()函數(shù)剩余部分。
切片操作對(duì)數(shù)組的影響
在Go中,切片和數(shù)組有一些共性,也有一些不同之處。
相同之處:
1、切片和數(shù)組在定義時(shí),需要指定內(nèi)部的元素類型,一旦定義元素類型,就只能存儲(chǔ)該類型的元素。
2、切片雖然是單獨(dú)的一種類型,底層仍然是一個(gè)數(shù)組,在Go源碼中,有這樣一段定義,通過閱讀這段代碼,可以總結(jié)出切片底層是一個(gè)struct數(shù)據(jù)結(jié)構(gòu)。
type?slice?struct?{
?array?unsafe.Pointer?#?指向底層數(shù)組的指針
?len???int?#?切片的長度,也就是說當(dāng)前切片中的元素個(gè)數(shù)
?cap???int?#?切片的容量,也就是說切片最大能夠存儲(chǔ)多少個(gè)元素
}
不同之處:
1、切片和數(shù)組最大的不同之處,在于切片的長度和容量是動(dòng)態(tài)的,可以根據(jù)實(shí)際情況實(shí)現(xiàn)動(dòng)態(tài)擴(kuò)容,而數(shù)組是固定長度,一經(jīng)定義長度,存儲(chǔ)的元素就不能超過定義時(shí)的長度。
下面有這樣一種場景,需要特別注意。
從一個(gè)切片中生產(chǎn)新的切片,使用截取實(shí)現(xiàn)。
func?clipSliceBySlice()?{
?s?:=?make([]int,?1000000)
?start?:=?time.Now()
?_?=?s[0:500000]
?elapsed?:=?time.Since(start)
?fmt.Printf("Time?taken?to?generate?slice?from?slice:?%s\n",?elapsed)
}從一個(gè)切片中生成新的切片,使用copy()函數(shù)實(shí)現(xiàn)。
func?clipSliceByCopy()?{
?s?:=?make([]int,?1000000)
?start?:=?time.Now()
?s2?:=?make([]int,?500000)
?copy(s2,?s[0:500000])
?elapsed?:=?time.Since(start)
?fmt.Printf("Time?taken?to?copy?slice?using?copy()?function:?%s\n",?elapsed)
}這兩段代碼,都是從一個(gè)切片中生成一個(gè)新的切片,但誰的性能效果更好呢?
1、第一種方式,生成新切片,底層仍然與原切片共用一個(gè)底層數(shù)組。在生成切片時(shí),效率會(huì)更高一些。但存在一個(gè)問題,如果原切片和新切片對(duì)自身的元素做了修改,底層數(shù)組也會(huì)隨著改變,這樣會(huì)導(dǎo)致另外一個(gè)切片也跟著受影響。這種方式雖然效率更高,但是共用同一個(gè)底層數(shù)組,會(huì)存在數(shù)據(jù)安全問題。
2、第二種方式,生成新切片,使用的是copy()函數(shù)實(shí)現(xiàn),會(huì)發(fā)生一個(gè)內(nèi)存拷貝。這樣新切片就是存儲(chǔ)在新的內(nèi)存中,其底層的數(shù)組和原切片底層的數(shù)組,不在是共享。不管是老切片還是新切片內(nèi)部元素發(fā)生變化,都只會(huì)影響到自身。這種方式雖然消耗的內(nèi)存更大,但數(shù)據(jù)更加安全。
使用歸納
在實(shí)際的開發(fā)過程中,我們一般使用切片的場景要比數(shù)組多,這是為什么呢?
1、動(dòng)態(tài)擴(kuò)展:切片可以動(dòng)態(tài)擴(kuò)展或縮減,而數(shù)組的長度是固定的。使用切片可以更方便地處理不確定長度的數(shù)據(jù)集。
2、內(nèi)存效率:切片的底層實(shí)現(xiàn)是數(shù)組,但是通過切片可以對(duì)底層的數(shù)組進(jìn)行引用,避免了復(fù)制底層數(shù)據(jù)的開銷。因此,使用切片可以更高效地處理大量數(shù)據(jù)。
3、零值初始化:切片有一個(gè)默認(rèn)值為0的長度和容量,這使得初始化切片更加方便。
4、內(nèi)置函數(shù):切片有許多內(nèi)置函數(shù),如append()、copy()等,這些函數(shù)可以更方便地操作切片。
本文總結(jié)
根據(jù)上面的幾個(gè)小問題進(jìn)行演示,我們在日常開發(fā)中,使用切片重點(diǎn)可以關(guān)注在動(dòng)態(tài)擴(kuò)容和引用傳值上面,這也是經(jīng)常出現(xiàn)問題的點(diǎn)。下面細(xì)分幾點(diǎn)進(jìn)行歸納:
1、由于切片是引用類型,因此容易出現(xiàn)多個(gè)變量引用同一個(gè)底層數(shù)組,導(dǎo)致內(nèi)存泄露和意外修改數(shù)據(jù)的情況。
2、當(dāng)切片長度超過底層數(shù)組容量時(shí),可以導(dǎo)致切片重新分配內(nèi)存,這可能會(huì)帶來性能問題。
3、在使用切片時(shí)沒有正確計(jì)算長度和容量,也可能導(dǎo)致意料之外的結(jié)果。
4、切片常常被用作函數(shù)參數(shù),由于其引用類型的特性,可能會(huì)導(dǎo)致函數(shù)內(nèi)對(duì)切片數(shù)據(jù)的修改影響到外部變量。
5、如果切片的底層數(shù)組被修改,可能會(huì)對(duì)所有引用該底層數(shù)組的切片數(shù)據(jù)造成影響。
到此這篇關(guān)于一文詳解Go語言中切片的底層原理的文章就介紹到這了,更多相關(guān)Go切片內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- go中實(shí)現(xiàn)字符切片和字符串互轉(zhuǎn)
- Golang切片連接成字符串的實(shí)現(xiàn)示例
- Go語言之重要數(shù)組類型切片(slice)make,append函數(shù)解讀
- GO語言中創(chuàng)建切片的三種實(shí)現(xiàn)方式
- golang字符串切片去重的幾種算法
- 詳解golang的切片擴(kuò)容機(jī)制
- Go?語言中切片的三種特殊狀態(tài)
- 淺談go中切片比數(shù)組好用在哪
- 一文總結(jié)Go語言切片核心知識(shí)點(diǎn)和坑
- 深入剖析Go語言中數(shù)組和切片的區(qū)別
- Go語言實(shí)戰(zhàn)之切片內(nèi)存優(yōu)化
- Go切片的具體使用
相關(guān)文章
Go語言實(shí)戰(zhàn)學(xué)習(xí)之流程控制詳解
這篇文章主要為大家詳細(xì)介紹了Go語言中的流程控制,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語言有一定的幫助?,需要的朋友可以參考下2022-08-08
Golang服務(wù)中context超時(shí)處理的方法詳解
在Go語言中,Context是一個(gè)非常重要的概念,它存在于一個(gè)完整的業(yè)務(wù)生命周期內(nèi),Context類型是一個(gè)接口類型,在實(shí)際應(yīng)用中,我們可以使用Context包來傳遞請(qǐng)求的元數(shù)據(jù),本文將給大家介紹Golang服務(wù)中context超時(shí)處理的方法和超時(shí)原因,需要的朋友可以參考下2023-05-05
在Go中實(shí)現(xiàn)高效可靠的鏈路追蹤系統(tǒng)
在當(dāng)今互聯(lián)網(wǎng)應(yīng)用的架構(gòu)中,分布式系統(tǒng)已經(jīng)成為主流,分布式系統(tǒng)的優(yōu)勢在于能夠提供高可用性、高并發(fā)性和可擴(kuò)展性,本文將介紹鏈路追蹤的概念和原理,并重點(diǎn)介紹如何在Golang中實(shí)現(xiàn)高效可靠的鏈路追蹤系統(tǒng),需要的朋友可以參考下2023-10-10

