🏠 总目录📚 本教程 附录B · 技术栈对照
📑 本页目录(点开跳转)

附录B · 技术栈对照:换个栈,哪些变了、哪些没变

📖 不写 Python 的人从这里进;写 Python 的人拿它确认「哪些是概念、哪些只是 FastAPI 的叫法」。

01–15 章末尾各有一张「换个栈怎么对应」小表,一共 15 张。这里把它们按概念族重排成一份总对照,并补上原表缺的两块:Java / Spring(企业里最常见的那个后端栈,原表一次没出现过)和前端的 React / Vue(原表只在 09、10 两章出现过)。

⚠️ 每一格背后的「为什么」留在各章,这里不重讲 —— 每一族都标了它出自哪一章,看不懂某一格就回去看那一章。


🗓️ 先声明一次,后面不再重复

这份表里的库名会过期,判据不会。

发现名字对不上时,拿最左边那一列去搜 —— 那一列是概念,它不动;右边四列是叫法,它们每年都在动。

所以这里刻意不写版本号、不写性能对比、不写「现在大家都在用 X」。那三样是全表里唯一必然会腐、而且删掉完全不影响价值的部分。会过期的具体东西单独标 🗓️。


🎯 一句话

四个栈都得干同一件事,区别只在「它把哪一半藏起来了」—— 藏起来不等于替你做完,而且藏得越深,你越晚发现它其实没做。


🧭 零、这份表和「一行 API 都不写」那条立场怎么共处

站里 智能体工程教程 16b 写过一句很硬的话:「因为框架会过期而原理不会,这章只写映射和坑,一行 API 都不写。」

⚠️ 那说的是 Agent 框架(LangChain 那一类),立场是对的 —— 那层封装薄、换得快、封的还是你本来就该懂的机制。但全栈不给代码没法学。 00 章第五节已经拍板过折中,原话三条:

做法 为什么
每段代码前先讲「为什么是这个形状」 形状不过期,API 会。你换到别的框架,找的就是同一个形状的东西
每章末尾一张「换个栈怎么对应」表 覆盖 Node 和 Go。目的不是教那两个栈,是让你看出哪些是概念、哪些只是这个框架的叫法
🗓️ 会过期的东西单独标 版本号、平台控制台路径 —— 尽量给「怎么自己查最新的」

这份附录就是第二条的产物,只是把 16 处分散的对照收成一处、并补了两个栈。它和 16b 不矛盾:16b 拒绝写的是「怎么调这个 API」,这里写的是「这个概念在你那个栈里叫什么」。 后者恰恰是 16b 自己在做的事 —— 它管那叫「一张对回机制的地图」。

💡 怎么用:不写 Python 的人,先在这里确认概念对得上,再回正文读原理,把代码当伪码看。


🧱 一、起一个 HTTP 服务

📍 出自 03 章(另见 02 章

概念 Python / FastAPI Node / TypeScript Go Java / Spring
路由 + 请求校验 装饰器 + Pydantic 模型 app.post + Zod schema 🗓️ http.HandleFunc + struct tag @PostMapping + DTO 上的 Bean Validation 注解
等待不占线程 async def + 事件循环 天生单线程事件循环,IO 默认异步 goroutine,运行时自动挪走阻塞的 四个栈里唯一要你选的:MVC 一请求一线程(阻塞)vs WebFlux 事件循环 🗓️
对应的坑 async def 里写同步调用 处理函数里跑 CPU 密集活,同样卡死 忘了把 context 传下去,取消传不到 线程池被慢的上游占满,队列越排越长
每请求要做的事挂哪 Depends(...) 中间件 + ctx 上挂对象 中间件 + context.Context Filter / HandlerInterceptor
统一错误形状 @app.exception_handler 错误中间件(放最后) 包一层 handler 返回 error @RestControllerAdvice + @ExceptionHandler

不变的是什么「每个请求都要做、却不属于任何一个业务函数的事,得有一个统一的地方挂」 —— 鉴权、限流、拿连接、生成 trace_id、识别租户。这个概念不会过期,Depends 这个名字会。四个栈里只有 FastAPI 把它叫「依赖注入」,另外三个都用中间件干同一件事。

⚠️ Spring 这一列有个名字陷阱:Spring 确实有一个真正的依赖注入容器,但它注的是启动期的单例 bean,和 FastAPI 的 Depends每个请求跑一遍)不是同一件事。「每请求要做的事」在 Spring 里仍然走 Filter / Interceptor —— 名字撞了,位置没撞。

「等待不占线程」那一行值得多看两眼:Python 用 async def、Node 天生如此、Go 交给运行时 —— 这三个都是语言层就定死的。只有 Java 把它做成了一个你要自己选的部署形态(阻塞的 MVC 还是反应式的 WebFlux),🗓️ 而虚拟线程又让「阻塞写法」重新变得便宜。⭐ 所以 03 章那条判据在 Java 里换了个形态但没消失:你仍然要知道自己写的这段代码,等待的时候占不占着一条执行流。


🌊 二、分批往外写响应

📍 出自 05 章(另见 01 章

概念 Python / FastAPI Node / TypeScript Go Java / Spring
分批往外写 StreamingResponse(生成器) 多次 res.write() / 返回 ReadableStream w.Write() + ⭐ Flusher.Flush() MVC:SseEmitter.send();WebFlux:返回 Flux 并声明 text/event-stream
⚠️ 谁在缓冲 框架不缓冲,代理会 同左;compression 中间件要按 MIME 排除 ResponseWriter 自己就带缓冲,不 Flush 就攒着 每次 send() 即发;⚠️ 但异步请求有一个默认超时,长回答会被它切断 🗓️
感知客户端断开 await request.is_disconnected() req.on('close') / AbortSignal ctx.Done()r.Context() SseEmitteronError / onTimeout 回调;WebFlux 的取消信号
收尾必做 finally 里关上游 finally / close 事件里关上游 defer 里关上游 onCompletion 回调里关上游

⭐⭐ 不变的是什么「分批往外写」在每个栈里都要显式表达,没有一个栈是「你只管 yield,剩下的它替你办」。 区别只在它把哪一半藏起来了:FastAPI 藏进 StreamingResponse 这个名字里、Go 明写成 Flusher.Flush()、Spring 藏进一个你从没设过的超时配置里。

⚠️ Spring 那个默认超时是这一族最容易咬人的格子:它不是「不流式」,是流到一半被框架自己掐断,而本地开发时你的回答都短,一次都不会触发。⭐ 判据和 04 章的读取超时完全同源:这个数要大于「一次合法的静默能有多长」,不是由手感决定。

有一件事四列写的是同一个词:⚠️ 反向代理的缓冲。Nginx 的 proxy_buffering off、云平台入口网关的默认值 —— 那一层和你用什么语言毫无关系05 章 · 14 章)。换栈重写时,人们通常记得搬业务逻辑,忘了搬这一件。


🗄️ 三、数据库与连接池

📍 出自 06 章(向量那一层见 07 章

概念 Python Node / TypeScript Go Java / Spring
驱动 asyncpg / psycopg 🗓️ pg / postgres.js 🗓️ pgx 🗓️ JDBC 驱动;反应式走 R2DBC 🗓️
查询层 SQLAlchemy(async) Prisma / Drizzle / Knex 🗓️ pgx(+ sqlc 生成代码)/ GORM 🗓️ Spring Data JPA / jOOQ / 直接写 SQL 的模板类 🗓️
迁移工具 Alembic 🗓️ Prisma Migrate / Drizzle Kit 🗓️ golang-migrate / goose / Atlas 🗓️ Flyway / Liquibase 🗓️
连接池 引擎上的 pool_size pgPool({ max }) pgxpoolMaxConns HikariCP 的 maximum-pool-size
⚠️ 事务边界看得见吗 async with session.begin(): 看得见 $transaction([...]) 看得见 tx, _ := pool.Begin(ctx) 看得见 💀 @Transactional 是个注解,起止看不见

不变的是什么:四个生态的迁移工具都长成「一串有序脚本 + 一张记录跑到哪了的表」,连接池都有一个「最大连接数」旋钮,执行计划都是同一句 EXPLAIN ANALYZE那是数据库的能力,跟语言无关)。换栈要重学的只有语法。

最后一行是这份附录里最值得单独记的一格。 06 章那条判据是「事务里只放数据库自己的操作,任何网络调用都在事务之外」,而它在 Spring 里最危险 ——

另外三个栈里,事务是一段你能用眼睛框出来的代码,你把 LLM 调用写进去,with 块或 Begin/Commit 之间会明显变长。@Transactional 把边界变成了「这个方法的全部」:你在这个方法里加一行调模型,事务就默默从几毫秒变成几秒到几十秒,代码的形状上什么都没变,评审时也看不出来

⚠️ 连接池默认值这一行别照抄数字。 每个池的默认值都不一样,而且都在变(🗓️ 用前查你那个池的文档)。⭐ 06 章给的判据才是不动的那个:pool_size 是一道除法 —— 数据库能承受的连接总数 ÷ 应用进程数 ÷ 还有谁在连同一个库。四个栈里这道除法一模一样。

向量那一层几乎不受栈影响:建表、<=> 运算符、索引选型、先过滤还是先检索 —— 全是 SQL 层和数据建模的事。各栈都有一个把向量类型接进驱动的小库(🗓️ 名字和各家 ORM 的支持程度都在变,用前先查包管理器里的真实包名)。⭐ 这是选 pgvector 的一个附带好处:你的知识不绑定在某个 SDK 上。


⏳ 四、后台任务与队列

📍 出自 11 章

概念 Python Node / TypeScript Go Java / Spring
请求内的后台活 BackgroundTasks 响应后继续 / setImmediate go func() @Async + 一个线程池
⚠️ 这一档的通病 进程重启就丢 一样会丢 一样会丢 一样会丢
数据库当队列 自己写 + SKIP LOCKED pg-boss 🗓️ River 🗓️ 原生 SQL 写 SKIP LOCKED;或现成的 JDBC 调度库 🗓️
专用队列 Celery / RQ / arq 🗓️ BullMQ 🗓️ asynq 🗓️ 消息中间件的 Spring 集成(AMQP / Kafka)🗓️
定时任务 APScheduler 🗓️ node-cron 🗓️ robfig/cron 🗓️ @Scheduled

不变的是什么:名字全不一样,四件事完全一样 —— 一张带状态的表、一条原子认领语句、一个由输入算出的幂等键、一套带抖动和上限的重试。这四件事不过期,上面那些包名会。

一个好例子:Java 生态里那些「把任务存进数据库」的调度库,打开一看就是一张带状态的表 + 一条原子认领语句 —— 11 章那四件事,一件不少。它不是「Java 的做法」,它就是那四件事换了个包装。

⚠️ @Scheduled 是这一族最会骗人的一格:它写起来像一行注解,看上去比另外三个栈都省事 —— 但多副本部署时每一台都会到点触发,于是「每小时结一次账」变成了每小时结 N 次。修法是加一把分布式锁(🗓️ 有现成的库),⭐ 而判据还是 11 章那条:一个由输入算出的幂等键。 另外三个栈的定时任务也有同一个问题,只是它们的写法不会让你误以为框架管了。


🔑 五、配置与密钥

📍 出自 13 章

概念 Python Node / TypeScript Go Java / Spring
读环境变量 os.environ process.env os.Getenv System.getenv / @Value 注入
加载 .env python-dotenv 🗓️ dotenv / 🗓️ 新版 Node 自带 --env-file godotenv 🗓️ ⭐ 没有内建 .env,用 profile + 分环境的配置文件
配置对象 + 校验 pydantic-settings 🗓️ zod / envalid 🗓️ envconfig / viper 🗓️ @ConfigurationProperties + @Validated
缺配置时 抛异常,进程退出 throwprocess.exit(1) log.Fatal 容器装配期就抛,服务根本起不来
日志脱敏 自定义 Formatter 日志库的 redact 选项 🗓️ slogReplaceAttr 日志框架的自定义 converter 🗓️

不变的是什么:四个栈形状完全一样 —— 一处声明全部配置 → 启动时校验 → 校验不过就别启动 → 打印时永远不吐密钥。 会过期的是包名,不会过期的是这四步。

第三步「校验不过就别启动」在 Spring 里是最彻底的:配置校验失败发生在容器装配期,进程直接起不来,根本没有「先跑起来、跑到那行代码才发现少个 Key」的中间状态。⚠️ 但这不是 Spring 独有的能力,是它默认帮你做了 —— 另外三个栈要你自己把校验放在启动路径上,放晚了就退化成运行期才炸

⚠️ Spring 这一列有个 grep 陷阱值得单独说:它会把环境变量名和配置项名当成同一个命名空间来匹配(把分隔符和大小写的差异抹平),好处是同一份配置既能写在文件里也能用环境变量盖掉;⚠️ 代价是你在代码里搜那个环境变量名会一无所获 —— 代码里出现的是配置项的名字,环境变量名是推算出来的。🗓️ 具体的匹配规则查文档,⭐ 但排查思路不变:从配置项名反推,不要指望全文搜索能找到那根线。


📈 六、日志与指标

📍 出自 15 章

概念 Python Node / TypeScript Go Java / Spring
结构化日志 logging + 自定义 Formatter / structlog 🗓️ pino 🗓️ log/slog 日志门面 + 一个 JSON 编码器 🗓️
请求上下文 contextvars AsyncLocalStorage context.Context必须手动一路传 MDC(挂在线程上)
⚠️ 什么时候会悄悄断 扔进线程池(run_in_executor 少数没接住上下文的库 不会断 —— 编译器逼你把它写进签名 换线程就断@Async、任何线程池、反应式链路
断了怎么补 contextvars.copy_context() 一般框架已替你接好 —— 不需要 在跳转处把 MDC 拷过去 🗓️
指标与追踪 OpenTelemetry SDK(四边通用) 同;⭐ 另有零改代码的字节码探针 🗓️

⭐⭐ 不变的是什么三个栈用隐式上下文(写起来舒服),一个栈用显式传递(写起来啰嗦)。隐式的那三个都有同一个 bug:换执行载体时上下文断掉,而且不报错。

15 章那个 run_in_executor 的坑不是 Python 的毛病,是隐式上下文这个设计的固有代价

断在哪 症状
Python 扔进线程池 日志里 request_id 变成默认值 -
Java @Async / 任何线程池 / 反应式 MDC 里那个 id 变成空
Node 少数没接住上下文的第三方库
⭐ Go 不会断 代价是每个函数签名都得多一个参数

⭐⭐ 三边的症状一模一样:日志里的 id 悄悄变成默认值,不抛异常、不打警告、本地单人开发时也看不出来。修法的形状也一模一样:在跳转的那一刻,把上下文整个拷过去。 Go 那种「什么都要手传」的啰嗦,买的就是这一条 —— 15 章的原话是「永远不会出现『第三层忘了传』」。

⚠️ 还有一个断点四个栈全都躲不掉:⭐⭐ 进了队列就必须写进 payload。 队列里的任务是另一个进程、可能另一台机器、可能几分钟之后才跑,任何进程内的上下文机制都帮不上忙(15 章第三节)。这一格没有语言差异,只有做没做。


🧪 七、把上游换成假的

📍 出自 03 章dependency_overrides)和 04 章FakeLLM

概念 Python Node / TypeScript Go Java / Spring
把模型客户端换成假的 04 章的 FakeLLM + dependency_overrides 传进去一个假实现 声明成 interface,测试传假实现 测试里换掉容器里那个 bean 🗓️
起一个假的上游 HTTP 服务 拦 HTTP 传输层的库 🗓️ 拦 HTTP 的库 🗓️ net/http/httptest 标准库自带 独立的打桩服务库 🗓️
不起真服务打自己的接口 TestClient(app) 直接调 app 的 fetch handler / 打桩客户端 🗓️ httptest.NewRecorder() 框架自带的 MVC / WebFlux 测试客户端
真起一个数据库跑集成测试 容器化测试依赖的库 🗓️(四边同名同形)

不变的是什么:这四行都不是测试技巧,是设计的结果。它们能成立的唯一前提是 03 章那条规矩:api/ 里不许直接出现模型 SDK 的名字。 业务代码对着你自己的接口写,「换成假的」才是一行的事;对着某个 SDK 写,换什么栈都换不掉。

04 章FakeLLM 值钱不在于省钱,在于它能故意抛 429、故意超时、故意返回半截 JSON —— ⭐ 重试和降级路径只有这样才测得到。 这一点四个栈完全一样。

⚠️ 第二行 Go 是唯一把「假上游」放进标准库的,⭐ 但那只省了一次安装:形状四家一模一样 —— 起一个真的 HTTP 服务,让被测代码真的走一遍网络栈,而不是 mock 掉客户端对象。⭐ 这个区别很重要:mock 客户端测不出超时怎么分、重试白名单对不对,而那两件恰恰是 04 章整章在讲的东西。


📦 八、容器与部署

📍 出自 14 章

概念 Python / FastAPI Node / TypeScript Go Java / Spring
先拷的「依赖清单」 requirements.txt package.json + lock 文件 go.mod + go.sum 构建文件(Maven / Gradle 的那份)
装依赖 pip install -r npm ci go mod download 构建工具的「只下依赖」那条命令 🗓️
多阶段的收益 中(去掉编译器和缓存) 中(去掉 devDependencies) 最大:产物是单个二进制,运行阶段可以用几乎空的基础镜像 中:运行阶段只要一个 JRE + 一个 jar 🗓️
日志不缓冲 PYTHONUNBUFFERED=1 默认就不缓冲 默认就不缓冲 默认就不缓冲
优雅关闭 uvicorn 处理 SIGTERM,lifespan 里收尾 server.close() + 自己监听 SIGTERM http.Server.Shutdown(ctx) ⭐ 一个配置开关就打开 🗓️
两个健康检查端点 自己写 /healthz/readyz 自己写 自己写 ⭐ 框架自带 liveness / readiness 🗓️

不变的是什么层缓存的排序法则、多阶段构建、.dockerignore、两个健康检查端点问的是两个问题、关机四步 —— 一条都不属于 Python。会过期的只是基础镜像的版本号和包管理器的名字。

Spring 是四个栈里替你做得最多的一个:优雅关闭一个开关、两个健康检查端点开箱就有。⚠️ 但它替你做的是端点,不是语义 —— 14 章那四步里最关键的是 ①先摘流量、②再停止接受新连接,顺序反了就永远等不完;而框架不知道负载均衡器什么时候把你摘下去,也不知道你正在流式的那个请求还要跑两分钟。

⭐⭐ 所以这一族的真正判据和栈无关,一共两条① 宽限期必须 ≥ 你最长的一次请求(⚠️ 各层默认值都在几十秒量级 🗓️,而一次长回答跑两分钟很正常 —— 不改的话每次发版都切断一批正在流式的用户); /readyz 要反映真实状态,并给「摘流量」这个动作留几秒。 框架给你端点,不会替你决定这两个数。

⭐ 另外「一个容器跑一个进程」和反向代理那一层,四列写的是同一句话 —— 那是容器和 HTTP 的规矩,不是某个语言的。


🎨 九、前端:原生 JS / React / Vue

📍 出自 09 章10 章

⚠️ 这一族的三列不是「三个后端栈」,是同一个浏览器上的三种写法 —— 所以判据也不同:不是「哪个更好」,是 ⭐ 「哪几行框架真能帮上忙」

概念 原生 JS(09 / 10 章) React Vue
收流 fetch + getReader() 一模一样 一模一样
拼帧与解码 手写 buffer + TextDecoder(⚠️ 要 stream: true 一模一样 一模一样
状态 一个数组 + 手动 render() useState / useReducer ref / reactive
渲染 手写 createElement JSX,框架自动 diff 模板,框架自动 diff
⭐ 只更新最后一条 手动改那个 DOM 节点 key 稳定时框架自动局部更新 同左
停止生成 AbortController 一模一样 一模一样
滚动跟随 手写 following 状态 ⚠️ 一样要手写 ⚠️ 一样要手写
Markdown 净化 自己接一个净化库 🗓️ ⚠️ 一样要接 ⚠️ 一样要接

⭐⭐ 不变的是什么八行里只有三行是框架真能帮上忙的(状态、渲染、只更新最后一条),另外五行换成任何框架都还在。 收流、拼帧、AbortController、滚动跟随、净化 —— 这些是浏览器和 HTTP 层的问题,以及「模型输出是不可信输入」这件事,不是框架能替你解决的。

这也正是 10 章值得单独一章的原因:AI 应用前端的难点不在 UI 层,在流式协议不可信内容上。⚠️ 别把这张表读成「所以不用框架」—— 09 章不用框架是因为它的读者是后端(目标是「做出不丢人的界面」);本来就写前端的人,00 章的路线 C 让你直接从 10 章开始


🧷 十、横着看这九张表,四条规律

前面每一族的「⭐ 不变的是什么」都只管那一族。把九族横过来看,会浮出四条更大的:

① ⭐⭐ 每个栈都得干同一件事,区别只在「它把哪一半藏起来了」

FastAPI 把 flush 藏进 StreamingResponse 这个名字里、Go 把它明写成 Flusher.Flush()、Spring 把流式的超时藏进一个你从没设过的配置项里。藏起来不等于替你做完 —— ⚠️ 藏得越深,你越晚发现它其实没做,而且往往是在生产环境、在最长的那个回答上才发现。

② ⭐⭐ 隐式的东西写起来舒服,代价永远是同一个:换执行载体时它悄悄断掉,而且不报错

contextvars 断在线程池、MDC 断在异步注解、@Transactional 让事务边界从代码里消失。三个例子的共同点是「不报错」 —— 它们不会在 CI 里红,只会让你的日志少一根线、让你的连接池在并发上来时耗尽。Go 那种什么都要手传的啰嗦,买的就是这一条。

③ ⭐ 框架能替你做的是端点和语法,替不了的是顺序和判据

Spring 自带两个健康检查端点,但「先摘流量再停止接受新连接」还是你的事;四家的迁移工具都是「有序脚本 + 一张记录表」,但扩展—收缩五步法是你的事;四家都有队列库,但幂等键怎么算是你的事。⭐ 凡是「顺序」和「阈值」,翻遍文档也找不到 —— 因为它取决于你的业务。

④ ⭐ 越靠近数据库和浏览器,栈的差别越小

EXPLAIN ANALYZESKIP LOCKED<=>、Nginx 的 proxy_bufferingfetch + getReader()AbortController、SSE 的帧格式 —— 这些格子里,四列(或三列)写的是同一个词。⭐ 换栈要重学的东西,比你以为的少得多:真正要重学的是最上面那一层(路由怎么写、依赖怎么注),越往下越通用。

⭐⭐ 这份表最终想让你得到的一句话: 看到一个陌生的栈时,别问「它的 API 怎么写」,问「这九件事它做了哪些、藏起来了哪些、一件没做的是哪些」。 前一个问题的答案半年就过期,后一个问题的答案跟着你换栈。


🔗 这一页连到哪里

去哪 为什么
00 · 怎么用这份教程 第五节是这份附录的立项依据(技术栈折中三条的原话),也讲了不写 Python 的人该怎么用这套教程
附录A · 速查 那一页答「该选哪个 / 别踩哪个」,这一页答「换个栈它叫什么」—— 两张查阅页,两个问题
03 · 后端骨架 想知道「每请求要做的事得有地方挂」为什么值得单独一层,回这一章第四节
05 · 流式输出 第二族每一格背后的原理都在这里,尤其是「谁在缓冲」那一行
06 · 关系数据库 @Transactional 那个坑的完整版:为什么「别把 LLM 调用包在事务里」是 AI 应用最贵的数据库错误
15 · 可观测性 第六族那张「什么时候会悄悄断」的表,源头是这一章第三节那段可跑代码
智能体工程 16b · 框架这层皮 同一个方法在 Agent 框架上的版本:把框架的名字一个个对回机制。⭐ 那一章更狠 —— 它一行 API 都不写

✅ 检查点

  1. 这份表最左边那一列和右边四列,哪一列会过期?发现名字对不上时该怎么办?
  2. ⭐ 「分批往外写响应」这件事,四个栈里有没有哪个是「你只管产出,剩下的它替你办」的?
  3. FastAPI 的 Depends 和 Spring 的依赖注入,是不是同一件事?
  4. ⭐⭐ contextvars 扔进线程池会断、Java 的 MDC 换线程会断、Go 不会断 —— 这三件事说明了什么共同的东西?代价各是什么?
  5. 「别把 LLM 调用包在事务里」这个坑,为什么在 Spring 里比另外三个栈更危险?
  6. 前端那张表八行里,换成 React 或 Vue 能真正省掉的是哪几行?剩下的为什么省不掉?
  7. Spring 自带两个健康检查端点,是不是就不用管 14 章那四步关机顺序了?
  8. ⭐ 为什么说「越靠近数据库和浏览器,栈的差别越小」?举两个四列写同一个词的例子。
👀 答案
  1. 右边四列(叫法)会过期,最左边那一列(概念)不会。 发现库名对不上时,拿概念那一列去搜。这也是这份表不写版本号、不写性能对比、不写「现在大家都在用 X」的原因 —— 那是唯一必然会腐、删掉还不影响价值的部分。
  2. ⭐⭐ 没有一个。 四个栈都要显式表达这件事,区别只在它把哪一半藏起来了:FastAPI 藏进 StreamingResponse 这个名字里、Go 要你手动 Flusher.Flush()ResponseWriter 默认就带缓冲)、Spring 每次 send() 虽然即发,但异步请求有个默认超时会把长回答切断。⚠️ 而且四个栈之外还站着一层反向代理的缓冲,那一层和语言完全无关。
  3. 不是。 Spring 确实有一个真的 DI 容器,但它注的是启动期的单例 bean;FastAPI 的 Depends每个请求跑一遍的。「每请求要做的事」在 Spring 里仍然走 Filter / Interceptor —— 名字撞了,位置没撞。 四个栈里只有 FastAPI 把这件事叫「依赖注入」,另外三个都用中间件。
  4. ⭐⭐ 说明 「隐式上下文写起来舒服,代价永远是同一个:换执行载体时它悄悄断掉,而且不报错」。三边症状一模一样:日志里的 id 变成默认值(Python 里是 -),不抛异常、不打警告、本地单人开发看不出来。修法形状也一样:在跳转的那一刻把上下文整个拷过去(Python 是 copy_context())。Go 的代价是每个函数签名都要多一个参数,买到的是「永远不会出现第三层忘了传」。⚠️ 还有一个四个栈都躲不掉的断点:进了队列就必须写进 payload —— 那是另一个进程、可能另一台机器、可能几分钟之后。
  5. ⭐ 因为另外三个栈里事务是一段你能用眼睛框出来的代码async with session.begin(): / $transaction([...]) / BeginCommit),把慢调用塞进去会明显变长;而 @Transactional 把边界变成了「这个方法的全部」—— 你在方法里加一行调模型,事务就从几毫秒默默变成几秒到几十秒,代码形状什么都没变,评审也看不出来。判据不变:事务里只放数据库自己的快操作,任何网络调用都在事务之外。
  6. ⭐⭐ 只有三行:状态、渲染、以及「只更新最后一条」(key 稳定时框架自动局部更新)。另外五行换任何框架都还在:收流(fetch + getReader())、拼帧解码(⚠️ TextDecoderstream: true)、停止生成(AbortController)、滚动跟随、Markdown 净化。原因是它们不是 UI 层的问题 —— 前三个是浏览器和 HTTP 层的,后两个是「模型输出是不可信输入」带来的。⭐ 这正是 10 章值得单独一章的理由。
  7. 不是。 框架替你做的是端点,不是语义:它不知道负载均衡器什么时候把你摘下去,也不知道你正在流式的那个请求还要跑两分钟。判据仍然是两条 —— ① 宽限期必须 ≥ 你最长的一次请求(各层默认值常在几十秒量级,而长回答跑两分钟很正常,不改就每次发版切断一批用户);/readyz 要反映真实状态,并给「先摘流量」留几秒(⚠️ 摘流量必须在停止接受新连接之前,反过来就永远等不完)。
  8. ⭐ 因为那两头是共享的运行时:数据库和浏览器只有一个,各语言的库只是把请求转发给它们。⭐ 四列写同一个词的例子(任取两个):EXPLAIN ANALYZESKIP LOCKED<=> 运算符、Nginx 的 proxy_bufferingfetch + getReader()AbortController、SSE 帧格式。⭐ 真正要重学的是最上面那一层(路由怎么写、依赖怎么注、错误处理器挂在哪),越往下越通用 —— 所以换栈的成本比大多数人估的小得多。
打卡记录保存在你的浏览器里,首页能看到总进度