Go 语言协程(Goroutine)及其并发模型的深度指南。
第一部分:协程基础与生命周期控制
如何启动协程、主协程的特性以及最基本的同步工具。
go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
/**
* 知识点 1-10: 基础定义与生命周期
* 1. 使用 'go' 关键字即可启动一个协程。
* 2. 协程是用户态轻量级线程,初始栈空间仅 2KB。
* 3. 主协程 (Main Goroutine) 结束,所有子协程立即强制退出。
* 4. 协程的执行顺序是随机的,由调度器决定。
* 5. runtime.Gosched() 用于主动让出 CPU 时间片。
* 6. runtime.Goexit() 立即终止当前协程,但会执行 defer。
* 7. runtime.NumGoroutine() 获取当前运行中的协程数量。
* 8. 闭包捕获问题:协程内访问外部循环变量需传参,否则会引用最终值。
* 9. sync.WaitGroup 用于等待一组协程完成,是基础同步工具。
* 10. WaitGroup 的 Add() 数量必须与 Done() 一致,否则会死锁或 Panic。
*/
func basicDemo() {
var wg sync.WaitGroup
// 演示闭包捕获陷阱与正确传参
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) { // 必须通过参数传递 i
defer wg.Done()
fmt.Printf("子协程 %d 正在运行\n", id)
}(i)
}
wg.Wait()
fmt.Println("所有基础协程执行完毕")
}
func runtimeControl() {
go func() {
defer fmt.Println("协程退出前的清理工作")
fmt.Println("准备退出协程...")
runtime.Goexit() // 终止协程
fmt.Println("这行代码永远不会执行")
}()
time.Sleep(time.Millisecond * 100)
fmt.Printf("当前活跃协程数: %d\n", runtime.NumGoroutine())
}
func main() {
basicDemo()
runtimeControl()
}
第二部分:通道 (Channel) 深度解析
通道是协程间通信的桥梁,遵循"不要通过共享内存来通信,而要通过通信来共享内存"的哲学。
go
package main
import (
"fmt"
"time"
)
/**
* 知识点 11-30: 通道机制
* 11. 通道是线程安全的,底层自带锁。
* 12. 无缓冲通道 (Unbuffered):发送和接收必须同时就绪,否则阻塞。
* 13. 有缓冲通道 (Buffered):在容量范围内发送不阻塞。
* 14. 关闭通道:close(ch)。重复关闭或关闭 nil 通道会 Panic。
* 15. 向已关闭通道发送数据会 Panic。
* 16. 从已关闭通道读取数据:返回零值和 false。
* 17. 单向通道:chan<- 仅发送,<-chan 仅接收。常用于函数参数约束。
* 18. select 语句:随机选择一个就绪的通道操作。
* 19. select default 分支:实现非阻塞发送或接收。
* 20. 通道可以用于实现信号量 (Semaphore)。
* 21. range 遍历通道:直到通道被关闭且数据取完才结束。
* 22. nil 通道的读写会永久阻塞。
* 23. 内存泄漏:未关闭且无接收者的协程会永久阻塞在发送处。
*/
func channelPatterns() {
// 演示有缓冲通道
ch := make(chan string, 2)
ch <- "消息 1"
ch <- "消息 2"
fmt.Println("有缓冲通道已满,接下来的发送将阻塞")
// 演示 select 超时机制
timeoutCh := make(chan bool, 1)
go func() {
time.Sleep(time.Second * 2)
timeoutCh <- true
}()
select {
case msg := <-ch:
fmt.Println("收到:", msg)
case <-time.After(time.Second * 1):
fmt.Println("请求超时!")
}
}
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("工人 %d 开始处理任务 %d\n", id, j)
time.Sleep(time.Millisecond * 500)
results <- j * 2
}
}
func workerPoolDemo() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动 3 个工人
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 关闭任务通道,告知工人没有新任务了
// 收集结果
for a := 1; a <= numJobs; a++ {
<-results
}
fmt.Println("所有任务处理完成")
}
func main() {
channelPatterns()
workerPoolDemo()
}
第三部分:同步原语与锁 (sync 包)
当必须访问共享资源时,Go 提供了传统的锁机制。
go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
/**
* 知识点 31-50: 同步原语
* 31. sync.Mutex:互斥锁,保护共享资源。
* 32. 不要拷贝锁:锁被使用后拷贝会导致逻辑失效(Panic)。
* 33. sync.RWMutex:读写锁。允许多个并发读,但写时互斥。
* 34. RLock() 与 RUnlock():读锁操作。
* 35. 性能对比:在读多写少的场景,RWMutex 优于 Mutex。
* 36. sync.Once:确保函数只执行一次(常用于单例模式)。
* 37. sync.Cond:条件变量,用于协程间的等待/通知机制。
* 38. sync.Pool:对象池,减轻 GC 压力。
* 39. sync.Map:并发安全 Map,优化了读写分离。
* 40. 原子操作 (sync/atomic):利用 CPU 指令实现无锁并发,性能极高。
*/
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock 之后务必 defer Unlock,防止 Panic 导致死锁
defer c.mu.Unlock()
c.v[key]++
}
func atomicDemo() {
var count int64 = 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 原子递增,无需加锁
atomic.AddInt64(&count, 1)
}()
}
wg.Wait()
fmt.Printf("原子计数结果: %d\n", count)
}
var (
instance *SafeCounter
once sync.Once
)
func GetInstance() *SafeCounter {
once.Do(func() {
fmt.Println("首次初始化单例对象")
instance = &SafeCounter{v: make(map[string]int)}
})
return instance
}
func main() {
atomicDemo()
c := GetInstance()
c.Inc("hits")
fmt.Printf("计数器: %d\n", c.v["hits"])
}
第四部分:上下文控制 (Context Package)
Context 是管理协程树、超时控制和元数据传递的标准方式。
go
package main
import (
"context"
"fmt"
"time"
)
/**
* 知识点 51-70: Context 模式
* 51. Context 负责在协程树中传递取消信号、截止日期和键值。
* 52. context.Background():根 Context,通常由 main 开启。
* 53. context.WithCancel():返回可手动取消的 Context。
* 54. context.WithTimeout():到达指定时间后自动取消。
* 55. context.WithDeadline():到达某个时刻后自动取消。
* 56. Done() 通道:当 Context 被取消时,该通道会被关闭。
* 57. Err():返回取消的原因(Timeout 或 Canceled)。
* 58. context.WithValue():传递请求作用域内的元数据(慎用,非类型安全)。
* 59. 最佳实践:将 Context 作为函数的第一个参数传入,名为 ctx。
* 60. 级联取消:父 Context 取消,所有子 Context 也会同步取消。
*/
func longRunningTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("任务 %s 被取消: %v\n", name, ctx.Err())
return
default:
fmt.Printf("任务 %s 正在处理中...\n", name)
time.Sleep(time.Millisecond * 300)
}
}
}
func main() {
// 演示超时控制
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
defer cancel() // 释放资源
go longRunningTask(ctx, "Worker-1")
// 等待足够长的时间观察结果
time.Sleep(time.Second * 2)
fmt.Println("主程序退出")
}
第五部分:GMP 调度模型与高级进阶
这部分涉及 Go 运行时的底层逻辑和更高级的并发模式。
go
package main
import (
"fmt"
"runtime"
"sync"
)
/**
* 知识点 71-100: GMP 模型与高级实践
* 71. G (Goroutine):协程,保存任务状态。
* 72. M (Machine):内核线程,负责执行代码。
* 73. P (Processor):处理器,保存本地协程队列,连接 G 和 M。
* 74. 调度策略:Work Stealing(任务窃取),从其他 P 偷取 G。
* 75. 调度策略:Hand Off(接管),阻塞时 P 与 M 分离,寻找新 M。
* 76. runtime.GOMAXPROCS():设置 P 的数量,默认为 CPU 核心数。
* 77. 协程泄露:开启了协程但无法退出,导致内存持续增长。
* 78. 并发安全检测:使用 'go run -race' 检查数据竞争。
* 79. 生产者消费者模式:使用管道解耦数据产生和处理。
* 80. 扇入 (Fan-in):多个管道汇聚到一个管道。
* 81. 扇出 (Fan-out):一个任务分发到多个协程并行处理。
* 82. 错误传播:在协程中使用特定的 Result 结构体传递 Error。
* 83. 优雅退出:通过信号量或 Context 确保协程在关机前完成清理。
* 84. 无锁编程:尽量使用原子操作或通道,避免锁竞争。
* 85. 协程栈监控:使用 pprof 工具分析协程堆栈快照。
*/
// 扇入模式演示
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// 设置最大并行核心数
runtime.GOMAXPROCS(runtime.NumCPU())
c1 := make(chan int, 1)
c2 := make(chan int, 1)
c1 <- 10
c2 <- 20
close(c1)
close(c2)
// 合并两个通道的数据
for n := range merge(c1, c2) {
fmt.Printf("从合并通道获取: %d\n", n)
}
}
核心知识点补充总结
- 并发安全检测 :在开发过程中,务必使用
go test -race ./...。这是发现隐藏并发 Bug 的利器。 - 死锁预防 :死锁通常发生在多个协程互相等待对方释放资源(通道或锁)。保持加锁顺序一致,或尽量使用
select配备超时机制。 - Panic 处理 :子协程内部如果发生 Panic 且未捕获(
recover),会导致整个进程崩溃。在所有重要的长生命周期协程中,务必在开头defer一个recover函数。 - 避免全局变量:并发环境下,全局变量是万恶之源。尽可能通过参数传递或使用单例模式配合锁。
- 合理设置缓冲区:过大的缓冲区会掩盖下游处理能力不足的问题,导致系统"虚假健康"并在压力突增时崩溃。