Dig101: dig more, simplified more and know more
Slice做爲go經常使用的數據類型,在平常編碼中很是常見。
相對於數組的定長不可變,slice使用起來就靈活了許多。android
首先咱們看下源碼中slice結構的定義git
// src/runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }
slice數據結構如上,Data指向底層引用的數組內存地址, len是已用長度,cap是總容量。
爲驗證如上所述,咱們嘗試聲明一個slice a,獲取 a的sliceHeader頭信息,並用%p
獲取&a, sh, a, a[0]
的地址
看看他們的地址是否相同。github
a := make([]int, 1, 3) //reflect.SliceHeader 爲 slice運行時數據結構 sh := (*reflect.SliceHeader)(unsafe.Pointer(&a)) fmt.Printf("slice header: %#v\naddress of a: %p &a[0]: %p | &a: %p sh:%p ", sh, a, &a[0],&a, sh) //slice header: &reflect.SliceHeader{Data:0xc000018260, Len:1, Cap:3} //address of a: 0xc000018260 &a[0]: 0xc000018260 | &a: 0xc00000c080 sh:0xc00000c080
結果發現a和&a[0]
地址相同。 這個好理解,切片指向地址即對應底層引用數組首個元素地址
而&a和sh及sh.Data
指向地址相同。這個是由於這三個地址是指slice自身地址。
這裏【slice自身地址不一樣於slice指向的底層數據結構地址】, 清楚這一點對於後邊的一些問題會更容易判斷。golang
<!--more-->segmentfault
這裏做爲一個小插曲,咱們看下當fmt.Printf("%p",a)
時發生了什麼
內部調用鏈 fmtPointer -> Value.Pointer
而後根據Pointer方法對應slice的註釋以下數組
// If v's Kind is Slice, the returned pointer is to the first // element of the slice. If the slice is nil the returned value // is 0. If the slice is empty but non-nil the return value is non-zero.
發現沒,正是咱們上邊說的,slice不爲空時,返回了第一個元素的地址
有點迷惑性是否是,但其實做爲使用slice的咱們,更關心的是底層指向的數據不是麼。安全
再一點就是,基於go中全部賦值和參數傳遞都是值傳遞,對於大數組而言,拷貝一個指向他的slice就高效多了
上一篇Go之for-range排坑指南 有過描述, 詳見 0x03 對大數組這樣遍歷有啥問題?session
總結下, slice是一個有底層數組引用的結構裏,有長度,有容量。數據結構
就這麼簡單? 不,光這樣還不足以讓它比數組更好用。
slice還支持很是方便的切片操做和append時自動擴容,這讓他更加flexibleapp
答案是【只能和nil比較】
s := make([]int, 5) a := s println(s == a) //invalid operation: s == a (slice can only be compared to nil)
這個也其實好理解,當你比較兩個slice,你是想比較他們自身呢?(必然不一樣啊,由於有值拷貝)
仍是比較他們底層的數組?(那長度和容量也一塊兒比較麼)
確實沒有什麼意義去作兩個slice的比較。
slice經過三段參數來操做:x[from:len:cap]
即對x從from
索引位置開始,截取len
長度,cap
大小的新切片返回
可是len和cap不能大於x原來的len和cap
三個參數均可省略,默認爲x[0:len(x):cap(x)]
切片操做一樣適用於array
以下都是經過src[:]
常規對切片(指向的底層數組)或數組的引用
s:=make([]int,5) x:=s[:] arr:=[5]int{} y:=arr[:]
配合copy和append,slice的操做還有不少,官方wikiSlice Tricks 有更豐富的例子
好比更通用的拷貝b = append(a[:0:0], a...)
好比cut或delete時增長對不使用指針的nil標記釋放(防止內存泄露)
//Cut copy(a[i:], a[j:]) for k, n := len(a)-j+i, len(a); k < n; k++ { a[k] = nil // or the zero value of T } a = a[:len(a)-j+i] //Delete if i < len(a)-1 { copy(a[i:], a[i+1:]) } a[len(a)-1] = nil // or the zero value of T a = a[:len(a)-1]
不熟悉的話,建議好好練習一下去感覺
總的來講,append時會按需自動擴容
以下代碼所示,容量不夠時觸發了擴容從新開闢底層數組,x 和 s 底層指向的數組已不是同一個
s := make([]int, 5) x := append(s, 1) fmt.Printf("x dataPtr: %p len: %d cap: %d\ns dataPtr: %p len: %d cap: %d", x, len(x), cap(x), s, len(s), cap(s)) // x dataPtr: 0xc000094000 len: 6 cap: 10 // s dataPtr: 0xc000092030 len: 5 cap: 5
具體查閱源碼,你會發現編譯時將append分爲三類並優化
除按需擴容外
x = append(y, make([]T, y)...)
使用memClr提升初始化效率
x = append(l1, l2...)
或者 x = append(slice, string)
直接複製l2
x = append(src, a, b, c)
肯定待append數目下,直接作賦值優化
具體編譯優化以下
註釋有簡化,詳見internal/gc/walk.go: append
switch { case isAppendOfMake(r): // x = append(y, make([]T, y)...) will rewrite to // s := l1 // n := len(s) + l2 // if uint(n) > uint(cap(s)) { // s = growslice(T, s, n) // } // s = s[:n] // lptr := &l1[0] // sptr := &s[0] // if lptr == sptr || !hasPointers(T) { // // growslice did not clear the whole underlying array // (or did not get called) // hp := &s[len(l1)] // hn := l2 * sizeof(T) // memclr(hp, hn) // } //使用memClr提升初始化效率 r = extendslice(r, init) case r.IsDDD(): // DDD is ... syntax // x = append(l1, l2...) will rewrite to // s := l1 // n := len(s) + len(l2) // if uint(n) > uint(cap(s)) { // s = growslice(s, n) // } // s = s[:n] // memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T)) //直接複製l2 r = appendslice(r, init) // also works for append(slice, string). default: // x = append(src, a, b, c) will rewrite to // s := src // const argc = len(args) - 1 // if cap(s) - len(s) < argc { // s = growslice(s, len(s)+argc) // } // n := len(s) // s = s[:n+argc] // s[n] = a // s[n+1] = b // ... //肯定待append數目下,直接作賦值優化 r = walkappend(r, init, n) }
這裏關於append實現有幾點能夠提下
答案是【總的來講是至少返回要求的長度n最大則爲翻倍】
具體狀況是:
mallocgc
內存分配大小適配來肯定len. (n-2n之間)擴容留出最多一倍的餘量,主要仍是爲了減小可能的擴容頻率。
mallocgc內存適配實際是go內存管理作了內存分配的優化, 固然內部也有內存對齊的考慮。
雨痕Go學習筆記第四章內存分配,對這一塊有很詳盡的分析,值得一讀。
至於爲啥要內存對齊能夠參見Golang 是否有必要內存對齊?,一篇不錯的文章。
uint
的做用是啥?//n爲目標slice總長度,類型int,cap(s)類型也爲int if uint(n) > uint(cap(s)) s = growslice(T, s, n) }
答案是【爲了不溢出的擴容】
int有正負,最大值math.MaxInt64 = 1<<63 - 1
uint無負數最大值math.MaxUint64 = 1<<64 - 1
uint正值是int正值範圍的兩倍,int溢出了變爲負數,uint(n)
則必大於原s的cap,條件成立
到growslice內部,對於負值的n會panic,以此避免了溢出的擴容
答案是【這個取決於待清零的內存是否已經初始化爲type-safe(類型安全)狀態,及類型是否包含指針】
具體來看,memclrNoHeapPointers
使用場景是
其餘場景就是typedmemclr
, 並且若是用於清零的Type(類型)包含指針,他會多一步WriteBarrier(寫屏障),用於爲GC(垃圾回收)運行時標記對象的內存修改,減小STW(stop the world)
因此memclrNoHeapPointers
第一個使用場景爲啥不含指針就不用解釋了。
想了解更多能夠看看zero-initialization-versus-zeroing
以及相關源碼的註釋memclrNoHeapPointers和typedmemclr
本文代碼見 NewbMiao/Dig101-Go
文章首發公衆號: newbmiao (歡迎關注,獲取及時更新內容)推薦閱讀:Dig101-Go系列