ESC
输入关键词搜索文章
目录

Go 入门 05

goroutine · channel · context
让并发代码能启动,也能结束
go启动任务
chan传递数据
ctx取消超时
Chapter 05
并发不是“同时写更多代码”

Go 的并发模型建立在 goroutine 和 channel 之上。goroutine 是由 Go 运行时调度的轻量执行单元;channel 是 goroutine 之间传递值的通道。官方文档强调 Go 显式支持并发编程,而官方博客用 pipeline、context 等模式说明并发代码还必须处理失败、取消和资源释放。

定义:并发与并行

并发是让多个任务在结构上可以独立推进;并行是多个任务在物理 CPU 上真正同时执行。Go 的写法关注并发结构,运行时和硬件决定其中多少工作能并行。

Goroutine
用 go 启动 goroutine

在函数调用前加 go,就会启动一个新的 goroutine 执行它。主 goroutine 结束时,程序也会结束,所以示例里通常需要同步手段等待后台任务完成。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var waitGroup sync.WaitGroup

    waitGroup.Add(1)
    go func() {
        defer waitGroup.Done()
        fmt.Println("work in goroutine")
    }()

    waitGroup.Wait()
}
sync.WaitGroup 用于等待一组 goroutine 完成。标准库文档把 sync 定位为基础同步原语;更高层的同步通常优先通过 channel 和通信完成。
Channel
用 channel 传递结果

channel 让 goroutine 之间通过发送和接收值协作。发送使用 channel <- value,接收使用 value := <-channel

func square(number int, results chan<- int) {
    results <- number * number
}

func main() {
    results := make(chan int)

    go square(4, results)

    result := <-results
    fmt.Println(result)
}

channel 可以带方向标注。chan<- int 表示只能发送 int 的 channel,<-chan int 表示只能接收 int 的 channel。方向标注能让函数签名更清楚。

channel 不是队列魔法

无缓冲 channel 的发送和接收会互相等待。如果没有对应接收者,发送方会阻塞;如果没有发送者,接收方会阻塞。

Pipeline
pipeline:把并发拆成阶段

Go 官方博客把 pipeline 描述为由 channel 连接的一系列阶段。每个阶段接收上游数据,执行处理,再把结果发给下游。这个模型适合流式处理和分阶段计算。

func generate(numbers ...int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for _, number := range numbers {
            output <- number
        }
    }()
    return output
}

func square(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for number := range input {
            output <- number * number
        }
    }()
    return output
}

func main() {
    for result := range square(generate(2, 3, 4)) {
        fmt.Println(result)
    }
}

这个例子里,每个阶段都负责关闭自己创建的输出 channel。下游用 range 读取 channel,直到它被关闭。

Context
context:把取消、超时传下去

很多真实服务不是“任务一定完成”,而是“用户取消了请求”“超时了”“上游已经失败”。context 包用于跨 API 边界传递截止时间、取消信号和请求范围的值。标准库文档要求需要 Context 的函数显式把它作为第一个参数,通常命名为 ctx

func fetchWithTimeout(ctx context.Context, url string) error {
    request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return fmt.Errorf("create request: %w", err)
    }

    response, err := http.DefaultClient.Do(request)
    if err != nil {
        return fmt.Errorf("send request: %w", err)
    }
    defer response.Body.Close()

    return nil
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    if err := fetchWithTimeout(ctx, "https://go.dev"); err != nil {
        log.Println(err)
    }
}
defer cancel() 很重要。标准库文档说明,忘记调用 CancelFunc 会让相关资源停留到父 context 被取消;go vet 也会检查控制流上是否使用了 CancelFunc。
Practice
初学者的并发边界

初学阶段不要把所有循环都改成 goroutine。并发适合 I/O 等待、独立任务、流水线处理和后台监听;不适合为了“看起来高级”而拆碎简单逻辑。判断是否需要并发,可以问三个问题:任务是否能独立推进?结果如何汇合?取消和错误如何传回?

问题如果答不上来建议
谁等待 goroutine 结束?可能泄漏后台任务使用 WaitGroup、channel 或 context 管理生命周期
错误传给谁?失败会被静默丢弃通过 error channel、返回值或上层聚合处理
如何取消?超时后仍继续占资源传入 context,并监听 ctx.Done()

表 1:写并发代码前必须回答的生命周期问题。

复习速查

  • goroutine:用 go 启动的轻量执行单元。
  • channel:goroutine 之间传递值的通道。
  • WaitGroup:等待一组 goroutine 完成。
  • pipeline:由 channel 连接的一系列处理阶段。
  • context:跨 API 传递取消、超时和请求范围数据。

参考来源