go int 最大值_Dig101 - Go之灵活的slice
Dig101: dig more, simplified more and know more
Slice作為go常用的數據類型,在日常編碼中非常常見。 相對于數組的定長不可變,slice使用起來就靈活了許多。
0x01 slice 到底是什么?
首先我們看下源碼中slice結構的定義
// src/runtime/slice.go type slice struct {array unsafe.Pointerlen intcap int }slice數據結構如上,Data指向底層引用的數組內存地址, len是已用長度,cap是總容量。 為驗證如上所述,我們嘗試聲明一個slice a,獲取 a的sliceHeader頭信息,并用%p獲取&a, sh, a, a[0]的地址 看看他們的地址是否相同。
a := make([]int, 1, 3) //reflect.SliceHeader 為 slice運行時數據結構 sh := (*reflect.SliceHeader)(unsafe.Pointer(&a)) fmt.Printf("slice header: %#vnaddress 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指向的底層數據結構地址】, 清楚這一點對于后邊的一些問題會更容易判斷。
這里作為一個小插曲,我們看下當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 對大數組這樣遍歷有啥問題?
總結下, slice是一個有底層數組引用的結構里,有長度,有容量。
就這么簡單? 不,光這樣還不足以讓它比數組更好用。 slice還支持非常方便的切片操作和append時自動擴容,這讓他更加flexible
0x02 slice能比較么?
答案是【只能和nil比較】
s := make([]int, 5) a := s println(s == a) //invalid operation: s == a (slice can only be compared to nil)這個也其實好理解,當你比較兩個slice,你是想比較他們自身呢?(必然不同啊,因為有值拷貝) 還是比較他們底層的數組?(那長度和容量也一起比較么) 確實沒有什么意義去做兩個slice的比較。
0x03 花樣的切片操作
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]不熟悉的話,建議好好練習一下去感受
0x04 append 時發生了什么?
總的來說,append時會按需自動擴容 - 容量足夠,無擴容則直接拷貝待 append 的數據到原 slice 底層指向的數組 之后(原slice的len之后),并返回指向該數組首地址的新slice(len改變) - 容量不夠,有擴容則拷貝原有 slice 所指向部分數據到新開辟的數組,并對待 append 的數據附加到其后,并返回新數組首地址的新slice?(底層數組,len,cap均改變)
如下代碼所示,容量不夠時觸發了擴容重新開辟底層數組,x 和 s 底層指向的數組已不是同一個
s := make([]int, 5) x := append(s, 1) fmt.Printf("x dataPtr: %p len: %d cap: %dns 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: 50x05 append內部優化
具體查閱源碼,你會發現編譯時將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))//直接復制l2r = 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最大則為翻倍】 具體情況是: - len<1024時2倍擴容 - 大于且未溢出時1.25倍擴容 - 溢出則直接按申請大小擴容 - 最后按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,以此避免了溢出的擴容
內存清零初始化: memclrNoHeapPointers vs typedmemclr?
答案是【這個取決于待清零的內存是否已經初始化為type-safe(類型安全)狀態,及類型是否包含指針】
具體來看,memclrNoHeapPointers使用場景是 - 帶清零內存是初始化過的,且不含指針 - 帶清零內存未初始化過的,里邊內容是“垃圾值”(即非type-safe),需要初始化并清零
其他場景就是typedmemclr, 而且如果用于清零的Type(類型)包含指針,他會多一步WriteBarrier(寫屏障),用于為GC(垃圾回收)運行時標記對象的內存修改,減少STW(stop the world)
所以memclrNoHeapPointers第一個使用場景為啥不含指針就不用解釋了。
想了解更多可以看看zero-initialization-versus-zeroing 以及相關源碼的注釋memclrNoHeapPointers和typedmemclr
本文代碼見 NewbMiao/Dig101-Go歡迎關注我,不定期深挖技術總結
以上是生活随笔為你收集整理的go int 最大值_Dig101 - Go之灵活的slice的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iframe放大显示,如何让iFrame
- 下一篇: 信用卡纠纷被起诉了怎么办