并发编程是 Go 语言最鲜明的标签,也是它能在云原生时代脱颖而出的关键。Go 没有沿用传统的线程加锁模型,而是提供了 Goroutine(轻量级协程)和 Channel(通信管道)这对组合拳。Goroutine 让你用 go 关键字就能启动一个并发任务,Channel 则提供了安全的 Goroutine 间通信机制。本文深入讲解 Goroutine 的启动与调度、Channel 的两种类型(无缓冲/有缓冲)、select 多路复用、sync 包中的 WaitGroup 和 Mutex,并通过生产者-消费者和工作池两个实战案例,帮你掌握 Go 并发编程的核心技能。
一、Goroutine:轻量级并发单元
1.1 什么是 Goroutine?
Goroutine 是 Go 运行时管理的轻量级线程。它的初始栈大小仅为 2KB(操作系统线程通常为 1-2MB),可以轻松创建成千上万个 Goroutine 而不会耗尽内存。
启动一个 Goroutine 只需要在函数调用前加上 go 关键字:
go
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
// 启动一个 Goroutine 执行 sayHello
go sayHello()
// 主 Goroutine 等待 1 秒,否则子 Goroutine 可能还没执行完主程序就退出了
time.Sleep(1 * time.Second)
fmt.Println("Main function finished")
}
1.2 Goroutine 的调度模型:GMP
Go 运行时的调度器采用 GMP 模型:

调度器的工作原理:
GOMAXPROCS 决定了 P 的数量(默认等于 CPU 核心数)。
每个 P 维护一个本地 Goroutine 队列。
M 从 P 的队列中获取 G 并执行。
当一个 G 阻塞(如等待 I/O)时,M 会与 P 分离,P 会寻找或创建新的 M 继续执行其他 G。
当 G 被阻塞时,M 可以继续服务于其他 G,从而实现高效的并发。
设置 P 的数量(通常不需要手动调整):
go
import "runtime"
func main() {
// 获取 CPU 核心数
fmt.Println(runtime.NumCPU())
// 设置 P 的数量(一般保持默认)
runtime.GOMAXPROCS(4)
}
1.3 sync.WaitGroup:等待所有 Goroutine 完成
使用 time.Sleep 等待不是一个好的做法。sync.WaitGroup 提供了更可靠的等待机制:
go
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 函数结束时通知 WaitGroup 该任务已完成
fmt.Printf("Worker %d 开始工作\n", id)
// 模拟耗时操作
// time.Sleep(time.Second)
fmt.Printf("Worker %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 增加一个等待计数
go worker(i, &wg)
}
wg.Wait() // 阻塞等待所有 Goroutine 完成
fmt.Println("所有 Worker 已完成")
}
⚠️ 注意事项:
wg.Add(1) 必须在启动 Goroutine 之前调用,不能在 Goroutine 内部调用(否则可能因竞争条件导致计数错误)。
wg.Done() 等价于 wg.Add(-1),可以用 defer 确保一定执行。
WaitGroup 必须通过指针传递(值传递会拷贝计数器,导致死锁)。
二、Channel:Goroutine 间的通信管道
Go 并发模型的核心理念是:不要通过共享内存来通信,而要通过通信来共享内存。 Channel 正是实现这一理念的核心工具,它是一个类型化的通信管道,用于在 Goroutine 之间传递数据。
2.1 无缓冲 Channel(同步 Channel)
无缓冲 Channel 在发送和接收时都会阻塞,直到另一端准备就绪。它天然实现了 Goroutine 之间的同步。
go
func main() {
// 创建一个无缓冲的 int 类型 Channel
ch := make(chan int)
// 启动一个 Goroutine 发送数据
go func() {
fmt.Println("发送数据 42")
ch <- 42 // 发送,阻塞直到有接收者
fmt.Println("数据已发送完成")
}()
// 主 Goroutine 接收数据
fmt.Println("等待接收数据...")
value := <-ch // 接收,阻塞直到有发送者
fmt.Printf("接收到数据: %d\n", value)
// 输出顺序:
// 等待接收数据...
// 发送数据 42
// 接收到数据: 42
// 数据已发送完成
}
💡 关键理解:无缓冲 Channel 要求发送和接收同时就绪,否则会阻塞等待。这提供了一种天然的同步机制。
2.2 有缓冲 Channel(异步 Channel)
有缓冲 Channel 在缓冲区未满时发送不阻塞,在缓冲区非空时接收不阻塞。
go
func main() {
// 创建一个容量为 2 的缓冲 Channel
ch := make(chan int, 2)
// 发送两个数据(不会阻塞)
ch <- 1
ch <- 2
fmt.Println("两个数据已发送,缓冲区已满")
// 再发送第三个数据会阻塞(缓冲区已满)
// ch <- 3 // 死锁!
// 接收数据
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
2.3 关闭 Channel 与遍历
发送方可以主动关闭 Channel,接收方可以通过 v, ok := <-ch 检测 Channel 是否已关闭。更常见的是使用 for range 自动遍历直到 Channel 关闭。
go
func producer(ch chan<- int) { // 只发送 Channel
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 发送完成后关闭
}
func main() {
ch := make(chan int, 3)
go producer(ch)
// 使用 range 遍历 Channel,直到它被关闭
for v := range ch {
fmt.Println("接收到:", v)
}
fmt.Println("Channel 已关闭,遍历结束")
}
⚠️ Channel 操作规范:
只能由发送方关闭 Channel,接收方不应关闭。
关闭已关闭的 Channel 会引发 panic。
向已关闭的 Channel 发送数据会引发 panic。
从已关闭的 Channel 接收数据会返回零值(v, ok := <-ch 中 ok 为 false)。
2.4 Channel 的方向(只读/只写)
在函数签名中可以限制 Channel 的方向,提高类型安全:
go
// 只发送 Channel
func sendOnly(ch chan<- int, value int) {
ch <- value
// <-ch // 编译错误:不能从只发送 Channel 接收
}
// 只接收 Channel
func receiveOnly(ch <-chan int) {
value := <-ch
// ch <- 1 // 编译错误:不能向只接收 Channel 发送
fmt.Println(value)
}
func main() {
ch := make(chan int)
go sendOnly(ch, 42)
receiveOnly(ch)
}
三、select 多路复用
select 语句用于同时等待多个 Channel 操作,类似于 switch,但每个 case 是一个 Channel 操作:
go
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- "来自 ch1 的消息"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "来自 ch2 的消息"
}()
// 等待最先到达的消息
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("超时 3 秒")
}
// 输出: 来自 ch2 的消息(因为 ch2 先到达)
}
3.1 select 的典型用途
(1)超时控制
go
func doWork() string {
time.Sleep(2 * time.Second)
return "工作完成"
}
func main() {
ch := make(chan string)
go func() {
ch <- doWork()
}()
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(1 * time.Second):
fmt.Println("超时!工作未在 1 秒内完成")
}
// 输出: 超时!工作未在 1 秒内完成
}
(2)非阻塞的 Channel 操作
使用 default 分支实现非阻塞操作:
go
func main() {
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("接收到:", v)
default:
fmt.Println("没有数据可接收,不阻塞")
}
select {
case ch <- 42:
fmt.Println("发送成功")
default:
fmt.Println("缓冲区已满,不阻塞")
}
}
四、互斥锁:保护共享资源
虽然 Channel 是 Go 推荐的并发通信方式,但某些场景下共享内存加锁仍然更合适(如缓存、状态管理)。sync.Mutex 提供了互斥锁,sync.RWMutex 提供了读写锁(允许并发读,互斥写)。
go
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := &Counter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
fmt.Println(counter.Value()) // 1000(安全)
}
五、实战:并发工作池(Worker Pool)
工作池模式是一种经典的并发设计模式,用于限制同时执行的并发任务数量,避免资源耗尽:
go
package main
import (
"fmt"
"sync"
"time"
)
// 任务结构体
type Task struct {
ID int
Data string
}
// 工作池
type WorkerPool struct {
numWorkers int
taskQueue chan Task
wg sync.WaitGroup
}
// 创建工作池
func NewWorkerPool(numWorkers int, queueSize int) *WorkerPool {
return &WorkerPool{
numWorkers: numWorkers,
taskQueue: make(chan Task, queueSize),
}
}
// 启动工作池
func (wp *WorkerPool) Start() {
for i := 0; i < wp.numWorkers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
}
// 工作 Goroutine
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
for task := range wp.taskQueue {
fmt.Printf("Worker %d 开始处理任务 %d: %s\n", id, task.ID, task.Data)
time.Sleep(100 * time.Millisecond) // 模拟处理时间
fmt.Printf("Worker %d 完成任务 %d\n", id, task.ID)
}
}
// 提交任务
func (wp *WorkerPool) Submit(task Task) {
wp.taskQueue <- task
}
// 关闭工作池(等待所有任务完成)
func (wp *WorkerPool) Stop() {
close(wp.taskQueue)
wp.wg.Wait()
}
func main() {
// 创建 3 个工作 Goroutine,队列容量 10
pool := NewWorkerPool(3, 10)
pool.Start()
// 提交 10 个任务
for i := 0; i < 10; i++ {
pool.Submit(Task{ID: i, Data: fmt.Sprintf("data-%d", i)})
}
// 等待所有任务完成
pool.Stop()
fmt.Println("所有任务已完成")
}
六、小结
Goroutine:go 关键字启动轻量级协程,栈大小仅 2KB,GMP 调度模型高效管理。
Channel:类型化的通信管道,无缓冲(同步)和有缓冲(异步)两种类型。
select:多路复用 Channel 操作,支持超时控制和默认分支(非阻塞)。
WaitGroup:等待一组 Goroutine 完成,Add、Done、Wait 三方法组合使用。
Mutex:sync.Mutex 互斥锁,sync.RWMutex 读写锁,保护共享资源。