數(shù)組是由相同類型元素的集合組成的數(shù)據(jù)結(jié)構(gòu)龄减,計(jì)算機(jī)會(huì)為數(shù)組分配一塊連續(xù)的內(nèi)存來(lái)保存其中的元素赘阀,我們可以利用數(shù)組中元素的索引快速訪問(wèn)特定元素涛浙。goalng中的數(shù)組在定義時(shí)必須指定長(zhǎng)度谈竿,創(chuàng)建之后長(zhǎng)度是不可變的咸灿。因?yàn)樵跀?shù)組創(chuàng)建過(guò)程中匪燕,golang會(huì)根據(jù)定義的長(zhǎng)度去申請(qǐng)連續(xù)的內(nèi)存空間蕾羊。與其他編程語(yǔ)言一樣喧笔,數(shù)組的指針指向數(shù)組開(kāi)頭元素。
golang的切片類型是基于數(shù)組實(shí)現(xiàn)的龟再,可以理解為一個(gè)管理數(shù)組自動(dòng)擴(kuò)容的結(jié)構(gòu)书闸,具體的結(jié)構(gòu)體定義如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array是一個(gè)指向數(shù)組結(jié)構(gòu)的指針,len和cap字段用于維護(hù)數(shù)組的長(zhǎng)度和容量利凑。當(dāng)我們定義一個(gè)切片類型時(shí)浆劲,切片的初始容量為0,當(dāng)我們逐漸append變量時(shí),切片會(huì)依據(jù)某種策略進(jìn)行擴(kuò)容哀澈。具體的擴(kuò)容策略比較復(fù)雜牌借,具體可以查看go/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
}
}
}
......
}
簡(jiǎn)單來(lái)說(shuō):如果需要的cap > oldcap*2 割按,就直接分配需要的cap膨报。否則,如果oldcap< 1024, 直接加倍哲虾。如果old <= 1024丙躏,反復(fù)增加25%。直到足夠存儲(chǔ)全部?jī)?nèi)容束凑。實(shí)際上的邏輯更加復(fù)雜晒旅,最終的cap還需要根據(jù)數(shù)據(jù)類型所占的空間進(jìn)行調(diào)整,具體可以參考源碼汪诉。接下來(lái)废恋,通過(guò)如下實(shí)驗(yàn)驗(yàn)證兩種簡(jiǎn)單的情況。首先是不指定默認(rèn)長(zhǎng)度和容量時(shí)扒寄。
func TestNewSlice(T *testing.T){
var slice []int
fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
for i:=0;i<100;i++{
slice = append(slice, i)
fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
}
}
實(shí)驗(yàn)結(jié)果如下:
slice len = 0 slice cap = 0
slice len = 1 slice cap = 1
slice len = 2 slice cap = 2
slice len = 3 slice cap = 4
slice len = 4 slice cap = 4
slice len = 5 slice cap = 8
slice len = 6 slice cap = 8
slice len = 7 slice cap = 8
slice len = 8 slice cap = 8
......
接下來(lái)是通過(guò)make指定初始的長(zhǎng)度和容量時(shí)鱼鼓。
func TestMakeSlice(T *testing.T){
var slice = make([]int,0,10)
fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
for i:=0;i<100;i++{
slice = append(slice, i)
fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
}
}
實(shí)驗(yàn)結(jié)果如下:
slice len = 0 slice cap = 3
slice len = 1 slice cap = 3
slice len = 2 slice cap = 3
slice len = 3 slice cap = 3
slice len = 4 slice cap = 6
slice len = 5 slice cap = 6
slice len = 6 slice cap = 6
slice len = 7 slice cap = 12
......
因此,如果我們知道切片大致需要的容量時(shí)该编,最好通過(guò) make方法迄本,指定cap值。這樣可以有效避免數(shù)組的頻繁擴(kuò)容课竣。從而避免切片在擴(kuò)容時(shí)導(dǎo)致的性能損失嘉赎。這部分損失包括擴(kuò)容時(shí)重新申請(qǐng)內(nèi)存、數(shù)據(jù)的拷貝以及后續(xù)的垃圾回收于樟。
golang數(shù)組和切片的區(qū)別除了體現(xiàn)在是否支持?jǐn)U容外公条,還體現(xiàn)在傳值操作上。具體我們通過(guò)如下實(shí)驗(yàn)來(lái)說(shuō)明迂曲。
func TestSliceObj(t *testing.T) {
var a1 = [3]int{1,2,3}
fmt.Printf("the a1 = %v \t\t a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])
a2 := a1
a1[0] = 10
fmt.Printf("after change the a1 = %v \t\t a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])
fmt.Printf("after change the a2 = %v \t\t a2 ptr = %p \t\t a2[0] ptr = %p\n",a2,&a2,&a2[0])
var s1 = []int{1,2,3}
fmt.Printf("the s1 = %v \t\t s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
s2 := s1
s1[0] = 10
fmt.Printf("after change the s1 = %v \t\t s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
fmt.Printf("after change the s2 = %v \t\t s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}
實(shí)驗(yàn)結(jié)果如下:
=== RUN TestSliceObj
the a1 = [1 2 3] a1 ptr = 0xc0000ca0a0 a1[0] ptr = 0xc0000ca0a0
after change the a1 = [10 2 3] a1 ptr = 0xc0000ca0a0 a1[0] ptr = 0xc0000ca0a0
after change the a2 = [1 2 3] a2 ptr = 0xc0000ca0e0 a2[0] ptr = 0xc0000ca0e0
the s1 = [1 2 3] s1 ptr = 0xc0000b40a0 s1[0] ptr = 0xc0000ca140
after change the s1 = [10 2 3] s1 ptr = 0xc0000b40a0 s1[0] ptr = 0xc0000ca140
after change the s2 = [10 2 3] s2 ptr = 0xc0000b40e0 s2[0] ptr = 0xc0000ca140
--- PASS: TestSliceObj (0.00s)
通過(guò)實(shí)驗(yàn)結(jié)果我們發(fā)現(xiàn)靶橱,對(duì)于數(shù)組array來(lái)說(shuō),數(shù)組的指針和數(shù)組第一個(gè)元素的第一個(gè)指針是相同的,說(shuō)明數(shù)組指針確實(shí)指向數(shù)組第一個(gè)元素地址关霸。接著我們將a1值賦值給a2并修改a1的值传黄。我們發(fā)現(xiàn)a1和a2分別指向不同的內(nèi)存空間,同時(shí)對(duì)a1的修改不會(huì)影響a2谒拴。說(shuō)明賦值過(guò)程是值傳遞尝江,在賦值過(guò)程中會(huì)重新申請(qǐng)一塊空間。
對(duì)于切片slice來(lái)說(shuō)英上,切片指針指向slice struct的位置炭序,而切片第一個(gè)元素的地址位才是底層數(shù)組真正的地址位。然后我們將s1賦值給s2苍日,我們發(fā)現(xiàn)是s1和s2指針?lè)謩e指向兩個(gè)不同的空間惭聂,說(shuō)明該賦值過(guò)程同樣也是值傳遞,即當(dāng)前內(nèi)存中存在兩個(gè)slice結(jié)構(gòu)體對(duì)象,分別為s1和s2相恃。與數(shù)組不同的是辜纲,兩個(gè)切片所對(duì)應(yīng)的底層數(shù)組是同一個(gè)。當(dāng)我們修改s1的值之后拦耐,s2也同樣發(fā)生了變化耕腾。
那么這是不是就說(shuō)明,golang中切片類型的賦值是指針復(fù)制或者說(shuō)是淺拷貝呢杀糯?
答案是否定的扫俺,接下來(lái)介紹一個(gè)golang切片使用過(guò)程中常見(jiàn)的坑。首先做一組實(shí)驗(yàn)固翰。
func TestAppendSlice(T *testing.T){
var s1 = []int{1,2,3,4}
fmt.Printf("the s1 = %v \t\t s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
s2 := s1
s1 = append(s1, 5)
fmt.Printf("after change the s1 = %v \t\t s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
fmt.Printf("after change the s2 = %v \t\t s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}
實(shí)驗(yàn)結(jié)果如下:
=== RUN TestAppendSlice
the s1 = [1 2 3 4] s1 ptr = 0xc00000c0e0 s1[0] ptr = 0xc000018200
after change the s1 = [1 2 3 4 5] s1 ptr = 0xc00000c0e0 s1[0] ptr = 0xc00001a180
after change the s2 = [1 2 3 4] s2 ptr = 0xc00000c120 s2[0] ptr = 0xc000018200
--- PASS: TestAppendSlice (0.00s)
通過(guò)實(shí)驗(yàn)結(jié)果我們發(fā)現(xiàn)狼纬,當(dāng)我們將s1賦值給s2之后,再修改s1骂际,s2并未像之前一樣也發(fā)生變化疗琉。另外我們發(fā)現(xiàn)s2對(duì)應(yīng)的底層數(shù)組與s1是相同的,這和之前的實(shí)驗(yàn)結(jié)論一致歉铝。不同之處在于盈简,s1在修改后指針指向的底層數(shù)組地址發(fā)生了變化,這是因?yàn)閍ppend操作恰好出發(fā)了一次數(shù)組擴(kuò)容太示,從而導(dǎo)致切片重新申請(qǐng)了一塊連續(xù)地址柠贤。
因此在使用切片時(shí)一定注意這一點(diǎn),尤其是當(dāng)發(fā)生參數(shù)傳遞時(shí)先匪,需要確定自己期待的是值傳遞還是指針傳遞。否則可能會(huì)遇到難以解釋的bug弃衍。