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

示例代码

package main

import (
    "fmt"
    "time"
)

// 一个模拟耗时任务的函数
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d started job %d\n", id, j)
        time.Sleep(time.Second) // 模拟耗时 1 秒
        fmt.Printf("Worker %d finished job %d\n", id, j)
        results <- j * 2 // 将结果发送回 results 通道
    }
}

func main() {
    // 创建两个通道:任务通道和结果通道
    // 设置缓冲区大小为 10,防止阻塞
    jobs := make(chan int, 10)
    results := make(chan int, 10)

    // 启动 3 个 Goroutine (Worker)
    // 它们会并发地从 jobs 通道抢任务做
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 发送 5 个任务
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // 关闭任务通道,告知 Worker 没有新任务了

    // 接收并打印结果
    for a := 1; a <= 5; a++ {
        <-results
    }

    fmt.Println("All jobs finished!")
}

关键点解释

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,但专门用于通道。

基本语法

select {
case msg1 := <-ch1:
    fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("Received from ch2:", msg2)
case ch3 <- value:
    fmt.Println("Sent to ch3")
default:
    fmt.Println("No channel ready")
}

工作原理

  • 随机选择:如果多个 case 同时就绪,select 会随机选择一个执行。

  • 阻塞等待:如果没有 case 就绪且没有 defaultselect 会阻塞直到某个 case 可以执行。

  • 非阻塞:如果有 default 分支,当所有 case 都未就绪时,会立即执行 default

常见使用场景

1. 超时控制
select {
case result := <-ch:
    fmt.Println("Got result:", result)
case <-time.After(3 * time.Second):
    fmt.Println("Timeout! Operation took too long")
}
2. 非阻塞接收
select {
case msg := <-ch:
    fmt.Println("Received:", msg)
default:
    fmt.Println("No message available")
}
3. 同时监听多个通道
for {
    select {
    case msg := <-ch1:
        fmt.Println("From ch1:", msg)
    case msg := <-ch2:
        fmt.Println("From ch2:", msg)
    case <-quit:
        fmt.Println("Quit signal received")
        return
    }
}
4. 实现通道的优先级
// 优先处理 highPriority 通道
select {
case msg := <-highPriority:
    handleHighPriority(msg)
default:
    select {
    case msg := <-lowPriority:
        handleLowPriority(msg)
    default:
        // 都没有消息
    }
}

注意事项

  • 如果所有 case 都是 nil 通道,且没有 defaultselect 会永久阻塞。

  • select 不会按顺序检查 case,而是随机选择就绪的 case。

  • 空的 select {} 会永久阻塞,可用于防止 main 函数退出。

小结

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

练习题

  1. 编写一个程序,启动两个 Goroutine,一个打印数字 1-5,另一个打印字母 A-E。观察输出顺序是否固定。

  2. 尝试修改示例代码,去掉通道缓冲(即 make(chan int)),看看程序的行为有何变化(可能会导致死锁,思考为什么)。


相关阅读