在 Go 语言的日常开发中,slice
基本上就是“家常便饭”。可问题来了:当你在多线程(goroutine)下对同一个 slice
做 append
时,它真的安全吗?
短答案:不安全。
长答案:且听我慢慢道来。
slice
在 Go 底层,其实就是这么一个小 struct:
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 当前长度
cap int // 容量
}
所以 slice
就像一个“搬家租户”:
append
的过程,其实是这样的:
1)看看房子里(cap)还能不能再塞一个人。
len++
,放进去。2)换大房子的时候:
这就像你宿舍太挤了,宿管阿姨给你换了个大寝室,结果你舍友还在旧宿舍搬床,另一个人已经躺到新宿舍的床上了——尴尬不?
多个 goroutine 同时 append
,都会修改 len
。
举个例子:
len = 5
,打算写 s[5]
。len = 5
,它也写 s[5]
。结果?两个人挤同一张床,最后还只算一个人入住。
数据覆盖,len 错乱。
事情更刺激了:
结果:
package main
import (
"fmt"
"sync"
)
func main() {
s := []int{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(v int) {
defer wg.Done()
s = append(s, v) // 并发 append
}(i)
}
wg.Wait()
fmt.Println("slice 长度:", len(s)) // < 1000,不确定
}
运行时:
-race
参数跑,更能看到一堆红色警告。var mu sync.Mutex
s := []int{}
mu.Lock()
s = append(s, v)
mu.Unlock()
简单粗暴,保证一个一个来,绝对安全。
ch := make(chan int, 1000)
s := []int{}
go func() {
for v := range ch {
s = append(s, v)
}
}()
ch <- 1
ch <- 2
// ...
大家排队交钥匙,唯一的搬家工负责 append
,高效又优雅。
var idx int64
s := make([]int, 1000)
pos := atomic.AddInt64(&idx, 1) - 1
s[pos] = v
适合已知人数的场景,大家直接去各自的房间,互不干扰。
append
本质上修改了 len
和底层数组,不是并发安全的。
扩容机制让情况更复杂:有的人在旧房子,有的人在新房子。
想要并发安全,可以选择:
sync.Mutex
—— 保姆式保护。channel
—— Go 风格排队。atomic
+ 预分配 —— 高性能方案。┌─────────────┐
│ slice │
│ len = 3 │
│ cap = 4 │
│ array ---> [0][1][2][_]
└─────────────┘
goroutine1 append: len = 4, array[3] = X
goroutine2 append: len = 4, array[3] = Y
结果:X 或 Y 被覆盖
再来扩容场景:
旧数组: [0][1][2]
↑ goroutine2 还在写
新数组: [0][1][2][_][_][_]
↑ goroutine1 已经搬过去
最终:数据丢失,互相看不见。
✍️ 写到这里,你应该能理解:
Go 的 slice 扩容和 append,就像一场宿舍搬家,大家要么排队、有序,要么闹成一锅粥。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。