4.7 KiB
4.7 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.go.errors-concurrency | 错误处理和并发:goroutine、channel、context | lesson | backend |
|
|
intermediate |
|
4 |
|
|
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:
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。
易错点
- goroutine 泄漏:goroutine 阻塞在没人接收的 channel 上永远不退出。每个 goroutine 都要想清楚“它什么时候结束”,配合
context或关闭 channel。 - 向已关闭的 channel 发送会 panic;关闭的责任在发送方,多个发送方时用
WaitGroup等全部结束再关闭。 - 循环变量:Go 1.22 起每次循环都是新变量,旧版本里在 goroutine 中直接用循环变量会全部拿到最后一个值。
- map 不是并发安全的:多个 goroutine 同时读写会直接崩溃(fatal error),用
Mutex或sync.Map。 - 把 context 存进结构体:context 应该随调用传递,不要长期保存。
- 吞掉错误:
_ = f()要有理由;记录日志和返回错误二选一,不要两个都做(会重复打印)。
面试常问
见 Go 面试题:GMP 调度、channel 的阻塞规则、context 的用途、如何发现数据竞争。