16 - Go 协程(goroutine):从基础到实战
文章目录 16 - Go 协程goroutine从基础到实战什么是 goroutine 第一个 goroutinegoroutine 执行机制 关键模型GMP 模型 调度流程简化版 为什么 goroutine 很轻goroutine channel核心组合 channel 基础示例goroutine 通信 带缓冲 channelgoroutine 实战场景 并发任务处理 使用 WaitGroup 控制并发 并发安全Mutexgoroutine 常见坑必会❌ 主 goroutine 提前退出❌ 闭包变量问题经典面试题❌ goroutine 泄漏goroutine 调度细节进阶⏱ 抢占式调度Go 1.14 调度时机性能优化建议 控制 goroutine 数量 使用 sync.Pool 复用对象 合理使用 channel总结 goroutine 核心要点 并发三件套 一句话总结 面试高频问题 16 - Go 协程goroutine从基础到实战Go 的并发之所以强大不是因为它快而是因为它简单且优雅。在 Go 语言中并发编程的核心就是goroutine。它让你用极低的成本实现高并发是 Go 被称为“云原生语言”的关键原因之一。什么是 goroutinegoroutine 是 Go 语言中的轻量级线程用户态线程 特点占用内存极小初始 ~2KB创建成本极低由 Go runtime 调度而不是操作系统可以轻松创建成千上万个 第一个 goroutinepackagemainimport(fmttime)// 定义一个普通函数funchello(){fmt.Println(Hello, world!)// 打印一句话}funcmain(){gohello()// 使用 go 关键字启动一个 goroutine协程// 此时 hello() 会在一个新的协程中异步执行// main 函数不会等待它执行完time.Sleep(time.Second)// 让主 goroutine 休眠 1 秒// 作用防止 main 提前退出// 如果没有这行代码程序可能在 hello() 执行前就结束了} 注意1. goroutine 是异步执行的go hello()不会阻塞main 会继续往下执行2. main 退出 所有 goroutine 结束这是很多新手最容易踩的坑goroutine 执行机制 关键模型GMP 模型Go 的调度核心是名称含义GGoroutineM线程MachinePProcessor调度器 关系G任务 → P队列 → M执行 调度流程简化版goroutineG加入队列P 负责调度 GM线程执行 G遇到阻塞 → 切换其他 G 为什么 goroutine 很轻相比传统线程对比项线程goroutine创建成本高极低内存MB级KB级调度OSGo runtime切换慢快goroutine channel核心组合Go 并发哲学不要通过共享内存来通信而要通过通信来共享内存 channel 基础ch:make(chanint)示例goroutine 通信packagemainimportfmt// 定义一个 worker 函数接收一个 int 类型的 channelfuncworker(chchanint){ch-100// 向 channel 发送数据 100// 如果没有接收者这里会阻塞很关键}funcmain(){ch:make(chanint)// 创建一个无缓冲 channel同步 channel// 特点发送和接收必须同时准备好否则会阻塞goworker(ch)// 启动 goroutine 执行 worker// worker 会尝试向 channel 发送数据v:-ch// 从 channel 接收数据// 如果没有数据这里会阻塞直到有数据写入fmt.Println(v)// 输出接收到的值100} 带缓冲 channelpackagemainimportfmt// 定义 worker 函数参数是一个 int 类型的 channelfuncworker(chchanint){ch-100// 向 channel 发送数据 100// 因为是带缓冲 channel所以只要 buffer 没满就不会阻塞}funcmain(){ch:make(chanint,2)// 创建一个带缓冲的 channel容量为 2// 表示最多可以暂存 2 个 intch-1// 第一次发送放入 buffer[0]ch-2// 第二次发送放入 buffer[1]// 此时 buffer 已满2/2fmt.Println(-ch)// 从 channel 取出一个值1// buffer 腾出一个位置fmt.Println(-ch)// 再取出一个值2// 此时 buffer 为空goworker(ch)// 启动 goroutine 执行 worker// 因为 buffer 已经空出空间所以可以正常写入 100fmt.Println(-ch)// 从 channel 取出一个值100fmt.Println(main function)// 主函数继续执行不会等待 worker}输出1 2 100 main function实际运行逻辑是创建 buffer 2 的 channel写入 1、2buffer 满读取 1、2buffer 清空启动 goroutine 写入 100main 继续执行读取 100 特点不会立即阻塞类似队列goroutine 实战场景 并发任务处理packagemainimport(fmttime)// 定义一个任务函数模拟耗时操作functask(idint){fmt.Println(start,id)// 打印任务开始time.Sleep(time.Second)// 模拟耗时 1 秒的业务逻辑比如 IO / 网络 / DBfmt.Println(end,id)// 打印任务结束}funcmain(){fori:0;i10;i{gotask(i)// 启动 10 个 goroutine 并发执行 task// 每个 goroutine 处理一个 id}time.Sleep(2*time.Second)// 主 goroutine 休眠 2 秒// 作用防止 main 函数提前退出// 否则子 goroutine 还没执行完程序就结束了}输出start9start6start4start5start8start0start1start2start7start3end9end6end5end4end0end8end3end1end2end7 输出是“交错的” 重点goroutine 调度是抢占式 不可控顺序10 个 goroutine 同时进入调度队列Go runtime 自动调度执行执行顺序 完全不确定 使用 WaitGroup 控制并发packagemainimport(fmtsync)// 定义一个任务函数接收 id 和 WaitGroup 指针functask(idint,wg*sync.WaitGroup){deferwg.Done()// defer 保证函数结束时一定调用 Done()// 表示该 goroutine 执行完成计数器 -1fmt.Println(task:,id)// 模拟任务执行}funcmain(){varwg sync.WaitGroup// 创建 WaitGroup用于控制 goroutine 同步fori:0;i10;i{wg.Add(1)// 每启动一个 goroutine计数器 1// 表示“还有一个任务未完成”gotask(i,wg)// 启动 goroutine 执行任务// 注意传指针否则会拷贝 wg错误写法}wg.Wait()// 阻塞主 goroutine// 直到 wg 计数器变为 0所有任务完成}输出顺序不一的task:9task:0task:1task:2task:3task:4task:5task:6task:7task:8 推荐生产环境必须用 WaitGroup而不是 sleepWaitGroup 是 Go 中用于“等待一组 goroutine 完成”的标准同步工具本质是计数器控制并发生命周期。 并发安全Mutexpackagemainimport(fmtsync)// 全局变量共享资源多个 goroutine 会同时访问varcountint// 定义互斥锁用于保护共享变量 countvarmu sync.Mutex// 定义任务函数接收 WaitGroup 指针funcadd(wg*sync.WaitGroup){deferwg.Done()// goroutine 执行完成后通知 WaitGroup -1mu.Lock()// 加锁同一时刻只允许一个 goroutine 进入临界区count// 临界区对共享变量进行修改非原子操作mu.Unlock()// 解锁允许其他 goroutine 进入临界区}funcmain(){varwg sync.WaitGroup// 用于等待所有 goroutine 执行完成fori:0;i1000;i{wg.Add(1)// 每启动一个 goroutine计数 1goadd(wg)// 启动 goroutine 执行加法操作}wg.Wait()// 阻塞主 goroutine等待所有任务完成fmt.Println(count)// 输出最终结果1000} Go 设计哲学不要通过共享内存通信而要通过通信共享内存Mutex 的作用是保证共享资源在并发访问时的“互斥性”从而避免数据竞争保证程序结果正确。goroutine 常见坑必会❌ 主 goroutine 提前退出gofunc(){fmt.Println(hello)}() 可能不会执行✔ 解决WaitGroupchannel阻塞 main❌ 闭包变量问题经典面试题fori:0;i3;i{gofunc(){fmt.Println(i)}()} 可能输出3 3 3✔ 正确写法fori:0;i3;i{gofunc(iint){fmt.Println(i)}(i)}❌ goroutine 泄漏funcworker(chchanint){-ch// 永远等不到} 没有关闭 channel → goroutine 永久阻塞正确写法funcworker(chchanint){forv:rangech{fmt.Println(v)}}主函数ch:make(chanint)goworker(ch)ch-1ch-2close(ch)// 关键关闭 channelgoroutine 调度细节进阶⏱ 抢占式调度Go 1.14以前协程不会主动让出 CPU现在Go runtime 会强制抢占 优势防止某个 goroutine 长时间占用 CPU 调度时机goroutine 切换发生在channel 阻塞IO 阻塞系统调用runtime 主动调度性能优化建议 控制 goroutine 数量❌ 错误for{gotask()}✔ 正确使用 worker pooljobs:make(chanint,100)forw:0;w5;w{goworker(jobs)} 使用 sync.Pool 复用对象减少 GC 压力高并发场景 合理使用 channel不要滥用简单场景用锁更高效总结 goroutine 核心要点go关键字开启协程本质是用户态线程由 GMP 模型调度与 channel 配合使用最优雅 并发三件套goroutinechannelsyncWaitGroup / Mutex 一句话总结goroutine 让并发变简单但并发本身并不简单。 面试高频问题goroutine 和线程区别答轻量级线程由 Go runtime 管理。GMP 模型是什么答Go 运行时调度模型包含 Ggoroutine、M线程和 P处理器。channel 是怎么实现的答基于管道通信底层实现依赖于 goroutine。如何避免 goroutine 泄漏答确保所有 goroutine 执行完毕或使用 context 控制。select 的作用答多路复用用于等待多个 channel 操作。