Go 语言并发编程核心与用法
并发原语goroutine:轻量级协程,go 函数启动;Channel:类型化管道,支持读写、缓冲 / 无缓冲、单向通道,核心是「通信共享内存」;sync 包:WaitGroup(等待)、Mutex(互斥锁)、Once(单例)、RWMutex(读写锁);Context:管理 goroutine 生命周期,传递取消信号 / 超时。经典模式生产者消费者:Channel 解耦生产消费;工作池:限定并发数
·
Go 语言并发编程核心与用法

一、并发核心思想(Go 的独特设计)
1. 设计哲学
- 不要通过共享内存通信,要通过通信共享内存:Go 推荐用 Channel 传递数据,而非加锁竞争共享内存;
- 轻量级并发:goroutine(协程)替代操作系统线程,成本低(初始栈仅 2KB,可动态扩缩);
- M:N 调度:Go 运行时(runtime)将 M 个 goroutine 调度到 N 个操作系统线程,兼顾并发性能和系统资源。
2. 核心概念对比(理解 goroutine 本质)
| 类型 | 栈大小 | 创建 / 销毁成本 | 调度方 | 数量限制 |
|---|---|---|---|---|
| 操作系统线程 | 固定(MB 级) | 高 | 操作系统内核 | 数百 / 数千(受限) |
| goroutine | 动态(KB 级) | 极低 | Go 运行时 | 数万 / 数十万(轻松支持) |
二、核心并发原语(必掌握)
1. Goroutine(协程)—— 并发执行单元
基础用法
- 创建:
go 函数名(参数),启动后异步执行,无需显式回收; - 特性:
- 主 goroutine 退出,所有子 goroutine 会被强制终止;
- goroutine 无返回值,需通过 Channel 传递结果;
- 共享进程内存空间,但有独立栈。
示例:基础 goroutine
package main
import (
"fmt"
"time"
)
// 子goroutine执行的函数
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("%s:Hello Goroutine!%d\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 启动2个goroutine(异步执行)
go sayHello("goroutine1")
go sayHello("goroutine2")
// 主goroutine等待(否则主goroutine退出,子goroutine终止)
time.Sleep(500 * time.Millisecond)
fmt.Println("主goroutine结束")
}
2. Channel(通道)—— 通信共享内存
核心本质
- 类型化的管道,用于 goroutine 间安全传递数据(自带同步互斥);
- 引用类型,需通过
make初始化,支持「无缓冲」和「有缓冲」两种模式。
基础用法
| 操作 | 语法示例 | 说明 |
|---|---|---|
| 声明 / 初始化 | var ch chan int / ch := make(chan int, 3) |
无缓冲:make(chan T);有缓冲:make(chan T, cap) |
| 发送数据 | ch <- value |
无缓冲:阻塞直到有 goroutine 接收;有缓冲:未满则不阻塞 |
| 接收数据 | val := <-ch / val, ok := <-ch |
ok=false 表示通道已关闭且无数据 |
| 关闭通道 | close(ch) |
关闭后无法发送,仍可接收剩余数据 |
| 遍历通道 | for val := range ch |
自动遍历直到通道关闭 |
示例 1:无缓冲 Channel(同步通信)
func main() {
// 无缓冲通道:发送和接收必须配对,否则阻塞
ch := make(chan string)
// 子goroutine发送数据
go func() {
ch <- "来自子goroutine的消息" // 阻塞,直到主goroutine接收
close(ch) // 发送完成后关闭通道
}()
// 主goroutine接收数据
msg := <-ch
fmt.Println(msg) // 输出:来自子goroutine的消息
}
示例 2:有缓冲 Channel(异步通信)
func main() {
// 有缓冲通道:容量3,未满时发送不阻塞
ch := make(chan int, 3)
// 发送3个数据(不阻塞)
ch <- 1
ch <- 2
ch <- 3
// 遍历接收所有数据
for val := range ch {
fmt.Println(val)
if val == 3 {
close(ch) // 遍历完关闭通道
}
}
}
示例 3:单向 Channel(约束读写)
// 只写通道:参数为chan<- int
func sendData(ch chan<- int) {
ch <- 100
close(ch)
}
// 只读通道:参数为<-chan int
func recvData(ch <-chan int) {
val := <-ch
fmt.Println(val) // 输出:100
}
func main() {
ch := make(chan int)
go sendData(ch)
recvData(ch)
}
3. Sync 包 —— 共享内存同步(补充方案)
Go 推荐用 Channel 通信,但部分场景需共享内存,sync包提供核心同步原语:
(1) sync.WaitGroup — 等待多个 goroutine 完成
- 核心方法:
Add(n)(设置等待数)、Done()(完成 1 个,计数 - 1)、Wait()(阻塞直到计数为 0); - 适用场景:主 goroutine 等待所有子 goroutine 完成,无需 Channel 传递结果。
func main() {
var wg sync.WaitGroup
// 启动3个goroutine
for i := 0; i < 3; i++ {
wg.Add(1) // 每启动1个,计数+1
go func(idx int) {
defer wg.Done() // 退出时计数-1
fmt.Printf("goroutine %d 执行\n", idx)
time.Sleep(100 * time.Millisecond)
}(i)
}
wg.Wait() // 阻塞直到所有Done
fmt.Println("所有goroutine执行完成")
}
(2) sync.Mutex/sync.RWMutex — 互斥锁 / 读写锁
sync.Mutex:排他锁,同一时间仅 1 个 goroutine 持有锁;sync.RWMutex:读写分离锁,支持「多读单写」,读多写少场景性能更优。
// 共享变量
var (
count int
lock sync.RWMutex
)
// 写操作:排他锁
func addCount() {
lock.Lock()
defer lock.Unlock()
count++
}
// 读操作:共享锁
func getCount() int {
lock.RLock()
defer lock.RUnlock()
return count
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
addCount()
}()
}
wg.Wait()
fmt.Println("最终计数:", getCount()) // 输出:1000(无并发安全问题)
}
(3) sync.Once — 确保函数仅执行一次
- 核心方法:
Once.Do(f func()),无论调用多少次,f 仅执行 1 次; - 适用场景:单例初始化、配置加载等。
var once sync.Once
var config map[string]string
// 初始化配置(仅执行1次)
func initConfig() {
fmt.Println("初始化配置")
config = map[string]string{"env": "prod"}
}
func main() {
// 多次调用Do,仅执行1次initConfig
for i := 0; i < 3; i++ {
once.Do(initConfig)
fmt.Println("配置:", config)
}
}
(4) sync.Cond — 条件变量
- 基于互斥锁,实现「等待 - 通知」机制;
- 核心方法:
Wait()(等待通知)、Signal()(通知 1 个 goroutine)、Broadcast()(通知所有 goroutine); - 适用场景:需等待某个条件满足后再执行(如生产者消费者模型)。
4. Context — 上下文(goroutine 生命周期管理)
核心作用
- 传递取消信号、超时时间、元数据;
- 控制 goroutine 树的生命周期(父 goroutine 取消,子 goroutine 也取消)。
常用类型
| 类型 | 用途 | 示例 |
|---|---|---|
| context.Background() | 根上下文,无超时、无取消 | 初始化顶级上下文 |
| context.WithCancel() | 创建可取消上下文 | 手动取消 goroutine |
| context.WithTimeout() | 创建带超时的上下文 | 超时自动取消 |
| context.WithDeadline() | 创建带截止时间的上下文 | 到时间自动取消 |
示例:Context 控制 goroutine 取消
func task(ctx context.Context, name string) {
for {
select { // select是多路复用
case <-ctx.Done(): // 接收取消信号
fmt.Printf("任务%s被取消:%v\n", name, ctx.Err())
return
default:
fmt.Printf("任务%s执行中...\n", name)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
// 创建带超时的上下文(200ms后取消)
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // 确保最终取消
// 启动任务goroutine
go task(ctx, "任务1")
// 主goroutine等待
time.Sleep(300 * time.Millisecond)
fmt.Println("主程序结束")
}
备注:select用于从多个通道中去数据
- select语句会一直等待, 直到某个case完成通信操作(default也算是一个case)。
- select语句有多个case满足时, 会随机选择一个来执行。(default永远会满足)
- 如果没有case满足时, select语句会阻塞, 需要注意使用,小心造成deadlock。
三、经典并发模式(实战高频)
1. 生产者消费者模型
- 核心:生产者 goroutine 生产数据写入 Channel,消费者 goroutine 从 Channel 读取数据处理;
- 优势:解耦生产和消费,自动平衡速度(Channel 阻塞特性)。
// 生产者
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 生产数据
fmt.Printf("生产:%d\n", i)
time.Sleep(50 * time.Millisecond)
}
close(ch) // 生产完成,关闭通道
}
// 消费者
func consumer(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for val := range ch {
fmt.Printf("消费:%d\n", val)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
ch := make(chan int, 2) // 缓冲通道,平衡生产消费速度
var wg sync.WaitGroup
wg.Add(2) // 2个消费者
go consumer(ch, &wg)
go consumer(ch, &wg)
producer(ch) // 启动生产者
wg.Wait() // 等待消费者完成
fmt.Println("生产消费完成")
}
2. 工作池模式(限定并发数)
- 核心:创建固定数量的 worker goroutine,从任务通道取任务执行,避免 goroutine 泛滥;
- 适用场景:批量任务(如批量请求接口、批量处理文件)。
// 任务函数
func worker(id int, tasks <-chan int, results chan<- int) {
for task := range tasks {
fmt.Printf("worker %d 处理任务:%d\n", id, task)
results <- task * 2 // 返回处理结果
time.Sleep(100 * time.Millisecond)
}
}
func main() {
const (
workerNum = 3 // 工作池大小
taskNum = 10 // 总任务数
)
// 创建任务通道和结果通道
tasks := make(chan int, taskNum)
results := make(chan int, taskNum)
// 启动工作池
for i := 0; i < workerNum; i++ {
go worker(i, tasks, results)
}
// 生产任务
for i := 0; i < taskNum; i++ {
tasks <- i
}
close(tasks) // 任务生产完成
// 收集结果
for i := 0; i < taskNum; i++ {
<-results
}
close(results)
fmt.Println("所有任务处理完成")
}
3. 错误组模式(errgroup)
- 核心:封装 goroutine+context,统一收集并发任务错误,支持快速失败(前文详细讲过,此处简化示例)。
func main() {
eg, ctx := errgroup.WithContext(context.Background())
// 启动3个并发任务
for i := 0; i < 3; i++ {
idx := i
eg.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if idx == 1 { // 模拟任务1出错
return fmt.Errorf("任务%d执行失败", idx)
}
fmt.Printf("任务%d执行成功\n", idx)
return nil
}
})
}
// 等待结果
if err := eg.Wait(); err != nil {
fmt.Printf("执行失败:%v\n", err)
} else {
fmt.Println("所有任务执行成功")
}
}
四、并发编程避坑点(核心重点)
1. 常见坑
- ❌ 竞态条件(数据竞争):多个 goroutine 同时读写共享变量,未加锁 / 未用 Channel;
- 解决方案:用 Channel 传递数据,或用 sync.Mutex/RWMutex 加锁;
- 检测工具:
go run -race main.go(竞态检测)。
- ❌ 主 goroutine 提前退出:子 goroutine 未执行完就终止;
- 解决方案:用 sync.WaitGroup、Channel、Context 等待。
- ❌ 通道使用不当:
- 向已关闭的通道发送数据(panic);
- 无缓冲通道发送后未接收(永久阻塞);
- 忘记关闭通道(遍历通道时永久阻塞)。
- ❌ 过度使用 goroutine:创建数万 / 数十万 goroutine 处理简单任务,导致调度开销;
- 解决方案:用工作池限定并发数。
- ❌ Context 使用不当:
- 未传递 Context(无法取消 goroutine);
- 长时间占用 Context(导致资源泄漏)。
2. 最佳实践
- ✅ 优先用 Channel 通信共享数据,而非共享内存加锁;
- ✅ 用 Context 管理 goroutine 生命周期,避免 goroutine 泄漏;
- ✅ 批量任务用工作池限定并发数;
- ✅ 用
go run -race检测数据竞争; - ✅ 避免在 goroutine 中捕获循环变量(需传值);
- ✅ 通道关闭遵循「生产者关闭,消费者只读」原则。
五、总结
核心要点
- 并发原语:
- goroutine:轻量级协程,
go 函数启动; - Channel:类型化管道,支持读写、缓冲 / 无缓冲、单向通道,核心是「通信共享内存」;
- sync 包:WaitGroup(等待)、Mutex(互斥锁)、Once(单例)、RWMutex(读写锁);
- Context:管理 goroutine 生命周期,传递取消信号 / 超时。
- goroutine:轻量级协程,
- 经典模式:
- 生产者消费者:Channel 解耦生产消费;
- 工作池:限定并发数,避免 goroutine 泛滥;
- errgroup:并发错误收集 + 快速失败。
- 避坑关键:
- 避免数据竞争(Channel / 锁);
- 正确管理 goroutine 生命周期(WaitGroup/Context);
- 规范使用 Channel(生产者关闭,避免向关闭通道发送)。
核心原则
- Go 并发的核心是「通信」而非「竞争」,优先用 Channel;
- 轻量级 goroutine 虽好,但需合理控制数量(工作池);
- 显式管理 goroutine 生命周期,避免泄漏和提前终止。
更多推荐



所有评论(0)