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用于从多个通道中去数据

  1. select语句会一直等待, 直到某个case完成通信操作(default也算是一个case)。
  2. select语句有多个case满足时, 会随机选择一个来执行。(default永远会满足)
  3. 如果没有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 中捕获循环变量(需传值);
  • ✅ 通道关闭遵循「生产者关闭,消费者只读」原则。

五、总结

核心要点

  1. 并发原语
    • goroutine:轻量级协程,go 函数启动;
    • Channel:类型化管道,支持读写、缓冲 / 无缓冲、单向通道,核心是「通信共享内存」;
    • sync 包:WaitGroup(等待)、Mutex(互斥锁)、Once(单例)、RWMutex(读写锁);
    • Context:管理 goroutine 生命周期,传递取消信号 / 超时。
  2. 经典模式
    • 生产者消费者:Channel 解耦生产消费;
    • 工作池:限定并发数,避免 goroutine 泛滥;
    • errgroup:并发错误收集 + 快速失败。
  3. 避坑关键
    • 避免数据竞争(Channel / 锁);
    • 正确管理 goroutine 生命周期(WaitGroup/Context);
    • 规范使用 Channel(生产者关闭,避免向关闭通道发送)。

核心原则

  • Go 并发的核心是「通信」而非「竞争」,优先用 Channel;
  • 轻量级 goroutine 虽好,但需合理控制数量(工作池);
  • 显式管理 goroutine 生命周期,避免泄漏和提前终止。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐