阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:24:57 +08:00
1 parent 0bae9916a9
commit 5eb67c5d08
65 files changed
+6457 -77

No files matched your search

+123
View File
@@ -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 的用途、如何发现数据竞争。