
看完這篇文章,下面這些高頻面試題你都會答了吧
- Go slice的底層實現(xiàn)原理
- Go array和slice的區(qū)別
- Go slice深拷貝和淺拷貝
- Go slice擴容機制是怎樣的?
- 為什么Go slice是非線程安全的?
實現(xiàn)原理
slice是無固定長度的數(shù)組,底層結(jié)構(gòu)是一個結(jié)構(gòu)體,包含如下3個屬性
一個 slice 在 golang 中占用 24 個 bytes
type slice struct {
array unsafe.Pointer
len int
cap int
}
array : 包含了一個指向一個數(shù)組的指針,數(shù)據(jù)實際上存儲在這個指針指向的數(shù)組上,占用 8 bytes
len: 當前 slice 使用到的長度,占用8 bytes
cap : 當前 slice 的容量,同時也是底層數(shù)組 array 的長度, 8 bytes
slice并不是真正意義上的動態(tài)數(shù)組,而是一個引用類型。slice總是指向一個底層array,slice的聲明也可以像 array一樣,只是長度可變。golang中通過語法糖,使得我們可以像聲明array一樣,自動創(chuàng)建slice結(jié)構(gòu)體
根據(jù)索引位置取切片slice 元素值時,默認取值范圍是(0~len(slice)-1),一般輸出slice時,通常是指 slice[0:len(slice)-1],根據(jù)下標就可以輸出所指向底層數(shù)組中的值
主要特性
引用類型
golang 有三個常用的高級類型slice、map、channel, 它們都是引用類型,當引用類型作為函數(shù)參數(shù)時,可能會修改原內(nèi)容數(shù)據(jù)。
func sliceModify(s []int) {
s[0] = 100
}
func sliceAppend(s []int) []int {
s = append(s, 100)
return s
}
func sliceAppendPtr(s *[]int) {
*s = append(*s, 100)
return
}
// 注意:Go語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。
// 拷貝的內(nèi)容是非引用類型(int、string、struct等這些),在函數(shù)中就無法修改原內(nèi)容數(shù)據(jù);
// 拷貝的內(nèi)容是引用類型(interface、指針、map、slice、chan等這些),這樣就可以修改原內(nèi)容數(shù)據(jù)。
func TestSliceFn(t *testing.T) {
// 參數(shù)為引用類型slice:外層slice的len/cap不會改變,指向的底層數(shù)組會改變
s := []int{1, 1, 1}
newS := sliceAppend(s)
// 函數(shù)內(nèi)發(fā)生了擴容
t.Log(s, len(s), cap(s))
// [1 1 1] 3 3
t.Log(newS, len(newS), cap(newS))
// [1 1 1 100] 4 6
s2 := make([]int, 0, 5)
newS = sliceAppend(s2)
// 函數(shù)內(nèi)未發(fā)生擴容
t.Log(s2, s2[0:5], len(s2), cap(s2))
// [] [100 0 0 0 0] 0 5
t.Log(newS, newS[0:5], len(newS), cap(newS))
// [100] [100 0 0 0 0] 1 5
// 參數(shù)為引用類型slice的指針:外層slice的len/cap會改變,指向的底層數(shù)組會改變
sliceAppendPtr(&s)
t.Log(s, len(s), cap(s))
// [1 1 1 100] 4 6
sliceModify(s)
t.Log(s, len(s), cap(s))
// [100 1 1 100] 4 6
}
公眾號后臺caspar回復【代碼】獲取本文所有示例代碼
切片狀態(tài)
切片有3種特殊的狀態(tài),分為「零切片」、「空切片」和「nil 切片」
func TestSliceEmptyOrNil(t *testing.T) {
var slice1 []int
// slice1 is nil slice
slice2 := make([]int, 0)
// slcie2 is empty slice
var slice3 = make([]int, 2)
// slice3 is zero slice
if slice1 == nil {
t.Log("slice1 is nil.")
// 會輸出這行
}
if slice2 == nil {
t.Log("slice2 is nil.")
// 不會輸出這行
}
t.Log(slice3) // [0 0]
}
非線程安全
slice不支持并發(fā)讀寫,所以并不是線程安全的,使用多個 goroutine 對類型為 slice 的變量進行操作,每次輸出的值大概率都不會一樣,與預期值不一致; slice在并發(fā)執(zhí)行中不會報錯,但是數(shù)據(jù)會丟失
/**
* 切片非并發(fā)安全
* 多次執(zhí)行,每次得到的結(jié)果都不一樣
* 可以考慮使用 channel 本身的特性 (阻塞) 來實現(xiàn)安全的并發(fā)讀寫
*/
func TestSliceConcurrencySafe(t *testing.T) {
a := make([]int, 0)
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
a = append(a, i)
wg.Done()
}(i)
}
wg.Wait()
t.Log(len(a))
// not equal 10000
}
如果想實現(xiàn)slice線程安全,有兩種方式:
方式一:通過加鎖實現(xiàn)slice線程安全,適合對性能要求不高的場景。
func TestSliceConcurrencySafeByMutex(t *testing.T) {
var lock sync.Mutex //互斥鎖
a := make([]int, 0)
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
lock.Lock()
defer lock.Unlock()
a = append(a, i)
}(i)
}
wg.Wait()
t.Log(len(a))
// equal 10000
}
方式二:通過channel實現(xiàn)slice線程安全,適合對性能要求高的場景。
func TestSliceConcurrencySafeByChanel(t *testing.T) {
buffer := make(chan int)
a := make([]int, 0)
// 消費者
go func() {
for v := range buffer {
a = append(a, v)
}
}()
// 生產(chǎn)者
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
buffer <- i
}(i)
}
wg.Wait()
t.Log(len(a))
// equal 10000
}
共享存儲空間
多個切片如果共享同一個底層數(shù)組,這種情況下,對其中一個切片或者底層數(shù)組的更改,會影響到其他切片
/**
* 切片共享存儲空間
*/
func TestSliceShareMemory(t *testing.T) {
slice1 := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"}
Q2 := slice1[3:6]
t.Log(Q2, len(Q2), cap(Q2))
// [4 5 6] 3 9
Q3 := slice1[5:8]
t.Log(Q3, len(Q3), cap(Q3))
// [6 7 8] 3 7
Q3[0] = "Unkown"
t.Log(Q2, Q3)
// [4 5 Unkown] [Unkown 7 8]
a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
t.Log(shadow, a)
// [2 3] [1 2 3 4 5]
shadow = append(shadow, 100)
// 會修改指向數(shù)組的所有切片
t.Log(shadow, a)
// [2 3 100] [1 2 3 100 5]
}
常用操作
創(chuàng)建
slice 的創(chuàng)建有4種方式,如下:
func TestSliceInit(t *testing.T) {
// 初始化方式1:直接聲明
var slice1 []int
t.Log(len(slice1), cap(slice1))
// 0, 0
slice1 = append(slice1, 1)
t.Log(len(slice1), cap(slice1))
// 1, 1, 24
// 初始化方式2:使用字面量
slice2 := []int{1, 2, 3, 4}
t.Log(len(slice2), cap(slice2))
// 4, 4, 24
// 初始化方式3:使用make創(chuàng)建slice
slice3 := make([]int, 3, 5)
// make([]T, len, cap) cap不傳則和len一樣
t.Log(len(slice3), cap(slice3))
// 3, 5
t.Log(slice3[0], slice3[1], slice3[2])
// 0, 0, 0
// t.Log(slice3[3], slice3[4])
// panic: runtime error: index out of range [3] with length 3
slice3 = append(slice3, 1)
t.Log(len(slice3), cap(slice3))
// 4, 5, 24
// 初始化方式4: 從切片或數(shù)組“截取”
arr := [100]int{}
for i := range arr {
arr[i] = i
}
slcie4 := arr[1:3]
slice5 := make([]int, len(slcie4))
copy(slice5, slcie4)
t.Log(len(slcie4), cap(slcie4), unsafe.Sizeof(slcie4))
// 2,99,24
t.Log(len(slice5), cap(slice5), unsafe.Sizeof(slice5))
// 2,2,24
}
增加
func TestSliceGrowing(t *testing.T) {
slice1 := []int{}
for i := 0; i < 10; i++ {
slice1 = append(slice1, i)
t.Log(len(slice1), cap(slice1))
}
// 1 1
// 2 2
// 3 4
// 4 4
// 5 8
// 6 8
// 7 8
// 8 8
// 9 16
// 10 16
}
刪除
func TestSliceDelete(t *testing.T) {
slice1 := []int{1, 2, 3, 4, 5}
var x int
// 刪除最后一個元素
x, slice1 = slice1[len(slice1)-1], slice1[:len(slice1)-1]
t.Log(x, slice1, len(slice1), cap(slice1))
// 5 [1 2 3 4] 4 5
// 刪除第2個元素
slice1 = append(slice1[:2], slice1[3:]...)
t.Log(slice1, len(slice1), cap(slice1))
// [1 2 4] 3 5
}
查找
v := s[i] // 下標訪問
修改
s[i] = 5 // 下標修改
截取
/**
* 切片截取
*/
func TestSliceSubstr(t *testing.T) {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[:]
// 截取 slice[left:right:max]
// left:省略默認0
// right:省略默認len(slice1)
// max: 省略默認len(slice1)
// len = right-left+1
// cap = max-left
t.Log(slice2, len(slice2), cap(slice2))
// 1 2 3 4 5] 5 5
slice3 := slice1[1:]
t.Log(slice3, len(slice3), cap(slice3))
// [2 3 4 5] 4 4
slice4 := slice1[:2]
t.Log(slice4, len(slice4), cap(slice4))
// [1 2] 2 5
slice5 := slice1[1:2]
t.Log(slice5, len(slice5), cap(slice5))
// [2] 1 4
slice6 := slice1[:2:5]
t.Log(slice6, len(slice6), cap(slice6))
// [1 2] 2 5
slice7 := slice1[1:2:2]
t.Log(slice7, len(slice7), cap(slice7))
// [2] 1 1
}
遍歷
切片有3種遍歷方式
/**
* 切片遍歷
*/
func TestSliceTravel(t *testing.T) {
slice1 := []int{1, 2, 3, 4}
for i := 0; i < len(slice1); i++ {
t.Log(slice1[i])
}
for idx, e := range slice1 {
t.Log(idx, e)
}
for _, e := range slice1 {
t.Log(e)
}
}
反轉(zhuǎn)
func TestSliceReverse(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
a[left], a[right] = a[right], a[left]
}
t.Log(a, len(a), cap(a))
// [5 4 3 2 1] 5 5
}
拷貝
開發(fā)中會經(jīng)常的把一個變量復制給另一個變量,那么這個過程,可能是深淺拷貝,那么今天幫大家區(qū)分一下這兩個拷貝的區(qū)別和具體的區(qū)別
深拷貝
拷貝的是數(shù)據(jù)本身,創(chuàng)造一個樣的新對象,新創(chuàng)建的對象與原對象不共享內(nèi)存,新創(chuàng)建的對象在內(nèi)存中開辟一個新的內(nèi)存地址,新對象值修改時不會影響原對象值。既然內(nèi)存地址不同,釋放內(nèi)存地址時,可分別釋放
值類型的數(shù)據(jù),默認賦值操作都是深拷貝,Array、Int、String、Struct、Float,Bool。引用類型的數(shù)據(jù)如果想實現(xiàn)深拷貝,需要通過輔助函數(shù)完成
比如golang深拷貝copy 方法會把源切片值(即 from Slice )中的元素復制到目標切片(即 to Slice )中,并返回被復制的元素個數(shù),copy 的兩個類型必須一致。copy 方法最終的復制結(jié)果取決于較短的那個切片,當較短的切片復制完成,整個復制過程就全部完成了
/**
* 深拷貝
*/
func TestSliceDeepCopy(t *testing.T) {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, 5, 5)
// 深拷貝
copy(slice2, slice1)
t.Log(slice1, len(slice1), cap(slice1))
// [1 2 3 4 5] 5 5
t.Log(slice2, len(slice2), cap(slice2))
// [1 2 3 4 5] 5 5
slice1[1] = 100
t.Log(slice1, len(slice1), cap(slice1))
// [1 100 3 4 5] 5 5
t.Log(slice2, len(slice2), cap(slice2))
// [1 2 3 4 5] 5 5
}
淺拷貝
拷貝的是數(shù)據(jù)地址,只復制指向的對象的指針,此時新對象和老對象指向的內(nèi)存地址是一樣的,新對象值修改時老對象也會變化。釋放內(nèi)存地址時,同時釋放內(nèi)存地址。
引用類型的數(shù)據(jù),默認全部都是淺拷貝,Slice、Map等
目的切片和源切片指向同一個底層數(shù)組,任何一個數(shù)組元素改變,都會同時影響兩個數(shù)組。
/**
* 淺拷貝
*/
func TestSliceShadowCopy(t *testing.T) {
slice1 := []int{1, 2, 3, 4, 5}
// 淺拷貝(注意 := 對于引用類型是淺拷貝,對于值類型是深拷貝)
slice2 := slice1
t.Logf("%p", slice1) // 0xc00001c120
t.Logf("%p", slice2) // 0xc00001c120
// 同時改變兩個數(shù)組,這時就是淺拷貝,未擴容時,修改 slice1 的元素之后,slice2 的元素也會跟著修改
slice1[0] = 10
t.Log(slice1, len(slice1), cap(slice1))
// [10 2 3 4 5] 5 5
t.Log(slice2, len(slice2), cap(slice2))
// [10 2 3 4 5] 5 5
// 注意下:擴容后,slice1和slice2不再指向同一個數(shù)組,修改 slice1 的元素之后,slice2 的元素不會被修改了
slice1 = append(slice1, 5, 6, 7, 8)
slice1[0] = 11
// 這里可以發(fā)現(xiàn),slice1[0] 被修改為了 11, slice1[0] 還是10
t.Log(slice1, len(slice1), cap(slice1))
// [11 2 3 4 5 5 6 7 8] 9 10
t.Log(slice2, len(slice2), cap(slice2))
// [10 2 3 4 5] 5 5
}
在復制 slice 的時候,slice 中數(shù)組的指針也被復制了,在觸發(fā)擴容邏輯之前,兩個 slice 指向的是相同的數(shù)組,觸發(fā)擴容邏輯之后指向的就是不同的數(shù)組了
擴容
擴容會發(fā)生在slice append的時候,當slice的cap不足以容納新元素,就會進行擴容
源碼:https://github.com/golang/go/blob/master/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.len < 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
}
}
}
// 省略一些后續(xù)...
}
- 如果新申請容量比兩倍原有容量大,那么擴容后容量大小 等于 新申請容量
- 如果原有 slice 長度小于 1024, 那么每次就擴容為原來的 2 倍
- 如果原 slice 大于等于 1024, 那么每次擴容就擴為原來的 1.25 倍
內(nèi)存泄露
由于slice的底層是數(shù)組,很可能數(shù)組很大,但slice所取的元素數(shù)量卻很小,這就導致數(shù)組占用的絕大多數(shù)空間是被浪費的
Case1:
比如下面的代碼,如果傳入的slice b是很大的,然后引用很小部分給全局量a,那么b未被引用的部分(下標1之后的數(shù)據(jù))就不會被釋放,造成了所謂的內(nèi)存泄漏。
var a []int
func test(b []int) {
a = b[:1] // 和b共用一個底層數(shù)組
return
}
那么只要全局量a在,b就不會被回收。
如何避免?
在這樣的場景下注意:如果我們只用到一個slice的一小部分,那么底層的整個數(shù)組也將繼續(xù)保存在內(nèi)存當中。當這個底層數(shù)組很大,或者這樣的場景很多時,可能會造成內(nèi)存急劇增加,造成崩潰。
所以在這樣的場景下,我們可以將需要的分片復制到一個新的slice中去,減少內(nèi)存的占用
var a []int
func test(b []int) {
a = make([]int, 1)
copy(a, b[:0])
return
}
Case2:
比如下面的代碼,返回的slice是很小一部分,這樣該函數(shù)退出后,原來那個體積較大的底層數(shù)組也無法被回收
func test2() []int{
s = make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, p)
}
s2 := s[100:102]
return s2
}
如何避免?
將需要的分片復制到一個新的slice中去,減少內(nèi)存的占用
func test2() []int{
s = make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
// 一些計算...
s = append(s, p)
}
s2 := make([]int, 2)
copy(s2, s[100:102])
return s2
}
切片與數(shù)組對比
數(shù)組是一個固定長度的,初始化時候必須要指定長度,不指定長度的話就是切片了
數(shù)組是值類型,將一個數(shù)組賦值給另一個數(shù)組時,傳遞的是一份深拷貝,賦值和函數(shù)傳參操作都會復制整個數(shù)組數(shù)據(jù),會占用額外的內(nèi)存;切片是引用類型,將一個切片賦值給另一個切片時,傳遞的是一份淺拷貝,賦值和函數(shù)傳參操作只會復制len和cap,但底層共用同一個數(shù)組,不會占用額外的內(nèi)存。
//a是一個數(shù)組,注意數(shù)組是一個固定長度的,初始化時候必須要指定長度,不指定長度的話就是切片了
a := [3]int{1, 2, 3}
//b是數(shù)組,是a的一份深拷貝
b := a
//c是切片,是引用類型,底層數(shù)組是a
c := a[:]
for i := 0; i < len(a); i++ {
a[i] = a[i] + 1
}
//改變a的值后,b是a的拷貝,b不變,c是引用,c的值改變
fmt.Println(a)
//[2,3,4]
fmt.Println(b)
//[1 2 3]
fmt.Println(c)
//[2,3,4]
//a是一個切片,不指定長度的話就是切片了
a := []int{1, 2, 3}
//b是切片,是a的一份拷貝
b := a
//c是切片,是引用類型
c := a[:]
for i := 0; i < len(a); i++ {
a[i] = a[i] + 1
}
//改變a的值后,b是a的淺拷貝,b的值改派,c是引用,c的值改變
fmt.Println(a)
//[2,3,4]
fmt.Println(b)
//[2,3,4]
fmt.Println(c)
//[2,3,4]
總結(jié)
- 創(chuàng)建切片時可根據(jù)實際需要預分配容量,盡量避免追加過程中進行擴容操作,有利于提升性能
- 使用 append() 向切片追加元素時有可能觸發(fā)擴容,擴容后將會生成新的切片
- 使用 len()、cap()計算切片長度、容量時,時間復雜度均為 O(1),不需要遍歷切片
- 切片是非線程安全的,如果要實現(xiàn)線程安全,可以加鎖或者使用Channel
- 大數(shù)組作為函數(shù)參數(shù)時,會復制整個數(shù)組數(shù)據(jù),消耗過多內(nèi)存,建議使用切片或者指針
- 切片作為函數(shù)參數(shù)時,可以改變切片指向的數(shù)組,不能改變切片本身len和cap;想要改變切片本身,可以將改變后的切片返回 或者 將切片指針作為函數(shù)參數(shù)。
- 如果只用到大slice的一小部分,建議將需要的分片復制到一個新的slice中去,減少內(nèi)存的占用
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布!