Go 入门 05
Go 的并发模型建立在 goroutine 和 channel 之上。goroutine 是由 Go 运行时调度的轻量执行单元;channel 是 goroutine 之间传递值的通道。官方文档强调 Go 显式支持并发编程,而官方博客用 pipeline、context 等模式说明并发代码还必须处理失败、取消和资源释放。
定义:并发与并行
并发是让多个任务在结构上可以独立推进;并行是多个任务在物理 CPU 上真正同时执行。Go 的写法关注并发结构,运行时和硬件决定其中多少工作能并行。
在函数调用前加 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 让 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 的发送和接收会互相等待。如果没有对应接收者,发送方会阻塞;如果没有发送者,接收方会阻塞。
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 包用于跨 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。
初学阶段不要把所有循环都改成 goroutine。并发适合 I/O 等待、独立任务、流水线处理和后台监听;不适合为了“看起来高级”而拆碎简单逻辑。判断是否需要并发,可以问三个问题:任务是否能独立推进?结果如何汇合?取消和错误如何传回?
| 问题 | 如果答不上来 | 建议 |
|---|---|---|
| 谁等待 goroutine 结束? | 可能泄漏后台任务 | 使用 WaitGroup、channel 或 context 管理生命周期 |
| 错误传给谁? | 失败会被静默丢弃 | 通过 error channel、返回值或上层聚合处理 |
| 如何取消? | 超时后仍继续占资源 | 传入 context,并监听 ctx.Done() |
表 1:写并发代码前必须回答的生命周期问题。
复习速查
- goroutine:用
go启动的轻量执行单元。 - channel:goroutine 之间传递值的通道。
- WaitGroup:等待一组 goroutine 完成。
- pipeline:由 channel 连接的一系列处理阶段。
- context:跨 API 传递取消、超时和请求范围数据。
参考来源
- Ajmani, S. Go Concurrency Patterns: Pipelines and cancellation. go.dev/blog/pipelines
- Ajmani, S. Go Concurrency Patterns: Context. go.dev/blog/context
- The Go Authors. Package context. pkg.go.dev/context
- The Go Authors. Package sync. pkg.go.dev/sync