阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
0bae9916a9
commit
5eb67c5d08
65 files changed
+6457
-77
No files matched your search
@@ -0,0 +1,123 @@
|
||||
---
|
||||
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 的用途、如何发现数据竞争。
|
||||
Reference in new issue
Block a user