在Go语言里,数组是固定长度的值类型,切片是对底层数组的引用。当多个goroutine并发读写同一个数组或切片指向的底层数组时,如果没有同步控制,就会产生数据竞争,导致程序行为异常甚至崩溃。理解切片与三索引切片的特性,是编写安全并发代码的重要基础。
为什么并发访问数组不安全
Go中的数组在赋值时会复制整个结构,但切片持有指针、长度和容量,多个切片可能指向同一底层数组。看下面示例,两个goroutine同时修改同一底层数组的元素:
package main
import (
"fmt"
"sync"
)
func main() {
arr := [4]int{0, 0, 0, 0}
s := arr[:]
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
s[0]++ // 并发修改底层数组
}
}()
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
s[1]++
}
}()
wg.Wait()
fmt.Println(arr)
}
上述代码虽然修改的是不同索引,但由于共享底层数组且缺乏同步,在真实复杂逻辑中极易出现竞争。更安全的做法是让每个goroutine操作独立的数据区域。
用切片隔离并发访问区域
切片可以只引用底层数组的一部分。通过提前划分区间,把不同区间交给不同goroutine,可以减少冲突。例如:
package main
import (
"fmt"
"sync"
)
func main() {
arr := [4]int{1, 2, 3, 4}
// 把数组拆成两个独立切片区间
left := arr[0:2]
right := arr[2:4]
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := range left {
left[i] *= 2
}
}()
go func() {
defer wg.Done()
for i := range right {
right[i] *= 2
}
}()
wg.Wait()
fmt.Println(arr)
}
这里left和right虽然共享arr,但访问的索引不重叠,逻辑上隔离了并发写入范围。
三索引切片限制容量防止越界扩展
Go支持三索引切片语法 s[low:high:max],其中max控制容量。用它创建子切片,可以避免子切片在append时意外覆盖其他goroutine的数据。
package main
import "fmt"
func main() {
base := make([]int, 6, 6)
for i := range base {
base[i] = i
}
// 三索引切片,容量被限制为3
sub := base[0:3:3]
fmt.Println(len(sub), cap(sub))
// 若执行 append(sub, 99) 将分配新底层数组,不影响base后续区域
}
在并发场景中,把三索引切片交给处理单元,能防止其通过append侵入他人的底层数组空间,从而提升安全边界。
实践建议
- 尽量让每个goroutine持有独立的切片或数组副本,减少共享。
- 必须共享时,使用三索引切片明确容量边界。
- 若无法避免交叉读写,应使用sync.Mutex或sync/atomic进行保护。
合理运用切片与三索引切片,可以在不少场景下替代锁,让Go并发程序既安全又高效。