本篇整理的内容是基于go 1.18 版本。
切片底层结构:
type slice struct {array unsafe.Pointerlen intcap int}
在创建slice时,容量和长度是相等的。例如,创建一个长度为5的切片,那么它的容量也是5。
在调用append函数时,如果新的长度 > 容量,会发生扩容,并改变底层数组。
扩容机制:
- 如果新申请长度大于 2 倍的旧容量,那么最终容量就是新的切片长度 。
- 如果旧容量小于256,那么最终容量就是旧容量的两倍 。
- 最终容量不一定是多少,因为涉及到移位。(大概是之前容量的1.25倍)
- 最终容量计算值溢出,则最终容量就是新的切片长度。
```python
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
} else {newcap = cap
}const threshold = 256if old.cap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}
```pythonvar overflow boolvar lenmem, newlenmem, capmem uintptr// Specialize for common values of et.size.// For 1 we don't need any division/multiplication.// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.// For powers of 2, use a variable shift.switch {case et.size == 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == goarch.PtrSize:lenmem = uintptr(old.len) * goarch.PtrSizenewlenmem = uintptr(cap) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap) * goarch.PtrSize)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif goarch.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.Ctz64(uint64(et.size))) & 63} else {shift = uintptr(sys.Ctz32(uint32(et.size))) & 31}lenmem = uintptr(old.len) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}
我们只需知晓扩容的原理即可,不必纠结扩容后具体的数字。
案例:
func main() {a := []int{1, 2, 3}// silce 函数实现在下面slice(a)fmt.Println("1", a)// slicePtr1 函数实现在下面slicePtr1(&a)2fmt.Println("2", a)// slicePtr2 函数实现在下面slicePtr2(&a)fmt.Println("3", a)slicePtr3(&a)}func slice(s []int) {s[0] = 10s = append(s, 10)s[1] = 10}func slicePtr1(s *[]int) {(*s)[0] = 20*s = append(*s, 20)(*s)[1] = 20}func slicePtr2(s *[]int) {b := *sb[0] = 30b = append(b, 30)b[1] = 30}func slicePtr3(s *[]int) {b := *sb = append(b, 40)fmt.Println("4", b)*s = append(*s, 50)fmt.Println("5", b)}输出结果1:[10,2,3] //append前,共用同一底层数组;append后,发生扩容,不共用底层数组。2:[20,20,3,20] //传入指针,因此函数内修改会影响到外部。3:[30,30,3,20] //整个过程没有发生扩容,因此共用底层数组。4:[30,30,3,20,40]5:[30,30,3,20,50] //会将a底层数组的第五个元素覆盖为50,又共用同一底层数组。
线程安全切片:
切片不是线程安全的,如果我们想要线程安全的切片,可加锁或使用channel。
list := make([]int, 0)var wg sync.WaitGroupvar mu sync.Mutexfor i := 0; i < 100; i++ {i := iwg.Add(1)go func() {defer wg.Done()mu.Lock()list = append(list, i)mu.Unlock()}()}wg.Wait()
list := make([]int, 0)ch := make(chan int, 100)stopCh := make(chan int)var wgOne sync.WaitGroupgo func() {for i := 0; i < 100; i++ {i := iwgOne.Add(1)go func() {defer wgOne.Done()ch <- i}()}wgOne.Wait()close(ch)}()go func() {for i := range ch {list = append(list, i)}stopCh <- 1}()select {case <-stopCh:fmt.Println("接收到了信号,准备关闭goroutine")}
