
本文详解如何用 go 的 goroutine 和 channel 模拟双向银行账户转账,并通过 sync.mutex 和 sync.waitgroup 解决竞态条件与 goroutine 同步问题,确保余额守恒。
本文详解如何用 go 的 goroutine 和 channel 模拟双向银行账户转账,并通过 sync.mutex 和 sync.waitgroup 解决竞态条件与 goroutine 同步问题,确保余额守恒。
在 Go 中实现并发安全的银行转账系统,核心挑战不在“如何启动 goroutine”,而在于正确协调状态访问与执行时序。原始代码看似逻辑清晰:每个 Transfer 启动两个监听 goroutine(源账户接收负额、目标账户接收正额),并通过 channel 逐单位发送微支付。但运行结果中反复出现 ±1 的余额偏差,说明存在未被完全覆盖的并发缺陷。
? 根本问题分析
原始代码存在两类并发隐患:
-
数据竞争(Data Race):
Account.Balance被多个 goroutine(不同Listen函数)无保护地读写。即使为Listen()加了Mutex,若最终余额统计(fooEndBalance += ...)未同步访问,仍会读到中间态值; -
时序竞态(Timing Race):主 goroutine 在从
statuschannel 接收完 50 条完成消息后,立即关闭 channel 并开始计算最终余额,但此时部分Listengoroutine 可能尚未处理完所有 channel 数据(尤其是close(channel)后,range循环虽会退出,但defer或调度延迟可能导致最后几笔加法未落库)。
✅ 提示:
go run -race main.go可精准定位所有竞态点——它不仅会报Balance读写冲突,还会标记status关闭后仍尝试发送的潜在 panic。
✅ 正确实现:Mutex + WaitGroup + Channel 守护
以下是修复后的关键结构与逻辑(精简核心):
type Account struct {
sync.Mutex
Owner string
Number int
Balance int
}
func (a *Account) Add(amount int) {
a.Lock()
a.Balance += amount
a.Unlock()
}
func (a *Account) GetBalance() int {
a.Lock()
b := a.Balance
a.Unlock()
return b
}
func (t Transfer) Execute(done chan<h3>? 主函数同步改造(关键!)</h3><pre class="brush:php;toolbar:false;">func main() {
const ACCOUNTS = 25
const TRANSFERS = ACCOUNTS * 2
const AMOUNT = 5000
const BALANCE = 9000
// 初始化账户(略)
fooAccounts := make([]*Account, ACCOUNTS)
barAccounts := make([]*Account, ACCOUNTS)
for i := 0; i <h3>? 关键要点总结</h3>
-
Mutex 必须覆盖所有 Balance 访问:包括
Add()、GetBalance(),且不可遗漏任何路径(如日志打印、调试输出); -
WaitGroup 替代 channel 计数:
statuschannel 仅适合通知,不能作为“执行完成”的唯一依据;wg.Wait()才是强同步保证; -
避免过早关闭资源:
close(status)后仍可能有 goroutine 尝试发送——使用带缓冲的done chan struct{}更健壮; -
打印 Bug 也会导致误判:原代码中
fmt.Printf("... foo: %d, bar: %d\n", foo, foo)重复打印 foo,掩盖了真实差异,务必校验输出逻辑。
通过以上重构,程序将 100% 保持资金守恒:起始总余额 = 结束总余额,且每次运行结果确定。这正是 Go 并发模型的精髓——用组合原语(goroutine + channel + sync)显式表达协作,而非依赖隐式调度假设。










