欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文詳解Go語言中切片的底層原理

 更新時間:2023年06月28日 15:19:49   作者:7small7  
在Go語言中,切片作為一種引用類型數(shù)據(jù),相對數(shù)組而言是一種動態(tài)長度的數(shù)據(jù)類型,使用的場景也是非常多,所以本文主要來和大家聊聊切片的底層原理,需要的可以參考一下

大家好,我是二條,在上一篇我們學(xué)習(xí)了輕松理解Go中的內(nèi)存逃逸問題,今天接著我們學(xué)習(xí)Go中切片的相關(guān)知識。本文不會單獨去講解切片的基礎(chǔ)語法,只會對切片的底層和在開發(fā)中需要注意的事項作分析。

在Go語言中,切片作為一種引用類型數(shù)據(jù),相對數(shù)組而言是一種動態(tài)長度的數(shù)據(jù)類型,使用的場景也是非常多。但在使用切片的過程中,也有許多需要注意的事項。例如切片函數(shù)傳值、切片動態(tài)擴(kuò)容、切片對底層數(shù)組的引用問題等等。今天分享的主題,就是圍繞切片進(jìn)行。

切片的函數(shù)傳值

切片作為一種引用數(shù)據(jù)類型,在作為函數(shù)傳值時,如果函數(shù)內(nèi)部對切片做了修改,會影響到原切片上。

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ā)生了改變。這是因為切片是一種引用類型數(shù)據(jù),在傳遞到函數(shù)change()時,使用的都是相同的底層數(shù)組(切片底層本質(zhì)仍是一個數(shù)組)。因此,底層數(shù)組的值改變了,就會影響到其他指向該數(shù)組的切片上。

針對上述的問題,有什么解決方案,使得傳遞切片,不會影響原切片的值呢?可以采用切片復(fù)制的方式,重新創(chuàng)建一個新的切片當(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)建一個新的切片,當(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]

通過上述運行結(jié)果,在change函數(shù)中,對切片下標(biāo)為0做了值修改,對切片sl1的值沒有影響。

切片動態(tài)擴(kuò)容機(jī)制

在Go中,切片是一種動態(tài)長度引用數(shù)據(jù)類型。當(dāng)切片的容量不足以容納新增加的元素時,底層會實現(xiàn)自動擴(kuò)容用來存儲新添加的元素。先查看下面的一段實例代碼,證明切片存在動態(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操作,依次遞增。但切片的容量就不是依次遞增,從明面上看,有點像以2的倍數(shù)在增加。具體增加的規(guī)律是怎么樣的呢?

要弄明白Go中的切片是如何實現(xiàn)擴(kuò)容的,這就需要關(guān)注一下Go的版本。

在Go的1.18版本以前,是按照如下的規(guī)則來進(jìn)行擴(kuò)容:

1、如果原有切片的長度小于 1024,那么新的切片容量會直接擴(kuò)展為原來的 2 倍。

2、如果原有切片的長度大于等于 1024,那么新的切片容量會擴(kuò)展為原來的 1.25 倍,這一過程可能需要執(zhí)行多次才能達(dá)到期望的容量。

3、如果切片屬于第一種情況(長度小于 1024)并且需要擴(kuò)容的容量小于 1024 字節(jié),那么新的切片容量會直接增加到原來的長度加上需要擴(kuò)容的容量(新容量=原容量+擴(kuò)容容量)。

從Go的1.18版本開始,是按照如下的規(guī)則進(jìn)行擴(kuò)容:

1、當(dāng)原slice容量(oldcap)小于256的時候,新slice(newcap)容量為原來的2倍。

2、原slice容量超過256,新slice容量newcap = oldcap + (oldcap+3*256) / 4。

使用上面的代碼,將循環(huán)的值調(diào)到非常大,例如10w,甚至更大,你會發(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ī)律,這個規(guī)律是怎么來的,我們可以直接源代碼。

首先看1.18版本開始的底層代碼,你需要找到Go的源碼文件,路徑為runtime/slice.go,該文件中有一個名為growslice()函數(shù)。這個函數(shù)的代碼很長,我們重點關(guān)注下述代碼,其他的代碼除了做一些邏輯處理,還處理了內(nèi)存對齊問題,關(guān)于內(nèi)存對齊就不在本篇提及。

//?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倍之間尋找一種相對平衡的規(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ò)容時,則直接擴(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ī)律。如果你在實際的案例中,并非按照總結(jié)的規(guī)律進(jìn)行擴(kuò)容,這是因為切片擴(kuò)容之后還考慮了內(nèi)存對齊問題,也就是上述growslic()函數(shù)剩余部分。

切片操作對數(shù)組的影響

在Go中,切片和數(shù)組有一些共性,也有一些不同之處。

相同之處:

1、切片和數(shù)組在定義時,需要指定內(nèi)部的元素類型,一旦定義元素類型,就只能存儲該類型的元素。

2、切片雖然是單獨的一種類型,底層仍然是一個數(shù)組,在Go源碼中,有這樣一段定義,通過閱讀這段代碼,可以總結(jié)出切片底層是一個struct數(shù)據(jù)結(jié)構(gòu)。

type?slice?struct?{
?array?unsafe.Pointer?#?指向底層數(shù)組的指針
?len???int?#?切片的長度,也就是說當(dāng)前切片中的元素個數(shù)
?cap???int?#?切片的容量,也就是說切片最大能夠存儲多少個元素
}

不同之處:

1、切片和數(shù)組最大的不同之處,在于切片的長度和容量是動態(tài)的,可以根據(jù)實際情況實現(xiàn)動態(tài)擴(kuò)容,而數(shù)組是固定長度,一經(jīng)定義長度,存儲的元素就不能超過定義時的長度。

下面有這樣一種場景,需要特別注意。

從一個切片中生產(chǎn)新的切片,使用截取實現(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)
}

從一個切片中生成新的切片,使用copy()函數(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)
}

這兩段代碼,都是從一個切片中生成一個新的切片,但誰的性能效果更好呢?

1、第一種方式,生成新切片,底層仍然與原切片共用一個底層數(shù)組。在生成切片時,效率會更高一些。但存在一個問題,如果原切片和新切片對自身的元素做了修改,底層數(shù)組也會隨著改變,這樣會導(dǎo)致另外一個切片也跟著受影響。這種方式雖然效率更高,但是共用同一個底層數(shù)組,會存在數(shù)據(jù)安全問題。

2、第二種方式,生成新切片,使用的是copy()函數(shù)實現(xiàn),會發(fā)生一個內(nèi)存拷貝。這樣新切片就是存儲在新的內(nèi)存中,其底層的數(shù)組和原切片底層的數(shù)組,不在是共享。不管是老切片還是新切片內(nèi)部元素發(fā)生變化,都只會影響到自身。這種方式雖然消耗的內(nèi)存更大,但數(shù)據(jù)更加安全。

使用歸納

在實際的開發(fā)過程中,我們一般使用切片的場景要比數(shù)組多,這是為什么呢?

1、動態(tài)擴(kuò)展:切片可以動態(tài)擴(kuò)展或縮減,而數(shù)組的長度是固定的。使用切片可以更方便地處理不確定長度的數(shù)據(jù)集。

2、內(nèi)存效率:切片的底層實現(xiàn)是數(shù)組,但是通過切片可以對底層的數(shù)組進(jìn)行引用,避免了復(fù)制底層數(shù)據(jù)的開銷。因此,使用切片可以更高效地處理大量數(shù)據(jù)。

3、零值初始化:切片有一個默認(rèn)值為0的長度和容量,這使得初始化切片更加方便。

4、內(nèi)置函數(shù):切片有許多內(nèi)置函數(shù),如append()、copy()等,這些函數(shù)可以更方便地操作切片。

本文總結(jié)

根據(jù)上面的幾個小問題進(jìn)行演示,我們在日常開發(fā)中,使用切片重點可以關(guān)注在動態(tài)擴(kuò)容引用傳值上面,這也是經(jīng)常出現(xiàn)問題的點。下面細(xì)分幾點進(jìn)行歸納:

1、由于切片是引用類型,因此容易出現(xiàn)多個變量引用同一個底層數(shù)組,導(dǎo)致內(nèi)存泄露和意外修改數(shù)據(jù)的情況。

2、當(dāng)切片長度超過底層數(shù)組容量時,可以導(dǎo)致切片重新分配內(nèi)存,這可能會帶來性能問題。

3、在使用切片時沒有正確計算長度和容量,也可能導(dǎo)致意料之外的結(jié)果。

4、切片常常被用作函數(shù)參數(shù),由于其引用類型的特性,可能會導(dǎo)致函數(shù)內(nèi)對切片數(shù)據(jù)的修改影響到外部變量。

5、如果切片的底層數(shù)組被修改,可能會對所有引用該底層數(shù)組的切片數(shù)據(jù)造成影響。

到此這篇關(guān)于一文詳解Go語言中切片的底層原理的文章就介紹到這了,更多相關(guān)Go切片內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言如何使用分布式鎖解決并發(fā)問題

    Go語言如何使用分布式鎖解決并發(fā)問題

    這篇文章主要為大家詳細(xì)介紹了Go 語言生態(tài)中基于 Redis 實現(xiàn)的分布式鎖庫 redsync,并探討其使用方法和實現(xiàn)原理,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-03-03
  • go?defer避坑指南之拆解延遲語句

    go?defer避坑指南之拆解延遲語句

    這篇文章主要為大家詳細(xì)介紹了go?defer避坑指南之如何拆解延遲語句,掌握正確使用方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-11-11
  • go mod tidy拉取依賴包bug問題及解決

    go mod tidy拉取依賴包bug問題及解決

    這篇文章主要介紹了go mod tidy拉取依賴包bug問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-09-09
  • Go語言實戰(zhàn)學(xué)習(xí)之流程控制詳解

    Go語言實戰(zhàn)學(xué)習(xí)之流程控制詳解

    這篇文章主要為大家詳細(xì)介紹了Go語言中的流程控制,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Go語言有一定的幫助?,需要的朋友可以參考下
    2022-08-08
  • Go基于雪花算法生成隨機(jī)id

    Go基于雪花算法生成隨機(jī)id

    雪花算法是twitter開源的由64位整數(shù)組成的分布式ID,本文主要介紹了Go基于雪花算法生成隨機(jī)id,具有一定的參考價值,感興趣的可以了解一下
    2024-05-05
  • Go語言中的字符串拼接方法詳情

    Go語言中的字符串拼接方法詳情

    本文介紹Go語言中的string類型、strings包和bytes.Buffer類型,介紹幾種字符串拼接方法的相關(guān)資料,需要的朋友可以參考一下,希望對你有所幫助
    2021-10-10
  • go get 和 go install 對比介紹

    go get 和 go install 對比介紹

    go install和go get都是Go語言的工具命令,但它們之間有一些區(qū)別。go get:用于從遠(yuǎn)程代碼存儲庫(如 GitHub)中下載或更新Go代碼包。go install:用于編譯并安裝 Go 代碼包,本文go get和go install對比介紹的非常詳細(xì),需要的朋友可以參考一下
    2023-04-04
  • Golang服務(wù)中context超時處理的方法詳解

    Golang服務(wù)中context超時處理的方法詳解

    在Go語言中,Context是一個非常重要的概念,它存在于一個完整的業(yè)務(wù)生命周期內(nèi),Context類型是一個接口類型,在實際應(yīng)用中,我們可以使用Context包來傳遞請求的元數(shù)據(jù),本文將給大家介紹Golang服務(wù)中context超時處理的方法和超時原因,需要的朋友可以參考下
    2023-05-05
  • goLang引入自定義包的方法

    goLang引入自定義包的方法

    今天小編就為大家分享一篇goLang引入自定義包的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-06-06
  • 在Go中實現(xiàn)高效可靠的鏈路追蹤系統(tǒng)

    在Go中實現(xiàn)高效可靠的鏈路追蹤系統(tǒng)

    在當(dāng)今互聯(lián)網(wǎng)應(yīng)用的架構(gòu)中,分布式系統(tǒng)已經(jīng)成為主流,分布式系統(tǒng)的優(yōu)勢在于能夠提供高可用性、高并發(fā)性和可擴(kuò)展性,本文將介紹鏈路追蹤的概念和原理,并重點介紹如何在Golang中實現(xiàn)高效可靠的鏈路追蹤系統(tǒng),需要的朋友可以參考下
    2023-10-10

最新評論