03-反馈流
反馈流——发现问题后回退到哪
反馈流问的是:发现问题后,回到哪一步重新执行? 它是十条流里最特殊的一条——在函数级几乎不存在,却随着组织规模的成长,一路长到「跨系统回退、回到用户」的最强形态。反馈流的强弱,直接等于软件组织的成熟度。本文追踪这条流从无到有的完整生长过程。
一、定义:什么是真正的反馈流先给反馈流下一个精确的定义:
1发现问题后,回到问题产生的那一步,重新执行后续流程。
注意关键词是「回退」,而不是「跳过」或「终止」:
123跳过 出错就跳过当前,继续下一个 (不是反馈)终止 出错就停下不干了 (不是反馈)回退 回到产生问题的那一步,从那里重来 (这才是反馈)
反馈流和错误流的区别:
12错误流:发现错误后怎么办(处理)反馈流:发现错误后回到哪(回退)
没有错误流就没有反馈流——但反过来,有了错误流也不一定有反馈流。反馈流是错误流之上更「高级」的能力。
二、单函数:几乎不存在函数内部发现了错误,能做什么?
1234567函数内部发现错误 ↓能做什么? → 返回错误码?(本函数没有返回值) ...
02-错误流
错误流——出错时怎么办
错误流问的是:程序出错时怎么办? 它和控制流、反馈流是一条绳上的三股线——控制流决定正常时往哪走,错误流决定异常时往哪走,反馈流决定发现错后回到哪。错误流最容易被写乱:不是错误处理得越多越好,而是每层只处理自己能处理的,处理不了的往上交。本文追踪错误流从头到尾的演化。
一、起点:函数内部出错错误流最原始的形态,发生在一个函数内部。函数发现错误时,选择有限:
1234567错误发生在函数内部 ↓函数可以选择: 1. 容忍(打印标记继续) 2. 返回错误码(本函数没有返回值,做不到) 3. 抛异常(C 语言没有) 4. 崩溃(最差选择)
12if (argv[i] == NULL) printf("(null)"); // 容忍:不崩溃,打印标记继续
函数级错误流的核心限制:函数能做的错误处理方式有限——最多返回错误码或打印日志,无法「回到上一步重试」。
还有一个隐蔽的问题:函数可能根本没处理某个错误(比如 argc 为负数时的行为未定义)。错误流的起点,就已经暴露了「谁该为错误负责」这个问题的模糊性。
二、单文 ...
01-控制流
控制流——程序走向由谁决定
控制流问的是一个最朴素的问题:程序在每一个分支点往哪走?由谁决定? 这个问题看似简单,却随着程序组织的演化不断变换答案。从单函数到多系统,控制权像接力棒一样,从「数据」手里传到「调用者」手里,再传到「接口」手里,最终分成「框架化 / 接口化」两条路。本文追踪这条流从头到尾的演化。
一、起点:一个分支点程序一旦出现判断,控制流就诞生了:
1234if (argv[i] == NULL) printf("(null)");else printf("%s", argv[i]);
在这个点,程序要「选择」。控制流关心的就是:这个选择由谁做出?
答案是:数据。
12argv[i] == NULL → 走第一条argv[i] != NULL → 走第二条
不是代码自己选了路,是数据告诉代码走哪条路。这是控制流最原始、最本质的形态。
二、单函数:数据决定,调用者掌握参数在一个函数内部,控制流由两样东西决定:
1函数内部的控制流由「参数 + 数据」决定
以 print_args 为例:
123 ...
05-多系统协作的十条流
多系统协作的十条流
当程序增长到多个子系统——每个子系统有自己的生命周期、自己的线程、自己的状态——十条流最终成型。启动流变成了「编排」,执行流变成了「跨系统调用」,状态流出现了「同步」,反馈流拥有了「跨系统回退」的能力。这是十条流最完整的形态。
一、从多层到多系统多层系统的每一层内部通常是「被动的」——业务层被入口层调用,基础设施层被业务层调用。但当程序足够复杂时,某些部分需要自己运行——它们有独立的线程、独立的生命周期、独立的状态。
123456789101112args_app/├── main.c ← 入口层:解析命令行,组装系统├── cli/ ← 入口子系统:CLI 解析│ ├── cli_parser.h/c├── format/ ← 业务子系统:格式化│ ├── formatter.h/c├── utils/log/ ← 全局工具框架化子系统:日志(有自己的线程)│ ├── log_system.h/c├── file_io/ ...
04-多层系统的十条流
多层系统的十条流
当文件进一步增加,代码开始按职责分层——有些文件负责「接受外部输入」,有些负责「处理业务逻辑」,有些负责「调用操作系统」。分层让十条流发生了最大的一次质变:反馈流终于拥有了真正的「回退重试」能力;用户流程第一次有了明确的边界;控制流开始在层之间分配。
一、从多文件到分层上一篇的多文件结构是平面的——三个文件之间没有「层次」的概念。现在加入更多功能,文件开始自然地分层:
123456args/├── main.c ← 入口层:接收命令行,调用业务层├── parse_args.h/c ← 业务层:解析参数├── format.h/c ← 业务层:格式化输出├── file_api.h/c ← 基础设施层:库接口的原子化封装(读写文件)└── utils/log.h/c ← 全局工具层:写日志(横切,不属于三层)
三层结构:
12345入口层:main.c ↓ 调用业务层:parse_args.c, format.c ↓ 调用基础设施层:file_api(原子化接口,封装库提供的文件 ...
03-多文件的十条流
多文件的十条流
当代码从一个文件拆成多个文件时,最关键的变化不是「文件变多了」,而是接口出现了。头文件定义了函数签名,实现了「能做什么」和「怎么做」的分离。十条流在这个阶段发生了质变——控制流第一次有了明确的「边界」,反馈流终于可以跨文件回退(启动流仍未形成,链接顺序是编译期问题,见第二节)。
一、从一个文件到多个文件上一篇的 args.c 把 main、print_args、format_arg 写在一个文件里。现在拆开:
1234567891011// format.hvoid format_arg(char *buf, size_t size, int index, const char *value);// format.c#include "format.h"void format_arg(char *buf, size_t size, int index, const char *value) { if (value == NULL) snprintf(buf, size, "arg[%d] = (null)& ...
02-单文件多函数的十条流
单文件多函数的十条流
当一个文件里不再只有一个函数,而是有若干个函数互相调用时,十条流开始发生变化。执行流不再是单棵树,而是多棵树组成的调用图;错误流第一次有了「传播」的可能;反馈流仍然很弱,但已经萌芽。
一、从一个函数到多个函数上一篇的 print_args 只有一个函数。现在把它拆成多个:
123456789101112131415161718192021222324// file: args.c// 函数 1:格式化单个参数void format_arg(char *buf, size_t size, int index, const char *value) { if (value == NULL) snprintf(buf, size, "arg[%d] = (null)", index); else snprintf(buf, size, "arg[%d] = %s", index, value);}// 函数 2:打印所有参数void print_args(int ar ...
01-单函数的十条流
单函数的十条流
一切从一个函数开始。在所有复杂的软件系统之前,程序只是若干条指令顺序执行。当这些指令被组织成一个函数,十条流就已经同时存在了——只是有几条还很微弱,甚至尚未诞生。这一篇用一个真实的小函数,把十条流全部展开。
一、从一条指令到一个函数最简单的程序:
1printf("hello");
只有一条指令。没有分支,没有循环,没有函数调用。十条流在这里几乎全部退化成一个点——指令执行了,结果出现了,结束。
加一个判断:
1234if (argc > 1) printf("hello %s", argv[1]);else printf("hello");
有了分支,程序不再只走一条路。控制流开始出现——程序在某个点要「选择」。
加一个循环:
12for (int i = 0; i < argc; i++) printf("arg[%d] = %s\n", i, argv[i]);
有了循环,同一段代码可以被执行多次。执行流不再是线性的。
把这些组织成一个函数:
1 ...
06-Protocol
Protocol——协议数据结构有什么
Protocol 回答的问题是:「收发的字节长什么样?」 它被逼出来的原因是「裸字节没法用,每个调用者都要自己猜」。这一篇讲它有什么。关键结论:Protocol 是数据结构,不是组件——它是「消息长什么样」的一份定义。
一、它解决什么问题网络收发的是原始字节:
1收到:0x00 0x00 0x00 0x10 0x00 0x00 0x00 0x02 ...
调用者需要知道「这 4 字节是长度、接下来 4 字节是命令、后面是数据」——每个调用者都要自己猜、自己定义。版本一改,所有地方都要跟着改。
Protocol 被逼出来的原因:把「消息长什么样」的定义统一起来,只写一份。
二、它有什么(就是一份 struct 定义)Protocol 的全部内容,就是一个数据结构:
12345struct Message { uint32_t length; // 消息长度 uint32_t command; // 命令类型 uint8_t payload[];// 消息体};
1它有什么:只有「定义」,没有「 ...
05-Buffer
Buffer——缓冲区有什么
Buffer 回答的问题是:「收发的字节先存哪?」 它被逼出来的原因是「TCP 是流式协议,一次收不到整条消息」。这一篇讲它有什么——职责、内部结构、边界、异常、形态。注意:Buffer 是无状态工具,不是有状态组件。
一、它解决什么问题TCP 是流式协议,不是消息协议:
1234一次 recv 可能收到: → 半条消息(消息还没发完) → 一条半消息(两条粘在一起) → 恰好一条(运气好)
调用者不能假设「一次 recv = 一条消息」。需要一个地方先把字节攒起来,攒够一条消息再处理。这就是 Buffer 被逼出来的原因。
12接收:字节先存进 Buffer,攒够一条消息再取出处理发送:待发的数据先排进 Buffer,再逐步写出
二、它有什么(内部结构)Buffer 是「数据 + 算法 + 接口」,但无状态:
12345678910111213class Buffer { // —— 数据 —— std::vector<char> data_; // 字节存储 size_t re ...
