切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu)黎比,使用這種結(jié)構(gòu)可以用來(lái)管理數(shù)據(jù)集合墨闲。切片的設(shè)計(jì)想法是由動(dòng)態(tài)數(shù)組概念而來(lái)氏涩,為了開(kāi)發(fā)者可以更加方便的使一個(gè)數(shù)據(jù)結(jié)構(gòu)可以自動(dòng)增加和減少酬诀。但是切片本身并不是動(dòng)態(tài)數(shù)據(jù)或者數(shù)組指針曹质。切片常見(jiàn)的操作有 reslice婴噩、append、copy羽德。與此同時(shí)几莽,切片還具有可索引,可迭代的優(yōu)秀特性宅静。
一. 切片和數(shù)組
關(guān)于切片和數(shù)組怎么選擇章蚣?接下來(lái)好好討論討論這個(gè)問(wèn)題。
在 Go 中姨夹,與 C 數(shù)組變量隱式作為指針使用不同纤垂,Go 數(shù)組是值類型矾策,賦值和函數(shù)傳參操作都會(huì)復(fù)制整個(gè)數(shù)組數(shù)據(jù)。
func main() {
arrayA := [2]int{100, 200}
var arrayB [2]int
arrayB = arrayA
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
testArray(arrayA)
}
func testArray(x [2]int) {
fmt.Printf("func Array : %p , %v\n", &x, x)
}
打印結(jié)果:
arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]
可以看到峭沦,三個(gè)內(nèi)存地址都不同贾虽,這也就驗(yàn)證了 Go 中數(shù)組賦值和函數(shù)傳參都是值復(fù)制的。那這會(huì)導(dǎo)致什么問(wèn)題呢吼鱼?
假想每次傳參都用數(shù)組蓬豁,那么每次數(shù)組都要被復(fù)制一遍。如果數(shù)組大小有 100萬(wàn)菇肃,在64位機(jī)器上就需要花費(fèi)大約 800W 字節(jié)地粪,即 8MB 內(nèi)存。這樣會(huì)消耗掉大量的內(nèi)存巷送。于是乎有人想到驶忌,函數(shù)傳參用數(shù)組的指針矛辕。
func main() {
arrayA := [2]int{100, 200}
testArrayPoint(&arrayA) // 1.傳數(shù)組指針
arrayB := arrayA[:]
testArrayPoint(&arrayB) // 2.傳切片
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
}
func testArrayPoint(x *[]int) {
fmt.Printf("func Array : %p , %v\n", x, *x)
(*x)[1] += 100
}
打印結(jié)果:
func Array : 0xc4200b0140 , [100 200]
func Array : 0xc4200b0180 , [100 300]
arrayA : 0xc4200b0140 , [100 400]
這也就證明了數(shù)組指針確實(shí)到達(dá)了我們想要的效果⌒︴耍現(xiàn)在就算是傳入10億的數(shù)組,也只需要再棧上分配一個(gè)8個(gè)字節(jié)的內(nèi)存給指針就可以了聊品。這樣更加高效的利用內(nèi)存飞蹂,性能也比之前的好。
不過(guò)傳指針會(huì)有一個(gè)弊端翻屈,從打印結(jié)果可以看到陈哑,第一行和第三行指針地址都是同一個(gè),萬(wàn)一原數(shù)組的指針指向更改了伸眶,那么函數(shù)里面的指針指向都會(huì)跟著更改惊窖。
切片的優(yōu)勢(shì)也就表現(xiàn)出來(lái)了。用切片傳數(shù)組參數(shù)厘贼,既可以達(dá)到節(jié)約內(nèi)存的目的界酒,也可以達(dá)到合理處理好共享內(nèi)存的問(wèn)題。打印結(jié)果第二行就是切片嘴秸,切片的指針和原來(lái)數(shù)組的指針是不同的毁欣。
由此我們可以得出結(jié)論:
把第一個(gè)大數(shù)組傳遞給函數(shù)會(huì)消耗很多內(nèi)存,采用切片的方式傳參可以避免上述問(wèn)題岳掐。切片是引用傳遞凭疮,所以它們不需要使用額外的內(nèi)存并且比使用數(shù)組更有效率。
但是串述,依舊有反例执解。
package main
import "testing"
func array() [1024]int {
var x [1024]int
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func slice() []int {
x := make([]int, 1024)
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func BenchmarkArray(b *testing.B) {
for i := 0; i < b.N; i++ {
array()
}
}
func BenchmarkSlice(b *testing.B) {
for i := 0; i < b.N; i++ {
slice()
}
}
我們做一次性能測(cè)試,并且禁用內(nèi)聯(lián)和優(yōu)化纲酗,來(lái)觀察切片的堆上內(nèi)存分配的情況衰腌。
go test -bench . -benchmem -gcflags "-N -l"
輸出結(jié)果比較“令人意外”:
BenchmarkArray-4 500000 3637 ns/op 0 B/op 0 alloc s/op
BenchmarkSlice-4 300000 4055 ns/op 8192 B/op 1 alloc s/op
解釋一下上述結(jié)果逝淹,在測(cè)試 Array 的時(shí)候,用的是4核桶唐,循環(huán)次數(shù)是500000栅葡,平均每次執(zhí)行時(shí)間是3637 ns,每次執(zhí)行堆上分配內(nèi)存總量是0尤泽,分配次數(shù)也是0 欣簇。
而切片的結(jié)果就“差”一點(diǎn),同樣也是用的是4核坯约,循環(huán)次數(shù)是300000熊咽,平均每次執(zhí)行時(shí)間是4055 ns,但是每次執(zhí)行一次闹丐,堆上分配內(nèi)存總量是8192横殴,分配次數(shù)也是1 。
這樣對(duì)比看來(lái)卿拴,并非所有時(shí)候都適合用切片代替數(shù)組衫仑,因?yàn)榍衅讓訑?shù)組可能會(huì)在堆上分配內(nèi)存,而且小數(shù)組在棧上拷貝的消耗也未必比
make 消耗大堕花。
二. 切片的數(shù)據(jù)結(jié)構(gòu)
切片本身并不是動(dòng)態(tài)數(shù)組或者數(shù)組指針文狱。它內(nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)通過(guò)指針引用底層數(shù)組,設(shè)定相關(guān)屬性將數(shù)據(jù)讀寫(xiě)操作限定在指定的區(qū)域內(nèi)缘挽。切片本身是一個(gè)只讀對(duì)象瞄崇,其工作機(jī)制類似數(shù)組指針的一種封裝。
切片(slice)是對(duì)數(shù)組一個(gè)連續(xù)片段的引用壕曼,所以切片是一個(gè)引用類型(因此更類似于 C/C++ 中的數(shù)組類型苏研,或者 Python 中的 list 類型)。這個(gè)片段可以是整個(gè)數(shù)組腮郊,或者是由起始和終止索引標(biāo)識(shí)的一些項(xiàng)的子集摹蘑。需要注意的是,終止索引標(biāo)識(shí)的項(xiàng)不包括在切片內(nèi)伴榔。切片提供了一個(gè)與指向數(shù)組的動(dòng)態(tài)窗口纹蝴。
給定項(xiàng)的切片索引可能比相關(guān)數(shù)組的相同元素的索引小。和數(shù)組不同的是踪少,切片的長(zhǎng)度可以在運(yùn)行時(shí)修改塘安,最小為 0 最大為相關(guān)數(shù)組的長(zhǎng)度:切片是一個(gè)長(zhǎng)度可變的數(shù)組。
Slice 的數(shù)據(jù)結(jié)構(gòu)定義如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片的結(jié)構(gòu)體由3部分構(gòu)成援奢,Pointer 是指向一個(gè)數(shù)組的指針兼犯,len 代表當(dāng)前切片的長(zhǎng)度,cap 是當(dāng)前切片的容量。cap 總是大于等于 len 的切黔。
如果想從 slice 中得到一塊內(nèi)存地址砸脊,可以這樣做:
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
如果反過(guò)來(lái)呢?從 Go 的內(nèi)存地址中構(gòu)造一個(gè) slice纬霞。
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
構(gòu)造一個(gè)虛擬的結(jié)構(gòu)體凌埂,把 slice 的數(shù)據(jù)結(jié)構(gòu)拼出來(lái)。
當(dāng)然還有更加直接的方法诗芜,在 Go 的反射中就存在一個(gè)與之對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu) SliceHeader瞳抓,我們可以用它來(lái)構(gòu)造一個(gè) slice
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)
三. 創(chuàng)建切片
make 函數(shù)允許在運(yùn)行期動(dòng)態(tài)指定數(shù)組長(zhǎng)度,繞開(kāi)了數(shù)組類型必須使用編譯期常量的限制伏恐。
創(chuàng)建切片有兩種形式孩哑,make 創(chuàng)建切片,空切片翠桦。
1. make 和切片字面量
func makeslice(et *_type, len, cap int) slice {
// 根據(jù)切片的數(shù)據(jù)類型横蜒,獲取切片的最大容量
maxElements := maxSliceCap(et.size)
// 比較切片的長(zhǎng)度憾朴,長(zhǎng)度值域應(yīng)該在[0,maxElements]之間
if len < 0 || uintptr(len) > maxElements {
panic(errorString("makeslice: len out of range"))
}
// 比較切片的容量申屹,容量值域應(yīng)該在[len,maxElements]之間
if cap < len || uintptr(cap) > maxElements {
panic(errorString("makeslice: cap out of range"))
}
// 根據(jù)切片的容量申請(qǐng)內(nèi)存
p := mallocgc(et.size*uintptr(cap), et, true)
// 返回申請(qǐng)好內(nèi)存的切片的首地址
return slice{p, len, cap}
}
還有一個(gè) int64 的版本:
func makeslice64(et *_type, len64, cap64 int64) slice {
len := int(len64)
if int64(len) != len64 {
panic(errorString("makeslice: len out of range"))
}
cap := int(cap64)
if int64(cap) != cap64 {
panic(errorString("makeslice: cap out of range"))
}
return makeslice(et, len, cap)
}
實(shí)現(xiàn)原理和上面的是一樣的,只不過(guò)多了把 int64 轉(zhuǎn)換成 int 這一步罷了蒙兰。
上圖是用 make 函數(shù)創(chuàng)建的一個(gè) len = 4闻鉴, cap = 6 的切片茵乱。內(nèi)存空間申請(qǐng)了6個(gè) int 類型的內(nèi)存大小。由于 len = 4孟岛,所以后面2個(gè)暫時(shí)訪問(wèn)不到,但是容量還是在的督勺。這時(shí)候數(shù)組里面每個(gè)變量都是0 渠羞。
除了 make 函數(shù)可以創(chuàng)建切片以外,字面量也可以創(chuàng)建切片智哀。
這里是用字面量創(chuàng)建的一個(gè) len = 6次询,cap = 6 的切片,這時(shí)候數(shù)組里面每個(gè)元素的值都初始化完成了瓷叫。需要注意的是 [ ] 里面不要寫(xiě)數(shù)組的容量屯吊,因?yàn)槿绻麑?xiě)了個(gè)數(shù)以后就是數(shù)組了,而不是切片了摹菠。
還有一種簡(jiǎn)單的字面量創(chuàng)建切片的方法盒卸。如上圖。上圖就 Slice A 創(chuàng)建出了一個(gè) len = 3次氨,cap = 3 的切片蔽介。從原數(shù)組的第二位元素(0是第一位)開(kāi)始切,一直切到第四位為止(不包括第五位)。同理虹蓄,Slice B 創(chuàng)建出了一個(gè) len = 2犀呼,cap = 4 的切片。
2. nil 和空切片
nil 切片和空切片也是常用的薇组。
var slice []int
nil 切片被用在很多標(biāo)準(zhǔn)庫(kù)和內(nèi)置函數(shù)中外臂,描述一個(gè)不存在的切片的時(shí)候,就需要用到 nil 切片律胀。比如函數(shù)在發(fā)生異常的時(shí)候专钉,返回的切片就是 nil 切片。nil 切片的指針指向 nil累铅。
空切片一般會(huì)用來(lái)表示一個(gè)空的集合跃须。比如數(shù)據(jù)庫(kù)查詢,一條結(jié)果也沒(méi)有查到娃兽,那么就可以返回一個(gè)空切片菇民。
silce := make( []int , 0 )
slice := []int{ }
空切片和 nil 切片的區(qū)別在于,空切片指向的地址不是nil投储,指向的是一個(gè)內(nèi)存地址第练,但是它沒(méi)有分配任何內(nèi)存空間,即底層元素包含0個(gè)元素玛荞。
最后需要說(shuō)明的一點(diǎn)是娇掏。不管是使用 nil 切片還是空切片,對(duì)其調(diào)用內(nèi)置函數(shù) append勋眯,len 和 cap 的效果都是一樣的婴梧。
四. 切片擴(kuò)容
當(dāng)一個(gè)切片的容量滿了,就需要擴(kuò)容了客蹋。怎么擴(kuò)塞蹭,策略是什么?
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&et))
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if et.size == 0 {
// 如果新要擴(kuò)容的容量比原來(lái)的容量還要小讶坯,這代表要縮容了番电,那么可以直接報(bào)panic了。
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// 如果當(dāng)前切片的大小為0辆琅,還調(diào)用了擴(kuò)容方法漱办,那么就新生成一個(gè)新的容量的切片返回。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
// 這里就是擴(kuò)容的策略
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
// 計(jì)算新的切片的容量婉烟,長(zhǎng)度娩井。
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}
// 判斷非法的值,保證容量是在增加隅很,并且容量不超過(guò)最大容量
if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
// 在老的切片后面繼續(xù)擴(kuò)充容量
p = mallocgc(capmem, nil, false)
// 將 lenmem 這個(gè)多個(gè) bytes 從 old.array地址 拷貝到 p 的地址處
memmove(p, old.array, lenmem)
// 先將 P 地址加上新的容量得到新切片容量的地址撞牢,然后將新切片容量地址后面的 capmem-newlenmem 個(gè) bytes 這塊內(nèi)存初始化率碾。為之后繼續(xù) append() 操作騰出空間。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 重新申請(qǐng)新的數(shù)組給新切片
// 重新申請(qǐng) capmen 這個(gè)大的內(nèi)存地址屋彪,并且初始化為0值
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
// 如果還不能打開(kāi)寫(xiě)鎖所宰,那么只能把 lenmem 大小的 bytes 字節(jié)從 old.array 拷貝到 p 的地址處
memmove(p, old.array, lenmem)
} else {
// 循環(huán)拷貝老的切片的值
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}
// 返回最終新切片,容量更新為最新擴(kuò)容之后的容量
return slice{p, old.len, newcap}
}
上述就是擴(kuò)容的實(shí)現(xiàn)畜挥。主要需要關(guān)注的有兩點(diǎn)仔粥,一個(gè)是擴(kuò)容時(shí)候的策略,還有一個(gè)就是擴(kuò)容是生成全新的內(nèi)存地址還是在原來(lái)的地址后追加蟹但。
1. 擴(kuò)容策略
先看看擴(kuò)容策略躯泰。
func main() {
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
newSlice[1] += 10
fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}
輸出結(jié)果:
Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
用圖表示出上述過(guò)程。
從圖上我們可以很容易的看出华糖,新的切片和之前的切片已經(jīng)不同了麦向,因?yàn)樾碌那衅牧艘粋€(gè)值,并沒(méi)有影響到原來(lái)的數(shù)組客叉,新切片指向的數(shù)組是一個(gè)全新的數(shù)組诵竭。并且 cap 容量也發(fā)生了變化。這之間究竟發(fā)生了什么呢兼搏?
Go 中切片擴(kuò)容的策略是這樣的:
如果切片的容量小于 1024 個(gè)元素卵慰,于是擴(kuò)容的時(shí)候就翻倍增加容量。上面那個(gè)例子也驗(yàn)證了這一情況佛呻,總?cè)萘繌脑瓉?lái)的4個(gè)翻倍到現(xiàn)在的8個(gè)裳朋。
一旦元素個(gè)數(shù)超過(guò) 1024 個(gè)元素,那么增長(zhǎng)因子就變成 1.25 吓著,即每次增加原來(lái)容量的四分之一鲤嫡。
注意:擴(kuò)容擴(kuò)大的容量都是針對(duì)原來(lái)的容量而言的,而不是針對(duì)原來(lái)數(shù)組的長(zhǎng)度而言的夜矗。
2. 新數(shù)組 or 老數(shù)組 泛范?
再談?wù)剶U(kuò)容之后的數(shù)組一定是新的么?這個(gè)不一定紊撕,分兩種情況。
情況一:
func main() {
array := [4]int{10, 20, 30, 40}
slice := array[0:2]
newSlice := append(slice, 50)
fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
newSlice[1] += 10
fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
fmt.Printf("After array = %v\n", array)
}
打印輸出:
Before slice = [10 20], Pointer = 0xc4200c0040, len = 2, cap = 4
Before newSlice = [10 20 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After slice = [10 30], Pointer = 0xc4200c0040, len = 2, cap = 4
After newSlice = [10 30 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After array = [10 30 50 40]
把上述過(guò)程用圖表示出來(lái)赡突,如下圖对扶。
通過(guò)打印的結(jié)果,我們可以看到惭缰,在這種情況下浪南,擴(kuò)容以后并沒(méi)有新建一個(gè)新的數(shù)組,擴(kuò)容前后的數(shù)組都是同一個(gè)漱受,這也就導(dǎo)致了新的切片修改了一個(gè)值络凿,也影響到了老的切片了。并且 append() 操作也改變了原來(lái)數(shù)組里面的值。一個(gè) append() 操作影響了這么多地方絮记,如果原數(shù)組上有多個(gè)切片摔踱,那么這些切片都會(huì)被影響!無(wú)意間就產(chǎn)生了莫名的 bug怨愤!
這種情況派敷,由于原數(shù)組還有容量可以擴(kuò)容,所以執(zhí)行 append() 操作以后撰洗,會(huì)在原數(shù)組上直接操作篮愉,所以這種情況下,擴(kuò)容以后的數(shù)組還是指向原來(lái)的數(shù)組差导。
這種情況也極容易出現(xiàn)在字面量創(chuàng)建切片時(shí)候试躏,第三個(gè)參數(shù) cap 傳值的時(shí)候,如果用字面量創(chuàng)建切片设褐,cap 并不等于指向數(shù)組的總?cè)萘康咴蹋敲催@種情況就會(huì)發(fā)生。
slice := array[1:2:3]
上面這種情況非常危險(xiǎn)络断,極度容易產(chǎn)生 bug 裁替。
建議用字面量創(chuàng)建切片的時(shí)候,cap 的值一定要保持清醒貌笨,避免共享原數(shù)組導(dǎo)致的 bug弱判。
情況二:
情況二其實(shí)就是在擴(kuò)容策略里面舉的例子,在那個(gè)例子中之所以生成了新的切片锥惋,是因?yàn)樵瓉?lái)數(shù)組的容量已經(jīng)達(dá)到了最大值昌腰,再想擴(kuò)容, Go 默認(rèn)會(huì)先開(kāi)一片內(nèi)存區(qū)域膀跌,把原來(lái)的值拷貝過(guò)來(lái)遭商,然后再執(zhí)行 append() 操作。這種情況絲毫不影響原數(shù)組捅伤。
所以建議盡量避免情況一劫流,盡量使用情況二,避免 bug 產(chǎn)生丛忆。
五. 切片拷貝
Slice 中拷貝方法有2個(gè)祠汇。
func slicecopy(to, fm slice, width uintptr) int {
// 如果源切片或者目標(biāo)切片有一個(gè)長(zhǎng)度為0,那么就不需要拷貝熄诡,直接 return
if fm.len == 0 || to.len == 0 {
return 0
}
// n 記錄下源切片或者目標(biāo)切片較短的那一個(gè)的長(zhǎng)度
n := fm.len
if to.len < n {
n = to.len
}
// 如果入?yún)?width = 0可很,也不需要拷貝了,返回較短的切片的長(zhǎng)度
if width == 0 {
return n
}
// 如果開(kāi)啟了競(jìng)爭(zhēng)檢測(cè)
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&to))
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
// 如果開(kāi)啟了 The memory sanitizer (msan)
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 {
// TODO: is this still worth it with new memmove impl?
// 如果只有一個(gè)元素凰浮,那么指針直接轉(zhuǎn)換即可
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
// 如果不止一個(gè)元素我抠,那么就把 size 個(gè) bytes 從 fm.array 地址開(kāi)始苇本,拷貝到 to.array 地址之后
memmove(to.array, fm.array, size)
}
return n
}
在這個(gè)方法中,slicecopy 方法會(huì)把源切片值(即 fm Slice )中的元素復(fù)制到目標(biāo)切片(即 to Slice )中菜拓,并返回被復(fù)制的元素個(gè)數(shù)瓣窄,copy 的兩個(gè)類型必須一致。slicecopy 方法最終的復(fù)制結(jié)果取決于較短的那個(gè)切片尘惧,當(dāng)較短的切片復(fù)制完成康栈,整個(gè)復(fù)制過(guò)程就全部完成了。
舉個(gè)例子喷橙,比如:
func main() {
array := []int{10, 20, 30, 40}
slice := make([]int, 6)
n := copy(slice, array)
fmt.Println(n,slice)
}
還有一個(gè)拷貝的方法啥么,這個(gè)方法原理和 slicecopy 方法類似,不在贅述了贰逾,注釋寫(xiě)在代碼里面了悬荣。
func slicestringcopy(to []byte, fm string) int {
// 如果源切片或者目標(biāo)切片有一個(gè)長(zhǎng)度為0,那么就不需要拷貝疙剑,直接 return
if len(fm) == 0 || len(to) == 0 {
return 0
}
// n 記錄下源切片或者目標(biāo)切片較短的那一個(gè)的長(zhǎng)度
n := len(fm)
if len(to) < n {
n = len(to)
}
// 如果開(kāi)啟了競(jìng)爭(zhēng)檢測(cè)
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&to))
pc := funcPC(slicestringcopy)
racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
}
// 如果開(kāi)啟了 The memory sanitizer (msan)
if msanenabled {
msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
}
// 拷貝字符串至字節(jié)數(shù)組
memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}
再舉個(gè)例子氯迂,比如:
func main() {
slice := make([]byte, 3)
n := copy(slice, "abcdef")
fmt.Println(n,slice)
}
輸出:
3 [97,98,99]
說(shuō)到拷貝,切片中有一個(gè)需要注意的問(wèn)題言缤。
func main() {
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
}
}
輸出:
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338
從上面結(jié)果我們可以看到嚼蚀,如果用 range 的方式去遍歷一個(gè)切片,拿到的 Value 其實(shí)是切片里面的值拷貝管挟。所以每次打印 Value 的地址都不變轿曙。
由于 Value 是值拷貝的,并非引用傳遞僻孝,所以直接改 Value 是達(dá)不到更改原切片值的目的的导帝,需要通過(guò) &slice[index]
獲取真實(shí)的地址。
Reference:
《Go in action》
《Go 語(yǔ)言學(xué)習(xí)筆記》
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: https://halfrost.com/go_slice/