Go 语言的 channel
是并发编程的核心组件之一,提供了一种安全、高效的方式来在线程(goroutine)间进行通信和同步。本文将从基础到进阶,结合实际案例,深入解析 channel
在生产环境中的经典应用场景。
1. 初识 Channel
在 Go 语言中,channel
主要用于协程间的通信,它可以传递数据,并且可以阻塞读取和写入,以实现同步机制。
go
ch := make(chan int) // 无缓冲通道
chBuffered := make(chan int, 5) // 有缓冲通道,容量为5
- 无缓冲通道 (阻塞):必须在有
goroutine
读取时才能写入,反之亦然。 - 有缓冲通道 (非阻塞):允许写入一定数量的数据,即使没有
goroutine
读取。
2. 生产者-消费者模式
生产者-消费者模式是 channel
最常见的应用之一。
go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func producer(ch chan<- int, wg *sync.WaitGroup, id int) {
defer wg.Done()
for i := 0; i < 5; i++ {
num := rand.Intn(100)
fmt.Printf("Producer %d produced: %d\n", id, num)
ch <- num // 发送数据到通道
time.Sleep(time.Millisecond * 500)
}
}
func consumer(ch <-chan int, wg *sync.WaitGroup, id int) {
defer wg.Done()
for num := range ch {
fmt.Printf("Consumer %d consumed: %d\n", id, num)
time.Sleep(time.Second) // 模拟处理时间
}
}
func main() {
rand.Seed(time.Now().UnixNano())
ch := make(chan int, 10) // 带缓冲通道
var wg sync.WaitGroup
for i := 1; i <= 2; i++ {
wg.Add(1)
go producer(ch, &wg, i)
}
for i := 1; i <= 3; i++ {
wg.Add(1)
go consumer(ch, &wg, i)
}
wg.Wait()
close(ch) // 关闭通道
}
关键点:
- 生产者
producer
负责向channel
发送数据。 - 消费者
consumer
读取channel
并处理数据。 close(ch)
关闭通道,避免消费者阻塞。sync.WaitGroup
确保所有协程执行完毕。
3. 使用 select
进行超时控制
如果某个 channel
长时间未返回数据,我们可以使用 select
和 time.After
进行超时控制。
go
package main
import (
"fmt"
"time"
)
func worker(ch chan int) {
time.Sleep(3 * time.Second) // 模拟耗时操作
ch <- 42
}
func main() {
ch := make(chan int)
go worker(ch)
select {
case res := <-ch:
fmt.Println("Received:", res)
case <-time.After(2 * time.Second): // 2秒超时
fmt.Println("Timeout!")
}
}
关键点:
time.After(2 * time.Second)
作为超时信号,防止channel
长时间阻塞。- 适用于网络请求超时、长时间等待任务的场景。
4. 使用 channel
控制并发数
在实际开发中,我们可能需要限制 goroutine
的并发数。例如,限制最多 3 个任务并发执行。
应用场景 | 并发数推荐 | 方法 |
---|---|---|
CPU 密集型任务 | runtime.NumCPU() 或 NumCPU * 1.5 |
限制 goroutine 数,减少切换开销 |
I/O 密集型任务 | runtime.NumCPU() * 10 或更多 |
由于 I/O 阻塞,可以增加 goroutine |
批量任务处理 | 由 channel 控制并发数 |
令牌池(Semaphore) |
超时任务或动态调整 | context.WithTimeout |
任务超时取消 |
负载自适应 | 监控 runtime.NumGoroutine() |
结合 Prometheus 动态调整 |
go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func worker(id int, ch chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
<-ch // 获取令牌
fmt.Printf("Worker %d is running...\n", id)
time.Sleep(time.Duration(rand.Intn(3)+1) * time.Second) // 模拟任务执行
fmt.Printf("Worker %d done\n", id)
ch <- struct{}{} // 归还令牌
}
func main() {
rand.Seed(time.Now().UnixNano())
const maxWorkers = 3
ch := make(chan struct{}, maxWorkers) // 限制并发数
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Add(1)
go worker(i, ch, &wg)
}
for i := 0; i < maxWorkers; i++ {
ch <- struct{}{} // 初始化令牌
}
wg.Wait()
}
关键点:
channel
容量限制了同时运行的goroutine
数量。<-ch
获取令牌,ch <- struct{}{}
归还令牌。- 适用于限流、线程池控制等场景。
5. context
+ channel
实现任务取消
context
可与 channel
结合,实现超时取消功能。
go
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopping...\n", id)
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
for i := 1; i <= 2; i++ {
go worker(ctx, i)
}
time.Sleep(5 * time.Second) // 让 worker 运行一段时间
}
关键点:
context.WithTimeout
设定超时时间,自动触发ctx.Done()
。select
监听ctx.Done()
以终止任务。
总结
场景 | 关键点 |
---|---|
生产者-消费者 | channel 作为消息队列,close 关闭通道 |
超时控制 | select + time.After 实现超时 |
并发控制 | channel 限制最大并发数 |
context 任务取消 |
ctx.Done() 监听取消信号 |
channel
是 Go 并发编程的基石,合理使用能够极大提升程序的健壮性和性能🚀。