golang之slice剖析
一、概述
切片是 Go 中的一種基本的資料結構,使用這種結構可以用來管理資料集合。切片的設計想法是由動態陣列概念而來,為了開發者可以更加方便的使一個數據結構可以自動增加和減少。但是切片本身並不是動態資料或者陣列指標。切片常見的操作有 reslice、append、copy。與此同時,切片還具有可索引,可迭代的優秀特性。
1.切片和陣列關於切片和陣列怎麼選擇?
在 Go 中,Go 陣列是值型別,賦值和函式傳參操作都會複製整個陣列資料。
1 func main() { 2arrayA := [2]int{100, 200}// 定義陣列並初始化內容 3var arrayB [2]int // 定義一個數組 4arrayB = arrayA 5fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) 6fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB) 7testArray(arrayA) 8 } 9 func testArray(x [2]int) {// 此處使用值傳遞 會導致地址不同 10fmt.Printf("func Array : %p , %v\n", &x, x) 11 }
列印結果:
arrayA : 0xc4200bebf0 , [100 200] arrayB : 0xc4200bec00 , [100 200] func Array : 0xc4200bec30 , [100 200]
可以看到,三個記憶體地址都不同,這也就驗證了 Go 中陣列賦值和函式傳參都是值複製的。那這會導致什麼問題呢?
假想每次傳參都用陣列,那麼每次陣列都要被複制一遍。如果陣列大小有 100萬,在64位機器上就需要花費大約 800W bytes,即 8MB 記憶體。這樣會消耗掉大量的記憶體。於是乎有人想到,函式傳參用陣列的指標。
1 func main() { 2arrayA := [2]int{100, 200} 3testArrayPoint(&arrayA)// 1.傳陣列指標 4arrayB := arrayA[:] 5testArrayPoint(&arrayB)// 2.傳切片 6fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) 7 } 8 func testArrayPoint(x *[]int) {// 此處使用的指標,會導致函式引數和arrayB指向同一塊記憶體 9fmt.Printf("func Array : %p , %v\n", x, *x) 10(*x)[1] += 100 11 }
列印結果:
func Array : 0xc4200b0140 , [100 200] func Array : 0xc4200b0180 , [100 300] arrayA : 0xc4200b0140 , [100 400]
這也就證明了陣列指標確實到達了我們想要的效果。現在就算是傳入10億的陣列,也只需要再棧上分配一個8個位元組的記憶體給指標就可以了。這樣更加高效的利用記憶體,效能也比之前的好。
不過傳指標會有一個弊端,從列印結果可以看到,第一行和第三行指標地址都是同一個,萬一原陣列的指標指向更改了,那麼函式裡面的指標指向都會跟著更改。
切片的優勢也就表現出來了。用切片傳陣列引數,既可以達到節約記憶體的目的,也可以達到合理處理好共享記憶體的問題。列印結果第二行就是切片,切片的指標和原來陣列的指標是不同的。
由此我們可以得出結論:
把第一個大陣列傳遞給函式會消耗很多記憶體,採用切片的方式傳參可以避免上述問題。切片是引用傳遞,所以它們不需要使用額外的記憶體並且比使用陣列更有效率。但是,依舊有反例。
package bench_test 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{ var 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() } } // // 雖然相對值傳遞,引用傳遞在給函式時不需要複製整個資料;但是並不以為所有的操作都需要使用slice 上面的例子是很好的驗證 // func array_param(x [81920]int) [81920]int { //var x [1024]int for i:=0; i<len(x);i++{ x[i] = i } return x } func slice_param(x []int) []int{ //var x = make([]int, 1024) for i:=0; i<len(x);i++{ x[i] = i } return x } func BenchmarkArrayParam(b *testing.B){ var x = [81920]int{} for i := 0; i < b.N; i++{ array_param(x) } } func BenchmarkSliceParam(b *testing.B){ var x = make([]int, 81920) for i := 0; i < b.N; i++{ slice_param(x) } } // // 當將陣列和切片作為函式引數時 其對應的引數資料量越大 相對來說切片的的引用傳遞會凸顯其優勢 // 不過需要需要注意的slice會涉及在heap進行記憶體分配: //切片底層陣列可能會在堆上分配記憶體,這樣使用陣列在stack進行拷貝未必弱於make的記憶體分配 //
我們做一次效能測試,並且禁用內聯和優化,來觀察切片的堆上記憶體分配的情況。
go test -bench . -benchmem -gcflags -N -l
輸出結果比較“令人意外”:
goos: linux goarch: amd64 pkg: gonotes/lesson-1/bench_test BenchmarkArray-810000001925 ns/op0 B/op0 allocs/op BenchmarkSlice-85000004158 ns/op8192 B/op1 allocs/op BenchmarkArrayParam-810000182077 ns/op0 B/op0 allocs/op BenchmarkSliceParam-810000166897 ns/op65 B/op0 allocs/op PASS okgonotes/lesson-1/bench_test7.589s
在測試 Array 的時候,用的是8核,迴圈次數是1000000,平均每次執行時間是1925 ns,每次執行堆上分配記憶體總量是0,分配次數也是0 。
而切片的結果就“差”一點,同樣也是用的是8核,迴圈次數是500000,平均每次執行時間是4158 ns,但是每次執行一次,堆上分配記憶體總量是8192,分配次數也是1 。
並非所有時候都適合用切片代替陣列,因為切片底層陣列可能會在堆上分配記憶體,而且小陣列在棧上拷貝的消耗也未必比 make 消耗大。
2.切片的資料結構
切片本身並不是動態陣列或者陣列指標。它內部實現的資料結構通過指標引用底層陣列,設定相關屬性將資料讀寫操作限定在指定的區域內。切片本身是一個只讀物件,其工作機制類似陣列指標的一種封裝。
切片(slice)是對陣列一個連續片段的引用,所以切片是一個引用型別(因此更類似於 C/C++ 中的陣列型別,或者 java 中的 list 型別)。這個片段可以是整個陣列,或者是由起始和終止索引標識的一些項的子集。需要注意的是,終止索引標識的項不包括在切片內。切片提供了一個與指向陣列的動態視窗。
給定項的切片索引可能比相關陣列的相同元素的索引小。和陣列不同的是,切片的長度可以在執行時修改,最小為 0 最大為相關陣列的長度:切片是一個長度可變的陣列。
Slice 的資料結構定義如下:
type slice struct { array unsafe.Pointer lenint capint }
切片的結構體由3部分構成,Pointer 是指向一個數組的指標,len 代表當前切片的長度,cap 是當前切片的容量。cap 總是大於等於 len 的。
slice
內部結構
如果想從 slice 中得到一塊記憶體地址,可以這樣做:
s := make([]byte, 200) ptr := unsafe.Pointer(s[0])
如果反過來呢?從 Go 的記憶體地址中構造一個 slice。
var ptr unsafe.Pointer var s1 = struct { addr uintptr4 len int5 cap int6 }{ptr, length, length} s := *(*[]byte)(unsafe.Pointer(s1))
構造一個虛擬的結構體,把 slice 的資料結構拼出來。
在 Go 的反射中就存在一個與之對應的資料結構 SliceHeader,我們可以用它來構造一個 slice
var o []bytesliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(o))) sliceHeader.Cap = length sliceHeader.Len = length sliceHeader.Data = uintptr(ptr)
3.建立切片
make 函式允許在執行期動態指定陣列長度,繞開了陣列型別必須使用編譯期常量的限制。
建立切片有兩種形式,make 建立切片,空切片。
3.1. make 和切片字面量
1 func makeslice(et *_type, len, cap int) slice {// 建立slice方法 2// 根據切片的資料型別,獲取切片的最大容量 3maxElements := maxSliceCap(et.size) 4// 比較切片的長度,長度值域應該在[0,maxElements]之間 5if len < 0 || uintptr(len) > maxElements { 6panic(errorString("makeslice: len out of range")) 7} 8// 比較切片的容量,容量值域應該在[len,maxElements]之間 9if cap < len || uintptr(cap) > maxElements { 10panic(errorString("makeslice: cap out of range")) 11} 12// 根據切片的容量申請記憶體 13p := mallocgc(et.size*uintptr(cap), et, true) 14// 返回申請好記憶體的切片的首地址 15return slice{p, len, cap} 16 }
還有一個 int64 的版本:
1 func makeslice64(et *_type, len64, cap64 int64) slice { 2len := int(len64) 3if int64(len) != len64 { 4panic(errorString("makeslice: len out of range")) 5} 6cap := int(cap64) 7if int64(cap) != cap64 { 8panic(errorString("makeslice: cap out of range")) 9} 10return makeslice(et, len, cap) 11 }
兩個方法差別在於,只不過多了把 int64 轉換成 int 這一步罷了。
make操作方式
上圖是用 make 函式建立的一個 len = 4, cap = 6 的切片。記憶體空間申請了6個 int 型別的記憶體大小。由於 len = 4,所以後面2個暫時訪問不到,但是容量還是在的。這時候數組裡面每個變數都是0 。
除了 make 函式可以建立切片以外,字面量也可以建立切片。
字面量方式
這裡是用字面量建立的一個 len = 6,cap = 6 的切片,這時候數組裡面每個元素的值都初始化完成了。需要注意的是 [ ] 裡面不要寫陣列的容量,因為如果寫了個數以後就是陣列了,而不是切片了。
圖片.png
還有一種簡單的字面量建立切片的方法。如上圖。上圖就 Slice A 創建出了一個 len = 3,cap = 3 的切片。從原陣列的第二位元素(0是第一位)開始切,一直切到第四位為止(不包括第五位)。同理,Slice B 創建出了一個 len = 2,cap = 4 的切片。
3.2. nil 和空切片
nil 切片和空切片也是常用的。
空切片
nil 切片被用在很多標準庫和內建函式中,描述一個不存在的切片的時候,就需要用到 nil 切片。比如函式在發生異常的時候,返回的切片就是 nil 切片。nil 切片的指標指向 nil。
空切片一般會用來表示一個空的集合。比如資料庫查詢,一條結果也沒有查到,那麼就可以返回一個空切片。
silce := make( []int , 0 ) slice := []int{ }
空切片
空切片和 nil 切片的區別在於,空切片指向的地址不是nil,指向的是一個記憶體地址,但是它沒有分配任何記憶體空間,即底層元素包含0個元素。
最後需要說明的一點是。不管是使用 nil 切片還是空切片,對其呼叫內建函式 append,len 和 cap 的效果都是一樣的。
4.切片擴容
當一個切片的容量滿了,就需要擴容了。怎麼擴,策略是什麼?
1 func growslice(et *_type, old slice, cap int) slice { 2if raceenabled { 3callerpc := getcallerpc(unsafe.Pointer(&et)) 4racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc,funcPC(growslice)) 5} 6if msanenabled { 7msanread(old.array, uintptr(old.len*int(et.size))) 8} 9if et.size == 0 { 10// 如果新要擴容的容量比原來的容量還要小,這代表要縮容了,那麼可以直接報panic了。 11if cap < old.cap { 12panic(errorString("growslice: cap out of range")) 13} 14// 如果當前切片的大小為0,還呼叫了擴容方法,那麼就新生成一個新的容量的切片返回。 15return slice{unsafe.Pointer(&zerobase), old.len, cap} 16} 17// 這裡就是擴容的策略 18newcap := old.cap 19doublecap := newcap + newcap 20if cap > doublecap { 21newcap = cap 22} else { 23if old.len < 1024 { 24newcap = doublecap 25} else { 26for newcap < cap { 27newcap += newcap / 4 28} 29} 30} 31// 計算新的切片的容量,長度。 32var lenmem, newlenmem, capmem uintptr 33const ptrSize = unsafe.Sizeof((*byte)(nil)) 34switch et.size { 35case 1: 36lenmem = uintptr(old.len) 37newlenmem = uintptr(cap) 38capmem = roundupsize(uintptr(newcap)) 39newcap = int(capmem) 40case ptrSize: 41lenmem = uintptr(old.len) * ptrSize 42newlenmem = uintptr(cap) * ptrSize 43capmem = roundupsize(uintptr(newcap) * ptrSize) 44newcap = int(capmem / ptrSize) 45default: 46lenmem = uintptr(old.len) * et.size 47newlenmem = uintptr(cap) * et.size 48capmem = roundupsize(uintptr(newcap) * et.size) 49newcap = int(capmem / et.size) 50} 51// 判斷非法的值,保證容量是在增加,並且容量不超過最大容量 52if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) { 53panic(errorString("growslice: cap out of range")) 54} 55var p unsafe.Pointer 56if et.kind&kindNoPointers != 0 { 57// 在老的切片後面繼續擴充容量 58p = mallocgc(capmem, nil, false) 59// 將 lenmem 這個多個 bytes 從 old.array地址 拷貝到 p 的地址處 60memmove(p, old.array, lenmem) 61// 先將 P 地址加上新的容量得到新切片容量的地址,然後將新切片容量地址後面的 capmem-newlenmem 個 bytes 這塊記憶體初始化。為之後繼續 append() 操作騰出空間。 62memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) 63} else { 64// 重新申請新的陣列給新切片 65// 重新申請 capmen 這個大的記憶體地址,並且初始化為0值 66p = mallocgc(capmem, et, true) 67if !writeBarrier.enabled { 68// 如果還不能開啟寫鎖,那麼只能把 lenmem 大小的 bytes 位元組從 old.array 拷貝到 p 的地址處 69memmove(p, old.array, lenmem) 70} else { 71// 迴圈拷貝老的切片的值 72for i := uintptr(0); i < lenmem; i += et.size { 73typedmemmove(et, add(p, i), add(old.array, i)) 74} 75} 76} 77// 返回最終新切片,容量更新為最新擴容之後的容量 78return slice{p, old.len, newcap} 79 }
上述就是擴容的實現。主要需要關注的有兩點,一個是擴容時候的策略,還有一個就是擴容是生成全新的記憶體地址還是在原來的地址後追加。
-
擴容策略
先看看擴容策略。
1 func main() { 2slice := []int{10, 20, 30, 40} 3newSlice := append(slice, 50) 4fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 5fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 6newSlice[1] += 10 7fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 8fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 9 }
輸出結果:
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
用圖表示出上述過程。
執行過程
從圖上我們可以很容易的看出,新的切片和之前的切片已經不同了,因為新的切片更改了一個值,並沒有影響到原來的陣列,新切片指向的陣列是一個全新的陣列。並且 cap 容量也發生了變化。這之間究竟發生了什麼呢?
Go 中切片擴容的策略是這樣的:
如果切片的容量小於 1024 個元素,於是擴容的時候就翻倍增加容量。上面那個例子也驗證了這一情況,總容量從原來的4個翻倍到現在的8個。
一旦元素個數超過 1024 個元素,那麼增長因子就變成 1.25 ,即每次增加原來容量的四分之一。
注意:擴容擴大的容量都是針對原來的容量而言的,而不是針對原來陣列的長度而言的。
-
新陣列 or 老陣列 ?
再談談擴容之後的陣列一定是新的麼?這個不一定,分兩種情況。
情況一:
1 func main() { 2array := [4]int{10, 20, 30, 40} 3slice := array[0:2] 4newSlice := append(slice, 50) 5fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 6fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 7newSlice[1] += 10 8fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 9fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 10fmt.Printf("After array = %v\n", array) 11 }
列印輸出:
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]
把上述過程用圖表示出來,如下圖。
例項執行過程
通過列印的結果,我們可以看到,在這種情況下,擴容以後並沒有新建一個新的陣列,擴容前後的陣列都是同一個,這也就導致了新的切片修改了一個值,也影響到了老的切片了。並且 append() 操作也改變了原來數組裡面的值。一個 append() 操作影響了這麼多地方,如果原陣列上有多個切片,那麼這些切片都會被影響!無意間就產生了莫名的 bug!
這種情況,由於原陣列還有容量可以擴容,所以執行 append() 操作以後,會在原陣列上直接操作,所以這種情況下,擴容以後的陣列還是指向原來的陣列。
這種情況也極容易出現在字面量建立切片時候,第三個引數 cap 傳值的時候,如果用字面量建立切片,cap 並不等於指向陣列的總容量,那麼這種情況就會發生。
slice := array[1:2:3]
上面這種情況非常危險,極度容易產生 bug 。
建議用字面量建立切片的時候,cap 的值一定要保持清醒,避免共享原陣列導致的 bug。
情況二:
情況二其實就是在擴容策略裡面舉的例子,在那個例子中之所以生成了新的切片,是因為原來陣列的容量已經達到了最大值,再想擴容, Go 預設會先開一片記憶體區域,把原來的值拷貝過來,然後再執行 append() 操作。這種情況絲毫不影響原陣列。
所以建議儘量避免情況一,儘量使用情況二,避免 bug 產生。
五. 切片拷貝
Slice 中拷貝方法有2個。
1 func slicecopy(to, fm slice, width uintptr) int { 2// 如果源切片或者目標切片有一個長度為0,那麼就不需要拷貝,直接 return 3if fm.len == 0 || to.len == 0 { 4return 0 5} 6// n 記錄下源切片或者目標切片較短的那一個的長度 7n := fm.len 8if to.len < n { 9n = to.len 10} 11// 如果入參 width = 0,也不需要拷貝了,返回較短的切片的長度 12if width == 0 { 13return n 14} 15// 如果開啟了競爭檢測 16if raceenabled { 17callerpc := getcallerpc(unsafe.Pointer(&to)) 18pc := funcPC(slicecopy) 19racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc) 20racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc) 21} 22// 如果開啟了 The memory sanitizer (msan) 23if msanenabled { 24msanwrite(to.array, uintptr(n*int(width))) 25msanread(fm.array, uintptr(n*int(width))) 26} 27size := uintptr(n) * width 28if size == 1 { 29// TODO: is this still worth it with new memmove impl? 30// 如果只有一個元素,那麼指標直接轉換即可 31*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer 32} else { 33// 如果不止一個元素,那麼就把 size 個 bytes 從 fm.array 地址開始,拷貝到 to.array 地址之後 34memmove(to.array, fm.array, size) 35} 36return n 37 }
在這個方法中,slicecopy 方法會把源切片值(即 fm Slice )中的元素複製到目標切片(即 to Slice )中,並返回被複制的元素個數,copy 的兩個型別必須一致。slicecopy 方法最終的複製結果取決於較短的那個切片,當較短的切片複製完成,整個複製過程就全部完成了。
copy過程
舉個例子,比如:
1 func main() { 2array := []int{10, 20, 30, 40} 3slice := make([]int, 6) 4n := copy(slice, array) 5fmt.Println(n,slice) 6 }
還有一個拷貝的方法,這個方法原理和 slicecopy 方法類似,不在贅述了,註釋寫在程式碼裡面了。
1 func slicestringcopy(to []byte, fm string) int { 2// 如果源切片或者目標切片有一個長度為0,那麼就不需要拷貝,直接 return 3if len(fm) == 0 || len(to) == 0 { 4return 0 5} 6// n 記錄下源切片或者目標切片較短的那一個的長度 7n := len(fm) 8if len(to) < n { 9n = len(to) 10} 11// 如果開啟了競爭檢測 12if raceenabled { 13callerpc := getcallerpc(unsafe.Pointer(&to)) 14pc := funcPC(slicestringcopy) 15racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc) 16} 17// 如果開啟了 The memory sanitizer (msan) 18if msanenabled { 19msanwrite(unsafe.Pointer(&to[0]), uintptr(n)) 20} 21// 拷貝字串至位元組陣列 22memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n)) 23return n 24 }
再舉個例子,比如:
1 func main() { 2slice := make([]byte, 3) 3n := copy(slice, "abcdef") 4fmt.Println(n,slice) 5 }
輸出:
13 [97,98,99]
說到拷貝,切片中有一個需要注意的問題。
1 func main() { 2slice := []int{10, 20, 30, 40} 3for index, value := range slice { 4fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index]) 5} 6 }
輸出:
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
從上面結果我們可以看到,如果用 range 的方式去遍歷一個切片,拿到的 Value 其實是切片裡面的值拷貝。所以每次列印 Value 的地址都不變。
vale的值拷貝
由於 Value 是值拷貝的,並非引用傳遞,所以直接改 Value 是達不到更改原切片值的目的的,需要通過slice[index] 獲取真實的地址。
六、簡單demo
package main import ( "fmt" "unsafe" ) func main() { xx := []int{100, 200, 300, 400, 500, 600, 700, 800} xxx := xx[2:6] fmt.Printf("xx's address is %p, %v\n", &xx, xx) fmt.Printf("xxx's address is %p, %v\n", &xxx, xxx) xxx[2] += 1000 // 由於指向同一塊記憶體會影響xx原有的內容 fmt.Printf("after updated, xxx's address %p, %v\n", &xxx, xxx) fmt.Printf("after updated, xx's address %p, %v\n", &xx, xx) fmt.Println("======================================") x := make([]int,0 ,5) fmt.Println(unsafe.Pointer(&x)) fmt.Printf("before append x's address =%p\n", &x) for i := 0; i<8; i++{ x = append(x,i) } fmt.Printf("after appendx's address =%p\n", &x) fmt.Println(unsafe.Pointer(&x)) fmt.Println("======================================") // 擴容都是在原有的地址上進行追加 也就會導致擴容前後記憶體地址是不變的 slice1 := make([]int, 0) fmt.Printf("slice1's address is %p \n", &slice1) for i := 0; i<1024; i++{ slice1 = append(slice1,i) } fmt.Printf("after append slice1's address is %p\n", &slice1) fmt.Println("======================================") // 擴容都是在原有的地址上進行追加 也就會導致擴容前後記憶體地址是不變的 slice2 := []int{10,20,30,40} newslice := append(slice2, 50) fmt.Printf("slice2 address=%p, %v\n", &slice2, slice2) fmt.Printf("newslice address=%p, %v\n", &newslice, newslice) newslice[1] += 100 fmt.Printf("After update,slice2 address=%p, %v\n", &slice2, slice2) fmt.Printf("After update,newslice address=%p, %v\n", &newslice, newslice) } // // 空切片: make([]int, 0) / []int{} 代表建立容量為0的slice,故而其會指向一塊記憶體地址,不過該記憶體地址沒有分配任何空間的 // nil切片:地址為nil; // 空切片和nil切片是不相同的;不過兩者對呼叫內建函式 append,len 和 cap 的效果都是一樣的。 // // 當slice本身的容量已滿的情況下 涉及到了擴容,只要會涉及如下內容 // 1、擴容策略 //需要擴容的大小超過了原有大小的2*old_slice,則直接使用申請的大小 //若是申請大小<=2*old_slice: //當old_slice < 1024 按照2*old_slice進行擴容 //當old_slice >= 1024 按照1.25 * old_slice進行擴容 //根據提供的擴容策略來計算新切片的容量和大小: //首先使用mallocgc在old後面進行擴充容量 //其次見old內容copy到p地址處 //最後得到新的切片容量地址 = P地址 + 新增擴容大小;初始化(capmem-newlenmen)間的記憶體地址初始化 // 2、在原有地址上進行追加(擴容的地址和原有的地址保持不變) //當申請的slice型別與old相同並且kind屬於非pointer 則在old基礎上進行追加mallogc //反之則重新申請新的底層陣列給新的切片 //首先重新申請新的陣列給到新的切片:重新申請capmem的記憶體地址,並進行初始化 //其次進行寫鎖檢查:當寫鎖開啟則通過迴圈copy老切片的內容;若是為開啟寫鎖則只能將lenmen大小的位元組從old陣列copy到p的地址處 // 3、返回申請的slice大小 //