Files
zhuangang/topics/backend/go/02-errors-concurrency.md
T

124 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.go.errors-concurrency
title: 错误处理和并发:goroutine、channel、context
type: lesson
domain: backend
stack: [go]
tags: [Go, 错误处理, goroutine, channel, context, 并发]
level: intermediate
prerequisites: [backend.go.basics-types]
est_hours: 4
related: [backend.go.scenario.batch-third-party-calls, backend.go.interview]
week: [1, 3]
order: 2
---
# 错误处理和并发:goroutine、channel、context
## 解释说明
**错误处理**
- 错误是普通的值。用 `fmt.Errorf("读取配置: %w", err)` **包装**错误,保留原始错误并加上下文;调用方用 `errors.Is(err, ErrNotFound)` 判断是不是某个错误,用 `errors.As(err, &target)` 取出某种类型的错误。
- 只在真正不可恢复的情况下用 `panic`(例如程序启动时配置缺失);HTTP 服务里用中间件 `recover`,防止一个请求的 panic 让整个进程退出。
**并发**
- **goroutine**:`go f()` 启动一个轻量级“线程”(初始栈几 KB,由 Go 运行时调度到系统线程上),可以轻松开成千上万个。
- **channel**:goroutine 之间传递数据的管道。无缓冲 channel 收发双方要同时就绪;有缓冲 channel 满了才阻塞发送。“不要通过共享内存来通信,而要通过通信来共享内存。”
- **select**:同时等待多个 channel,哪个先就绪处理哪个;常用来实现超时和取消。
- **sync**:`WaitGroup` 等待一组 goroutine 结束;`Mutex` / `RWMutex` 保护共享数据;`Once` 只执行一次。
- **context**:在调用链上传递**取消信号、超时和请求范围的值**。HTTP 请求进来时创建,往下传给数据库查询、外部调用;客户端断开或超时,下游全部及时停止。约定:`ctx` 作为函数的第一个参数。
## 案例
- 一个接口要同时查用户信息、订单列表、优惠券三个下游服务:三个 goroutine 并发查询,整体用 `context.WithTimeout` 限制 2 秒,任一失败就取消其他的(`errgroup` 的典型用法)。
- 消息消费者:从队列取消息、交给固定数量的 worker 处理(worker pool),服务关闭时通过 `context` 通知 worker 处理完手上的消息再退出。
## 代码示例
一个带超时和取消的 worker pool:
```go
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
var ErrInvalid = errors.New("无效的任务")
func process(ctx context.Context, n int) (int, error) {
if n < 0 {
return 0, fmt.Errorf("处理 %d: %w", n, ErrInvalid) // 包装错误
}
select {
case <-time.After(time.Duration(n) * 10 * time.Millisecond): // 模拟耗时操作
return n * n, nil
case <-ctx.Done(): // 超时或被取消
return 0, ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
jobs := make(chan int)
type result struct {
n, v int
err error
}
results := make(chan result)
var wg sync.WaitGroup
for w := 0; w < 3; w++ { // 3 个 worker
wg.Add(1)
go func() {
defer wg.Done()
for n := range jobs { // jobs 关闭后循环结束
v, err := process(ctx, n)
results <- result{n, v, err}
}
}()
}
go func() {
for _, n := range []int{1, 2, -1, 3, 50} {
jobs <- n
}
close(jobs) // 发送方负责关闭
}()
go func() { wg.Wait(); close(results) }() // 全部 worker 结束后关闭结果通道
for r := range results {
switch {
case errors.Is(r.err, ErrInvalid):
fmt.Println(r.n, "参数错误:", r.err)
case errors.Is(r.err, context.DeadlineExceeded):
fmt.Println(r.n, "超时")
default:
fmt.Println(r.n, "=>", r.v)
}
}
}
```
运行:`go run main.go`,再用 `go run -race main.go` 检查数据竞争。完整的生产写法(并发上限、单次超时、部分失败)见 [examples/batch-fetch](examples/batch-fetch/)。
## 易错点
- **goroutine 泄漏**:goroutine 阻塞在没人接收的 channel 上永远不退出。每个 goroutine 都要想清楚“它什么时候结束”,配合 `context` 或关闭 channel。
- **向已关闭的 channel 发送会 panic**;关闭的责任在发送方,多个发送方时用 `WaitGroup` 等全部结束再关闭。
- **循环变量**:Go 1.22 起每次循环都是新变量,旧版本里在 goroutine 中直接用循环变量会全部拿到最后一个值。
- **map 不是并发安全的**:多个 goroutine 同时读写会直接崩溃(fatal error),用 `Mutex` 或 `sync.Map`。
- **把 context 存进结构体**:context 应该随调用传递,不要长期保存。
- **吞掉错误**:`_ = f()` 要有理由;记录日志和返回错误二选一,不要两个都做(会重复打印)。
## 面试常问
见 [Go 面试题](interview.md):GMP 调度、channel 的阻塞规则、context 的用途、如何发现数据竞争。