本篇整理的内容是基于go 1.18 版本。
切片底层结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
在创建slice时,容量和长度是相等的。例如,创建一个长度为5的切片,那么它的容量也是5。
在调用append函数时,如果新的长度 > 容量,会发生扩容,并改变底层数组。
扩容机制:
- 如果新申请长度大于 2 倍的旧容量,那么最终容量就是新的切片长度 。
- 如果旧容量小于256,那么最终容量就是旧容量的两倍 。
- 最终容量不一定是多少,因为涉及到移位。(大概是之前容量的1.25倍)
- 最终容量计算值溢出,则最终容量就是新的切片长度。
```python
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
} else {newcap = cap
}const threshold = 256
if 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
}
}
```python
var overflow bool
var 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) > maxAlloc
newcap = int(capmem)
case et.size == goarch.PtrSize:
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if 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) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, 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)2
fmt.Println("2", a)
// slicePtr2 函数实现在下面
slicePtr2(&a)
fmt.Println("3", a)
slicePtr3(&a)
}
func slice(s []int) {
s[0] = 10
s = 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 := *s
b[0] = 30
b = append(b, 30)
b[1] = 30
}
func slicePtr3(s *[]int) {
b := *s
b = 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.WaitGroup
var mu sync.Mutex
for i := 0; i < 100; i++ {
i := i
wg.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.WaitGroup
go func() {
for i := 0; i < 100; i++ {
i := i
wgOne.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")
}