Go 的并发方法是其最独特和最受欢迎的特性之一。Go 引入了 goroutinechannel,无需依赖传统的线程模型,就能让编写并发代码变得轻而易举。[1]

Goroutine:轻量级并发

创建 goroutine 非常简单,只需在函数调用前加上 go 关键字:[2]

func main() {
    go sayHello()
    go fetchData("https://api.example.com/data")
    time.Sleep(time.Second)
}

与 OS 线程不同,goroutine 被多路复用到较少的 OS 线程上。Go 运行时处理调度,允许数千甚至数百万个 goroutine 并发运行而不会出现问题。[3]

Go 并发可视化

Channel:Goroutine 之间的通信

如果 goroutine 是参与者,那么 channel 就是连接它们的消息总线。Channel 提供类型安全的通信和同步:[4]

ch := make(chan string)

go func() {
    result := doWork()
    ch <- result  // 发送到 channel
}()

response := <-ch  // 从 channel 接收

这种模式消除了一整类 bug。对于基本的生产者-消费者场景,你不再需要互斥锁和条件变量——channel 为你处理同步。

Select 语句

当你有多个 channel 时,select 可以让你同时等待所有 channel:[5]

select {
case msg1 := <-ch1:
    fmt.Println("从 channel 1 收到消息:", msg1)
case msg2 := <-ch2:
    fmt.Println("从 channel 2 收到消息:", msg2)
case <-time.After(time.Second):
    fmt.Println("超时")
}

实战示例:并发爬虫

这是一个真实的例子——并发网页爬虫,同时获取多个页面:

func fetchAll(urls []string) []string {
    results := make(chan string, len(urls))
    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, _ := http.Get(u)
            body, _ := io.ReadAll(resp.Body)
            results <- string(body)
        }(url)
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    var output []string
    for r := range results {
        output = append(output, r)
    }
    return output
}

结论

Go 的并发模型成功源于其理念:通过共享内存来通信。与其担心锁和互斥锁,不如通过 channel 传递数据。

这种方法从简单脚本到生产系统都适用。Google、Dropbox 和 Twitch 等公司使用 Go,正是因为其并发模型使复杂的并行系统变得易于理解。

下次你需要处理多个并发操作时,考虑使用 goroutine 和 channel。未来的你会感谢现在的你。