Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。

示例代码

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8// 一个模拟耗时任务的函数
 9func worker(id int, jobs <-chan int, results chan<- int) {
10    for j := range jobs {
11        fmt.Printf("Worker %d started job %d\n", id, j)
12        time.Sleep(time.Second) // 模拟耗时 1 秒
13        fmt.Printf("Worker %d finished job %d\n", id, j)
14        results <- j * 2 // 将结果发送回 results 通道
15    }
16}
17
18func main() {
19    // 创建两个通道:任务通道和结果通道
20    // 设置缓冲区大小为 10,防止阻塞
21    jobs := make(chan int, 10)
22    results := make(chan int, 10)
23
24    // 启动 3 个 Goroutine (Worker)
25    // 它们会并发地从 jobs 通道抢任务做
26    for w := 1; w <= 3; w++ {
27        go worker(w, jobs, results)
28    }
29
30    // 发送 5 个任务
31    for j := 1; j <= 5; j++ {
32        jobs <- j
33    }
34    close(jobs) // 关闭任务通道,告知 Worker 没有新任务了
35
36    // 接收并打印结果
37    for a := 1; a <= 5; a++ {
38        <-results
39    }
40    
41    fmt.Println("All jobs finished!")
42}

关键点解释

Goroutine (协程)

  • 使用 go 关键字 即可启动一个新的协程。例如 go funcName()
  • 协程比线程更轻量,启动成本极低,Go 运行时可以在少数几个 OS 线程上调度成千上万个 Goroutine。
  • main 函数本身也是一个 Goroutine。当 main 结束时,所有其他 Goroutine 都会被强制终止。

Channel (通道)

  • 通道是 Goroutine 之间通信的管道。遵循“不要通过共享内存来通信,而要通过通信来共享内存”的原则。
  • 定义make(chan Type, capacity)
  • 发送ch <- value
  • 接收value := <-ch
  • 关闭close(ch)。关闭后不能再发送,但可以继续接收已有的数据。
  • Rangefor x := range ch 可以不断从通道读取数据,直到通道被关闭。

Select 选择器

select 语句是 Go 并发编程中的重要工具,用于同时等待多个通道操作。它的语法类似于 switch,但专门用于通道。

基本语法

 1select {
 2case msg1 := <-ch1:
 3    fmt.Println("Received from ch1:", msg1)
 4case msg2 := <-ch2:
 5    fmt.Println("Received from ch2:", msg2)
 6case ch3 <- value:
 7    fmt.Println("Sent to ch3")
 8default:
 9    fmt.Println("No channel ready")
10}

工作原理

  • 随机选择:如果多个 case 同时就绪,select 会随机选择一个执行。
  • 阻塞等待:如果没有 case 就绪且没有 defaultselect 会阻塞直到某个 case 可以执行。
  • 非阻塞:如果有 default 分支,当所有 case 都未就绪时,会立即执行 default

常见使用场景

===== 1. 超时控制

1select {
2case result := <-ch:
3    fmt.Println("Got result:", result)
4case <-time.After(3 * time.Second):
5    fmt.Println("Timeout! Operation took too long")
6}

===== 2. 非阻塞接收

1select {
2case msg := <-ch:
3    fmt.Println("Received:", msg)
4default:
5    fmt.Println("No message available")
6}

===== 3. 同时监听多个通道

 1for {
 2    select {
 3    case msg := <-ch1:
 4        fmt.Println("From ch1:", msg)
 5    case msg := <-ch2:
 6        fmt.Println("From ch2:", msg)
 7    case <-quit:
 8        fmt.Println("Quit signal received")
 9        return
10    }
11}

===== 4. 实现通道的优先级

 1// 优先处理 highPriority 通道
 2select {
 3case msg := <-highPriority:
 4    handleHighPriority(msg)
 5default:
 6    select {
 7    case msg := <-lowPriority:
 8        handleLowPriority(msg)
 9    default:
10        // 都没有消息
11    }
12}

注意事项

  • 如果所有 case 都是 nil 通道,且没有 defaultselect 会永久阻塞。
  • select 不会按顺序检查 case,而是随机选择就绪的 case。
  • 空的 select {} 会永久阻塞,可用于防止 main 函数退出。

小结

Go 将并发变得像普通函数调用一样简单。通过组合 Goroutine 和 Channel,我们可以轻松构建出高并发、低耦合的系统。

练习题

  1. 编写一个程序,启动两个 Goroutine,一个打印数字 1-5,另一个打印字母 A-E。观察输出顺序是否固定。
  2. 尝试修改示例代码,去掉通道缓冲(即 make(chan int)),看看程序的行为有何变化(可能会导致死锁,思考为什么)。

相关阅读