错误流——出错时怎么办
错误流问的是:程序出错时怎么办? 它和控制流、反馈流是一条绳上的三股线——控制流决定正常时往哪走,错误流决定异常时往哪走,反馈流决定发现错后回到哪。错误流最容易被写乱:不是错误处理得越多越好,而是每层只处理自己能处理的,处理不了的往上交。本文追踪错误流从头到尾的演化。
一、起点:函数内部出错
错误流最原始的形态,发生在一个函数内部。函数发现错误时,选择有限:
1 2 3 4 5 6 7
| 错误发生在函数内部 ↓ 函数可以选择: 1. 容忍(打印标记继续) 2. 返回错误码(本函数没有返回值,做不到) 3. 抛异常(C 语言没有) 4. 崩溃(最差选择)
|
1 2
| if (argv[i] == NULL) printf("(null)");
|
函数级错误流的核心限制:函数能做的错误处理方式有限——最多返回错误码或打印日志,无法「回到上一步重试」。
还有一个隐蔽的问题:函数可能根本没处理某个错误(比如 argc 为负数时的行为未定义)。错误流的起点,就已经暴露了「谁该为错误负责」这个问题的模糊性。
二、单文件多函数:错误第一次有了传播
拆出多个函数后,错误流增加了第一个新能力:错误可以在函数之间传播。
1 2 3 4 5
| format_arg 发现 argv[i] == NULL → 打印 (null) → 返回正常 print_args 继续循环 → 完全不知道 format_arg 内部发生了什么
|
这是单文件阶段错误流的典型模式:
1 2 3
| format_arg: 发现 NULL → 处理了(打印 (null))→ 返回正常 print_args: 不知道 format_arg 遇到了 NULL main: 不知道 print_args 内部发生了什么
|
文件级错误流的核心特征:错误传播是单向的——从被调用者到调用者。调用者可以选择忽略。
错误流在这里第一次暴露出它的两面性:传播是可能的,但吞掉也是可能的。 底层不返回错误码,上层就永远蒙在鼓里。
三、多文件:跨文件传播,每层可吞掉
跨文件后,错误传播路径变清晰了:
1 2 3 4 5
| format_arg 遇到格式化错误 → 返回错误码给 print_args → print_args 决定:跳过?打印错误信息?终止? → 返回错误码给 main → main 决定:退出?忽略?重试?
|
传播方向固定:从底层到上层,逐级传递。
1 2 3 4 5
| format.c: snprintf 返回 -1(格式化失败) ↓ 返回 -1 给 print.c print.c: 检查返回值,决定打印错误信息并跳过 ↓ print_args 继续循环 main.c: 无法知道(因为 print_args 没有返回错误码)
|
多文件错误流的核心变化:错误传播路径变得清晰,但每一层可以选择吞掉错误。 如果 print_args 不返回错误码,main.c 就完全不知道发生过格式化错误。
这个阶段确立了一条原则的雏形:错误要不要继续往上交,是每一层自己的决定。 但「每层该处理什么错误」这个更细的问题,要到分层后才真正展开。
四、多层:逐层上报,每层只处理自己能处理的
分层之后,错误流发生了最深刻的一次质变。错误有了层次:
1 2 3 4 5 6 7
| 基础设施层:file_io 写文件失败 → 返回错误码给业务层 业务层:parse_args 发现写入失败 → 决定:重试?跳过?终止? → 返回错误码给入口层 入口层:main 发现业务层失败 → 决定:打印错误信息?退出程序?
|
多层错误流的核心变化:错误传播有了层次。上层可以决定是否重试、是否降级、是否终止。
4a、异常处理流:每层的异常完全不同
分层后最关键的一个观察:每一层遇到的异常类型和处理方式完全不同,不应该混在一起处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 接口层(接受外部输入): 异常:参数缺失、参数类型错误、参数超出范围 处理:返回参数错误码,不往下传递 例如:用户传了负数的 argc → 返回 "invalid argument"
业务层(处理业务逻辑): 异常:业务规则违反、数据不一致、业务状态非法 处理:返回业务错误码,可能触发回退 例如:用户要删除不存在的文件 → 返回 "file not found"
算法层(纯计算): 异常:除以零、溢出、数组越界、精度丢失 处理:返回计算错误,不关心业务语义 例如:搜索算法在空数组上执行 → 返回 "empty input"
基础设施层(原子化接口): 异常:系统调用失败、资源耗尽、权限不足 处理:返回系统错误码(errno),不关心业务 例如:open() 返回 -1 → 返回 ENOENT
|
注意:业务层最初是没有原子化接口层的。 业务层直接使用其他库的接口(如 POSIX API、第三方库函数),配合数据结构和算法组成业务逻辑。原子化接口层是在「业务层的接口不断膨胀、职责不断混杂」后才被拆出来的——把系统调用、资源管理等底层操作从业务层中剥离。
异常处理流的三条分层原则:
1 2 3 4 5 6 7 8 9 10 11
| 1. 每层只处理自己能处理的异常 → 接口层处理参数错误,不处理文件不存在 → 基础设施层处理系统调用失败,不处理业务规则
2. 本层处理不了的异常,向上层传递 → 基础设施层遇到文件不存在 → 传递给业务层 → 业务层决定:重试?降级?终止?
3. 异常不应该跨层传播 → 接口层不应该直接处理基础设施层的异常 → 中间必须有业务层做翻译和决策
|
1 2 3 4 5 6 7 8 9 10 11
| 异常处理流的层次图:
接口层 参数异常 → 自己处理 其他异常 → 传递给业务层 ↓ 业务层 业务异常 → 自己处理 底层异常 → 翻译为业务错误 → 传递给接口层 ↓ 算法层 计算异常 → 返回错误给业务层 ↓ 基础设施层 系统异常 → 返回 errno 给业务层
|
五、多系统:主路径上报 + 旁路记录
多系统阶段,错误流出现了分支:
1 2 3 4 5 6 7
| file_ops: read() 失败 → 返回错误给 searcher searcher: 检查错误 → 决定:尝试备用文件?返回空结果? → 返回错误给 cli cli: 检查错误 → 决定:打印错误信息给用户?重试?
|
主路径:
1
| file_ops → searcher → cli → 用户(逐层上报)
|
但同时出现了旁路:
1 2 3
| searcher: 检查错误 → 调用 log.log_error("file read failed") ← 错误日志 → 返回错误给 cli
|
错误流的两条路径:
1 2
| 主路径:file_ops → searcher → cli → 用户(逐层上报) 旁路: searcher → log_system(记录错误日志,不影响主流程)
|
多系统错误流的核心变化:错误传播有了分支——主路径向上层报告,旁路向日志系统记录。
旁路的意义:错误既要上报给决策者(主路径),又要留下痕迹供事后排查(旁路)。这两件事解耦了——上报是「现在怎么办」,记录是「以后怎么查」。
六、错误流的五阶段演化总表
1 2 3 4 5 6 7
| 错误怎么处理 新出现的机制 ───────────────────────────────────────────────────────── 单函数 内部处理(容忍/返回码/崩溃) 错误处理手段有限 单文件 单向传播(底层吞掉,上层不知) 返回值传递错误码 多文件 跨文件传播,每层可吞掉 逐级上报路径 多层 逐层上报,每层处理自己的 异常分层 + 原子化接口层 多系统 主路径上报 + 旁路记录 错误日志旁路
|
一条主线贯穿始终:
1 2 3 4 5 6 7 8
| 错误流的问题 = 「谁该处理这个错误」
从「内部吞掉」→「单向传播」→「逐级上报」→「每层各管各的」→「上报+记录分离」 错误处理的成熟,就是「责任」被一层层厘清的过程。
原则不是「处理得越多越好」,而是: 每层只处理自己能处理的,处理不了的往上交, 同时留一条旁路记录痕迹,供事后排查。
|
七、错误流与其他线的关系
1 2 3 4 5 6
| 反馈流 错误流是反馈流的触发条件——先发现错,才能回退 单一职责 原子化接口层封装库接口 + 异常处理,正是错误流在接口层的落点 依赖关系 错误往上交,本质是「下层依赖上层决策」,依赖方向 = 错误传播方向 系统角色 每层异常类型不同(入口=参数、业务=规则、基础设施=系统),是角色的分界 拆解与组织 异常分层是「边界」在错误维度的体现——边界切对了,异常才分得清 七条流(各阶段) 本文是那条「错误流」列的纵向展开
|
一句话总结:错误流不是「出错就打日志」,而是「错误的责任怎么分层」——它追踪的是一条错误从发生到被正确处理(或被吞掉)的责任传递链。