Go 语言协程

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)
	}
}

核心知识点补充总结

  1. 并发安全检测 :在开发过程中,务必使用 go test -race ./...。这是发现隐藏并发 Bug 的利器。
  2. 死锁预防 :死锁通常发生在多个协程互相等待对方释放资源(通道或锁)。保持加锁顺序一致,或尽量使用 select 配备超时机制。
  3. Panic 处理 :子协程内部如果发生 Panic 且未捕获(recover),会导致整个进程崩溃。在所有重要的长生命周期协程中,务必在开头 defer 一个 recover 函数。
  4. 避免全局变量:并发环境下,全局变量是万恶之源。尽可能通过参数传递或使用单例模式配合锁。
  5. 合理设置缓冲区:过大的缓冲区会掩盖下游处理能力不足的问题,导致系统"虚假健康"并在压力突增时崩溃。
相关推荐
AI袋鼠帝2 小时前
火爆全网的Seedance2.0 十万人排队,我2分钟就用上了
前端
Jenlybein2 小时前
快速了解熟悉 Vite ,即刻上手使用
前端·javascript·vite
momo061172 小时前
AI Skill是什么?
前端·ai编程
言萧凡_CookieBoty2 小时前
用 AI 搞定用户系统:Superpowers 工程化开发教程
前端·ai编程
牛奶2 小时前
5MB vs 4KB vs 无限大:浏览器存储谁更强?
前端·浏览器·indexeddb
牛奶2 小时前
setTimeout设为0就马上执行?JS异步背后的秘密
前端·性能优化·promise
LaughingZhu4 小时前
Product Hunt 每日热榜 | 2026-04-05
前端·数据库·人工智能·经验分享·神经网络
SuperEugene4 小时前
Vue3 组件复用设计:Props / 插槽 / 组合式函数,三种复用方式选型|组件化设计基础篇
前端·javascript·vue.js
nFBD29OFC5 小时前
利用Vue元素指令自动合并tailwind类名
前端·javascript·vue.js