Go这样检测Data Race让并发程序又简单了许多

大家好,我是极客老墨。 写并发代码最怕什么?不是死锁,不是性能问题,而是那些偶尔才出现、难以复现的诡异 bug。这些 bug 的罪魁祸首往往就是 Data Race(数据竞争)。 什么是 Data Race? Data Race 的定义很简单:当两个或多个 goroutine 同时访问同一个变量,且至少有一个在写入时,就会发生数据竞争。 这就像两个人同时在一张纸上写字,最后的结果可能是谁的字都不完整,或者干脆就是一团乱码。 举个例子: 1var counter int 2 3func increment() { 4 counter++ // 看起来是一行代码,实际上是三个操作:读取、加1、写入 5} 6 7func main() { 8 go increment() 9 go increment() 10 time.Sleep(time.Second) 11 fmt.Println(counter) // 结果可能是 1,也可能是 2 12} 这段代码看起来没问题,但 counter++ 并不是原子操作。两个 goroutine 可能同时读到 0,然后都写入 1,最终结果就是 1 而不是期望的 2。 顺便说一句,Data Race 和 Race Condition 是两个不同的概念,很多人会混淆。我之前翻译过一篇文章专门讨论这个问题,感兴趣可以看看。 Go 内存模型定义了 Happens-Before 规则来保证程序执行的顺序性,这和 Java 内存模型的思路类似。但光有规则还不够,Go 从 1.1 版本开始提供了 Race Detector,让我们能在运行时检测出这些问题。 ...

2026-03-03 · 5 min · 870 words · 老墨

Go高级教程:其他并发工具

大家好,我是极客老墨。 Goroutine 和 Channel 是 Go 并发的基础,但有些场景它们不够用。频繁创建对象导致 GC 压力大?用 sync.Pool。并发读写 map 会 panic?用 sync.Map。简单的计数器用锁太重?用 atomic。 这篇就聊聊 Go 的高级并发工具,看看它们各自适合什么场景。 sync.Pool:对象复用 频繁创建和销毁大对象会给 GC 带来压力,sync.Pool 用于对象复用。 基本用法 1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8// 创建对象池 9var bufferPool = sync.Pool{ 10 // New 函数:池为空时创建新对象 11 New: func() interface{} { 12 fmt.Println("Creating new buffer") 13 return make([]byte, 1024) 14 }, 15} 16 17func main() { 18 // 从池中获取对象 19 buf := bufferPool.Get().([]byte) 20 21 // 使用对象 22 copy(buf, []byte("Hello, World!")) 23 fmt.Println(string(buf[:13])) 24 25 // 归还到池中 26 bufferPool.Put(buf) 27 28 // 再次获取(会复用刚才的对象) 29 buf2 := bufferPool.Get().([]byte) 30 fmt.Println(string(buf2[:13])) // 还是 "Hello, World!" 31} 要点: ...

2025-12-20 · 9 min · 1831 words · 老墨

GoLang教程——Context上下文实战

大家好,我是极客老墨。 写并发程序时,经常遇到这样的场景:用户关闭了浏览器,但后台的数据库查询还在跑;API 调用超时了,但 Goroutine 还在等待响应。这些"失控"的 Goroutine 会浪费资源,甚至导致内存泄漏。 Go 的 Context 就是用来解决这个问题的。它能控制 Goroutine 的生命周期,实现超时、取消和数据传递。 这篇就聊聊 Context 的核心用法,看看它是怎么管理并发任务的。 Context 是什么 Context 是一个接口,定义了四个方法: 1type Context interface { 2 Deadline() (deadline time.Time, ok bool) 3 Done() <-chan struct{} 4 Err() error 5 Value(key interface{}) interface{} 6} 核心功能: 取消信号:通知 Goroutine 停止工作 超时控制:限制任务执行时间 数据传递:在调用链中传递元数据 创建 Context Go 提供了几个函数来创建 Context。 Background 和 TODO 1import "context" 2 3// Background:根 Context,通常在 main 函数中使用 4ctx := context.Background() 5 6// TODO:当不确定用什么 Context 时使用 7ctx := context.TODO() 要点: Background 是最顶层的 Context TODO 用于占位,表示还没想好用什么 两者都不会被取消,没有超时,没有值 WithCancel:手动取消 1// 创建可取消的 Context 2ctx, cancel := context.WithCancel(context.Background()) 3 4// 调用 cancel 取消 Context 5cancel() WithTimeout:超时自动取消 1// 2 秒后自动取消 2ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 3defer cancel() WithDeadline:指定截止时间 1// 指定截止时间 2deadline := time.Now().Add(2 * time.Second) 3ctx, cancel := context.WithDeadline(context.Background(), deadline) 4defer cancel() WithValue:传递数据 1// 存储键值对 2ctx := context.WithValue(context.Background(), "userID", 123) 3 4// 获取值 5if userID, ok := ctx.Value("userID").(int); ok { 6 fmt.Println("User ID:", userID) 7} 超时控制 超时控制是 Context 最常用的场景。 ...

2025-03-02 · 7 min · 1481 words · 老墨

GoLang教程——并发进阶

大家好,我是极客老墨。 并发编程中,Channel 很好用,但不是万能的。有时候需要更精细的控制:等待一组任务完成、保护共享数据、限制并发数量。这时候就需要 sync 包的同步工具了。 这篇就聊聊 Go 的并发进阶工具,看看它们各自适合什么场景。 WaitGroup:等待组 WaitGroup 用于等待一组 Goroutine 完成,是最常用的同步工具。 基本用法 1import ( 2 "fmt" 3 "sync" 4 "time" 5) 6 7func worker(id int, wg *sync.WaitGroup) { 8 defer wg.Done() // 完成时调用 9 10 fmt.Printf("Worker %d starting\n", id) 11 time.Sleep(time.Second) 12 fmt.Printf("Worker %d done\n", id) 13} 14 15func main() { 16 var wg sync.WaitGroup 17 18 for i := 1; i <= 5; i++ { 19 wg.Add(1) // 增加计数 20 go worker(i, &wg) 21 } 22 23 wg.Wait() // 等待所有完成 24 fmt.Println("All workers completed") 25} 要点: Add(n) 增加计数器 Done() 减少计数器(等价于 Add(-1)) Wait() 阻塞直到计数器为 0 必须传递指针 *sync.WaitGroup 常见错误 1// ❌ 错误:在 goroutine 内部 Add 2go func() { 3 wg.Add(1) // 可能在 Wait 之后执行 4 defer wg.Done() 5 // ... 6}() 7 8// ✅ 正确:在启动前 Add 9wg.Add(1) 10go func() { 11 defer wg.Done() 12 // ... 13}() 批量添加 1func main() { 2 var wg sync.WaitGroup 3 4 // 一次性添加多个 5 wg.Add(5) 6 7 for i := 1; i <= 5; i++ { 8 go func(id int) { 9 defer wg.Done() 10 fmt.Printf("Task %d\n", id) 11 }(i) 12 } 13 14 wg.Wait() 15} Mutex:互斥锁 Mutex 用于保护共享数据,确保同一时间只有一个 Goroutine 访问。 ...

2024-12-05 · 9 min · 1714 words · 老墨

[GoLang避坑实战-12] 并发初体验:Goroutine 和 Channel 真的那么神吗?

大家好,我是极客老墨。 传统语言里写并发,要创建线程、加锁、处理竞态条件,一不小心就死锁。Go 的并发模型完全不同:用 Goroutine 代替线程,用 Channel 代替锁。这种 CSP(通信顺序进程)模型,让并发编程变得简单多了。 这篇就聊聊 Go 的并发基础,看看 Goroutine 和 Channel 是怎么配合工作的。 Goroutine 基础 Goroutine 是 Go 的轻量级协程,比线程轻量得多。 启动 Goroutine 使用 go 关键字启动一个 Goroutine。 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func sayHello() { 9 fmt.Println("Hello from goroutine") 10} 11 12func main() { 13 // 启动一个 Goroutine 14 go sayHello() 15 16 // 主 Goroutine 继续执行 17 fmt.Println("Hello from main") 18 19 // 等待一下,否则程序会立即退出 20 time.Sleep(time.Second) 21} 要点: go funcName() 启动一个新的 Goroutine Goroutine 是并发执行的,不会阻塞主程序 main 函数结束时,所有 Goroutine 都会被强制终止 Goroutine 的特点 1func printNumbers() { 2 for i := 1; i <= 5; i++ { 3 fmt.Printf("%d ", i) 4 time.Sleep(100 * time.Millisecond) 5 } 6} 7 8func printLetters() { 9 for i := 'A'; i <= 'E'; i++ { 10 fmt.Printf("%c ", i) 11 time.Sleep(100 * time.Millisecond) 12 } 13} 14 15func main() { 16 go printNumbers() 17 go printLetters() 18 19 time.Sleep(time.Second) 20 fmt.Println("\nDone") 21} 输出可能是:1 A 2 B 3 C 4 D 5 E 或其他顺序(不确定) ...

2024-11-19 · 13 min · 2588 words · 老墨

Data Race vs Race Condition

原文地址: https://blog.regehr.org/archives/490, 翻译并略作改动。 竞态条件(race Condition)是当事件的时间或顺序影响程序的正确性时发生的缺陷。一般来说,需要某种外部计时或排序非确定性来产生竞态条件;典型的例子有上下文切换、操作系统信号、多处理器上的内存操作和硬件 中断。 当程序中有两次内存访问时,就会发生数据竞争(Data Race): 目标为同一内存位置 由两个线程同时执行 不是读取操作 不是同步操作 上边这个定义来自微软研究院的 Sebastian Burckhardt。该定义的两个方面需要注意: “同时”意味着没有像锁这样的东西强制一个操作在另一个操作之前或之后发生。 “不是同步操作”是指程序可能包含特殊的内存操作,例如用于实现锁的操作,这些操作本身并不同步。 在实践中,它们两者存在相当大的重合:许多 Race Condition 是由 Data Race 引起的,并且许多 Data Race 导致 Race Condition。另一方面,两者也可以相互独立,可能产生没有 Data Race 的 Race Condition,也可能产生没有 Race Condition 的 Data Race。 让我们从一个在两个银行账户之间转移资金的简单函数开始: transfer1 (amount, account_from, account_to) { if (account_from.balance < amount) return NOPE; account_to.balance += amount; account_from.balance -= amount; return YEP; } 当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。 我们可以尝试这样修复它: transfer2 (amount, account_from, account_to) { atomic { bal = account_from.balance; } if (bal < amount) return NOPE; atomic { account_to.balance += amount; } atomic { account_from.balance -= amount; } return YEP; } 这里的“atomic”(原子性)是由语言运行时实现的,也许简单地通过在原子块开始时获取线程互斥体(Mutex)并在结束时释放它,也许使用某种事务(Transaction),或者也许通过禁用中断 —— 出于示例的目的,只要 atomic 块内的代码以原子方式执行就能解决竞争问题。 ...

2024-06-18 · 2 min · 235 words · 老墨