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

4.7 KiB
Raw Blame History

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
go
Go
错误处理
goroutine
channel
context
并发
intermediate
backend.go.basics-types
4
backend.go.scenario.batch-third-party-calls
backend.go.interview
1
3
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 的用途、如何发现数据竞争。