在Golang的并发编程场景中,很多自定义的数据结构需要支持多goroutine同时读写,这类结构的并发安全性直接决定了程序的稳定性。如果数据结构没有做好并发控制,很容易出现数据竞争、数据丢失或者程序崩溃的问题,因此需要通过合理的测试手段验证其并发安全性。

基础并发测试:多goroutine同时操作验证
最基础的测试方式是启动多个goroutine同时对目标数据结构执行读写操作,最后检查数据的最终状态是否符合预期。这种方式可以快速发现明显的并发问题,比如计数错误、元素丢失等。
以一个简单的并发安全计数器为例,先定义一个具备并发安全特性的计数器结构:
package concurrent_test
import (
"sync"
)
// SafeCounter 并发安全计数器
type SafeCounter struct {
mu sync.Mutex
value int
}
// Incr 计数器加1
func (c *SafeCounter) Incr() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
// Get 获取当前计数器值
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
接下来编写测试代码,启动100个goroutine同时调用Incr方法,最终预期计数器的值为100:
package concurrent_test
import (
"testing"
)
func TestSafeCounter_Basic(t *testing.T) {
counter := &SafeCounter{}
goroutineNum := 100
var wg sync.WaitGroup
wg.Add(goroutineNum)
for i := 0; i < goroutineNum; i++ {
go func() {
defer wg.Done()
counter.Incr()
}()
}
wg.Wait()
if counter.Get() != goroutineNum {
t.Errorf("预期计数器值为 %d,实际为 %d", goroutineNum, counter.Get())
}
}
如果计数器没有加锁,多次运行这个测试大概率会出现实际值小于100的情况,说明存在并发安全问题。
使用race检测器排查数据竞争
Golang内置了race检测器,可以在测试过程中自动检测是否存在数据竞争问题,比基础测试更能发现隐藏的并发隐患。使用方式很简单,只需要在执行测试时加上-race参数即可。
我们修改上面的计数器,去掉互斥锁模拟并发不安全的场景:
// UnsafeCounter 并发不安全计数器
type UnsafeCounter struct {
value int
}
func (c *UnsafeCounter) Incr() {
c.value++
}
func (c *UnsafeCounter) Get() int {
return c.value
}
编写对应的测试代码:
func TestUnsafeCounter_Race(t *testing.T) {
counter := &UnsafeCounter{}
goroutineNum := 100
var wg sync.WaitGroup
wg.Add(goroutineNum)
for i := 0; i < goroutineNum; i++ {
go func() {
defer wg.Done()
counter.Incr()
}()
}
wg.Wait()
}
执行测试命令go test -race,如果存在数据竞争,race检测器会输出详细的竞争发生位置,包括两个竞争操作的goroutine栈信息,帮助开发者快速定位问题。
高并发压力测试
基础测试的场景并发度较低,有些并发问题只有在高并发、长时间运行的场景下才会暴露。这时候可以编写压力测试,增加并发goroutine数量和操作次数,延长测试运行时间。
还是以并发安全计数器为例,编写压力测试代码:
func BenchmarkSafeCounter_Stress(b *testing.B) {
counter := &SafeCounter{}
goroutineNum := 1000
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
wg.Add(goroutineNum)
for j := 0; j < goroutineNum; j++ {
go func() {
defer wg.Done()
// 每个goroutine执行100次加操作
for k := 0; k < 100; k++ {
counter.Incr()
}
}()
}
wg.Wait()
}
// 验证总操作次数是否正确
expected := goroutineNum * 100 * b.N
if counter.Get() != expected {
b.Errorf("预期计数器值为 %d,实际为 %d", expected, counter.Get())
}
}
执行go test -bench=. -benchtime=10s命令运行压力测试,长时间的高并发操作可以更充分地验证数据结构的稳定性,发现偶现的并发问题。
不同测试方式的适用场景
三种测试方式各有侧重,实际测试中可以根据需求组合使用:
- 基础并发测试适合快速验证数据结构的逻辑是否符合预期,排查明显的功能问题
- race检测器适合在开发阶段持续使用,自动发现潜在的数据竞争问题,成本低效果好
- 压力测试适合上线前的验证,模拟生产环境的高并发场景,排查极端情况下的并发隐患
在测试并发安全的数据结构时,还需要注意覆盖不同的操作组合,比如同时有读有写的场景、不同操作的交叉执行场景,才能更全面地验证并发安全性。