首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go语言中slice的扩容与并发安全

Go语言中slice的扩容与并发安全

原创
作者头像
闫同学
发布2025-08-27 23:25:25
发布2025-08-27 23:25:25
1060
举报
文章被收录于专栏:Coding实践Coding实践

在 Go 语言的日常开发中,slice 基本上就是“家常便饭”。可问题来了:当你在多线程(goroutine)下对同一个 sliceappend 时,它真的安全吗?

短答案:不安全

长答案:且听我慢慢道来。

1. Slice 的真面目

slice 在 Go 底层,其实就是这么一个小 struct:

代码语言:go
复制
type slice struct {
    array unsafe.Pointer // 指向底层数组
    len   int            // 当前长度
    cap   int            // 容量
}
  • array:真正存放数据的数组指针。
  • len:当前有多少元素。
  • cap:底层数组最多能装多少。

所以 slice 就像一个“搬家租户”:

  • len:家里住了几口人。
  • cap:房子最多能住几口人。
  • array:房子地址。

2. append 并不简单

append 的过程,其实是这样的:

1)看看房子里(cap)还能不能再塞一个人。

  • 能塞?那就 len++,放进去。
  • 不能塞?那就换大房子!(扩容)

2)换大房子的时候:

  • 买一个更大的新房子。
  • 把旧房子的家具(老数据)全搬过去。
  • 把新住户塞进新房子。
  • 最后更新租户手里的“房产证”(slice 头部)。

这就像你宿舍太挤了,宿管阿姨给你换了个大寝室,结果你舍友还在旧宿舍搬床,另一个人已经躺到新宿舍的床上了——尴尬不?

3. 为什么 append 并发不安全?

情况 A:没扩容

多个 goroutine 同时 append,都会修改 len

举个例子:

  • goroutine1 读到 len = 5,打算写 s[5]
  • goroutine2 也读到 len = 5,它也写 s[5]

结果?两个人挤同一张床,最后还只算一个人入住。

数据覆盖,len 错乱

情况 B:触发扩容

事情更刺激了:

  • goroutine1 发现房子不够大,买了个大房子(新数组),家具都搬过去了。
  • goroutine2 还在旧房子刷墙(往旧数组里写数据)。

结果:

  • 有的人住进了新宿舍。
  • 有的人还留在旧宿舍。
  • 一点也不温馨,数据直接丢失

4. 现场演示:不安全的 append

代码语言:go
复制
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,不确定
}

运行时:

  • 有时长度 < 1000。
  • 有时直接 panic。
  • 加上 -race 参数跑,更能看到一堆红色警告。

5. 如何解决这场“宿舍闹剧”?

方案一:加锁(宿管阿姨看门)

代码语言:go
复制
var mu sync.Mutex
s := []int{}

mu.Lock()
s = append(s, v)
mu.Unlock()

简单粗暴,保证一个一个来,绝对安全。

方案二:用 channel 串行化(排队进宿舍)

代码语言:go
复制
ch := make(chan int, 1000)
s := []int{}

go func() {
    for v := range ch {
        s = append(s, v)
    }
}()

ch <- 1
ch <- 2
// ...

大家排队交钥匙,唯一的搬家工负责 append,高效又优雅。

方案三:atomic + 预分配(提前买好大别墅)

代码语言:go
复制
var idx int64
s := make([]int, 1000)

pos := atomic.AddInt64(&idx, 1) - 1
s[pos] = v

适合已知人数的场景,大家直接去各自的房间,互不干扰。

6. 总结

append 本质上修改了 len 和底层数组,不是并发安全的

扩容机制让情况更复杂:有的人在旧房子,有的人在新房子。

想要并发安全,可以选择:

  1. sync.Mutex —— 保姆式保护。
  2. channel —— Go 风格排队。
  3. atomic + 预分配 —— 高性能方案。

7. 小总结

代码语言:shell
复制
┌─────────────┐
│  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 被覆盖

再来扩容场景:

代码语言:shell
复制
旧数组: [0][1][2]
          ↑ goroutine2 还在写

新数组: [0][1][2][_][_][_]
                   ↑ goroutine1 已经搬过去

最终:数据丢失,互相看不见。

✍️ 写到这里,你应该能理解:

Go 的 slice 扩容和 append,就像一场宿舍搬家,大家要么排队、有序,要么闹成一锅粥。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Slice 的真面目
  • 2. append 并不简单
  • 3. 为什么 append 并发不安全?
    • 情况 A:没扩容
    • 情况 B:触发扩容
  • 4. 现场演示:不安全的 append
  • 5. 如何解决这场“宿舍闹剧”?
    • 方案一:加锁(宿管阿姨看门)
    • 方案二:用 channel 串行化(排队进宿舍)
    • 方案三:atomic + 预分配(提前买好大别墅)
  • 6. 总结
  • 7. 小总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档