在日常开发中,我们经常会遇到高并发的业务场景,比如钱包系统的转账。如何保证并发情况下的数据一致性,是 Go 工程师必须掌握的技能之一。今天我用一个简单的钱包转账例子,带大家看看 Go 中数据竞争是怎么发生的,以及如何用 sync.Mutex
和 sync.RWMutex
来解决。
1. 从问题开始:并发转账的数据错乱
假设我们实现了一个简单的钱包结构体 Wallet
,并提供了转账方法:
type Wallet struct {
Balance int
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
if w.Balance >= amount {
w.Balance -= amount
target.Balance += amount
}
}
在 main
函数里,我们让两个用户账户各自初始余额 1000,然后模拟 1000 个并发协程,每次从 userA
转账 1 元给 userB
:
func main() {
userA := &Wallet{Balance: 1000}
userB := &Wallet{Balance: 1000}
var wg sync.WaitGroup
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
userA.Transfer(1, userB)
}()
}
wg.Wait()
fmt.Printf("UserA余额: %d\n", userA.Balance)
fmt.Printf("UserB余额: %d\n", userB.Balance)
}
预期结果:
- A 转出 1000 元后余额为 0
- B 收入 1000 元后余额为 2000
但实际运行多次后,输出往往并不一致,比如:
UserA余额: 312
UserB余额: 1688
为什么会这样?
2. 问题的根源:数据竞争
原因在于:
- 多个 goroutine 同时在修改
userA.Balance
和userB.Balance
w.Balance -= amount
和target.Balance += amount
并不是原子操作- 导致读写交叉时数据覆盖或丢失,形成 race condition(竞态条件)
我们可以用 Go 内置的 -race
工具检测:
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:13 +0x78
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 8:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 8 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x8c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 24:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114048 by goroutine 14:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xb8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 24:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114038 by goroutine 18:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 30:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 18 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 30 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114048 by goroutine 32:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 25:
main.(*Wallet).Transfer()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
main.main.func1()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 32 (running) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 25 (finished) created at:
main.main()
/Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
UserA余额: 92
UserB余额: 1769
Found 5 data race(s)
exit status 66
输出会提示存在 DATA RACE,验证了我们的推断。
3. 解决方案一:加锁 —— sync.Mutex
最直接的办法就是用 互斥锁 (sync.Mutex
) 保护共享数据,确保在同一时间只有一个 goroutine 能执行转账操作。
type Wallet struct {
Balance int
mu sync.Mutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
w.mu.Lock()
defer w.mu.Unlock()
if w.Balance >= amount {
w.Balance -= amount
target.mu.Lock()
target.Balance += amount
target.mu.Unlock()
}
}
这样,每次转账都必须拿到锁,保证操作的完整性。
再次运行程序,结果稳定为:
UserA余额: 0
UserB余额: 2000
问题解决。
4. 优化方案:读写锁 —— sync.RWMutex
但是,如果钱包的读操作(比如查询余额)非常频繁,而写操作相对较少,这时使用 sync.Mutex
会导致读操作也被阻塞,降低整体性能。
Go 提供了 读写锁 sync.RWMutex
:
- 多个读可以并发执行(
RLock
/RUnlock
) - 写操作依然互斥(
Lock
/Unlock
)
修改代码如下:
type Wallet struct {
Balance int
mu sync.RWMutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
w.mu.Lock()
defer w.mu.Unlock()
if w.Balance >= amount {
w.Balance -= amount
target.mu.Lock()
target.Balance += amount
target.mu.Unlock()
}
}
func (w *Wallet) GetBalance() int {
w.mu.RLock()
defer w.mu.RUnlock()
return w.Balance
}
这样:
- 转账写操作依然是串行的
- 查询余额可以并发进行,不会相互阻塞
- 在高并发场景下,系统的整体性能会显著提升
5. 总结
通过这个钱包转账的例子,我们可以总结 Go 并发下的几个关键点:
- 数据竞争是并发编程的常见问题,务必通过
-race
检测工具来排查 - 互斥锁 Mutex 是解决写并发最直接可靠的方法
- 如果读多写少,可以选择 读写锁 RWMutex,提升读取并发能力
- 在实际系统中,还要结合业务逻辑,比如数据库事务、分布式锁,保证数据一致性
Go 并发编程的核心是对共享资源的正确管理,合理使用 Mutex 和 RWMutex,才能写出既安全又高效的代码。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—