踩坑记录
这里记录的全是真事:每一条都在生产或生产等价的验证里发生过,有具体文件、具体报错、具体时刻。写给 AI 的用意很直接 —— 你推测出的行为,八成已经被这些案例否定过一次。
索引:24 条故障
| # | 现象(一句话) | 类别 |
|---|---|---|
| 01 | 条件永假、分支从不执行、无任何报错 | 语义 |
| 02 | 三个文件同时编译失败,报错指向莫名的一行 | 语法 |
| 03 | 本该返回 400 的接口变成 500 | 错误处理 |
| 04 | HTTP 响应体开头多出一行日志,客户端解析失败 | Web |
| 05 | 逐像素画图要几秒 | 性能 |
| 06 | 同一段循环,别人跑 25 ms,你跑 6 秒 | 性能 |
| 07 | 写 17 MB 二进制时进程直接挂死 | IO |
| 08 | 改了 bytes 却全是 0,签名对不上 | IO |
| 09 | 字节查找「没找到」被当成「找到 0 号位」 | IO |
| 10 | 日志时间比实际早 8 小时 | 时间 |
| 11 | 定义一个同名函数后程序段错误 | 语义 |
| 12 | 列表拼接出来多了一个分隔符 | 语义 |
| 13 | 大整数运算结果变成 0 | 语义 |
| 14 | 读不存在的键,进程报 R1008 终止 | 语义 |
| 15 | --fast 编译失败不回落,解释轨却照跑 | 工具链 |
| 16 | import 的文件里定义的常量全是 null | 语义 |
| 17 | 每个请求首次连库慢 0.4 秒 | 数据库 |
| 18 | Web 接口偶发 500,日志只有一行「未定义变量」 | 部署 |
| 19 | 响应头只回来 Content-Type,其它全丢 | Web |
| 20 | systemd 托管的服务,日志里只有 Started | 部署 |
| 21 | 数组下标算出来是 0.99 导致取错元素 | 语义 |
| 22 | actor 的状态更新处报语法错 | 并发 |
| 23 | 读上传的 zip,内容莫名被截断 | IO |
| 24 | SQL 转义函数一调用就报参数错误 | 数据库 |
语义篇
01 · 条件永假,分支从不执行
现象:Mahesvara 的 md 站点把所有 public/ 下的静态资源返回 404,/css/style.css 请求失败,页面无样式;没有任何错误日志,服务一切「正常」。排查耗时约 40 分钟,最终靠逐条打印条件才定位。
根因:判断路径是否含 .. 的那行写成了 contains(p, "..") == 0。contains() 返回 bool,而 PuXian 里 false == 0 是 false(严格类型比较,不做隐式转换)⇒ 这个复合条件恒为假,整条分支成死代码。
修法:if spf != null and (not contains(p, "..")) and spf["is_dir"] == false:
== 0 / == 1 出现在条件里,先确认被比较的函数返回 bool 还是 int。02 · 一个保留字让三个文件同时编译失败
现象:send-code.px / phone-send-code.px / reset.px 同时报 E2001 语法错误:期望变量名,实际得到 from,报错位置在它们 import 的公共库第 478 行。
根因:公共库里写了 var from = ...(发信人地址)。from 是保留字。
修法:改名 sender。in 同样是保留字,不能当参数名。
11 · 自定义函数覆盖内建 → 段错误
现象:定义了 def env(name): 之后,程序在某些路径上直接段错误退出,无堆栈、无错误码。
根因:env 是内建。自定义同名函数遮蔽它之后,内部实现调用到自身形成无限递归,运行时不报错而是直接崩。
修法:不要用内建名做函数名/变量名。写之前花 3 秒确认:这个名词不在内置清单里。
12 · join 的参数顺序反了
现象:拼接列表得到 a,b,c, 或者直接报「join 分隔符需要 string」。
根因:PuXian 是 join(分隔符, 列表);Python 是 分隔符.join(列表)。参数顺序与直觉相反。
修法:join(",", parts)。
13 · 大整数静默回绕
现象:2 ** 64 输出 0;哈希/位运算拼出来的数偶尔对不上。
根因:整数是 64 位定长,溢出回绕不报错。
修法:超过 2^53 的中间结果改用字符串或分块处理;需要定长语义时显式取模。
14 · 缺键是运行时错误,不是 null
现象:d["missing"] 直接抛 运行时错误 [i_index]: R1008 键不存在,当前协程终止。
根因:[] 取值不做缺键兜底(旧文档里「返回 null」的说法已不成立)。
修法:if d.has(k): 或 if contains(d, k): 之后再取(没有 keys() 这个内建;要键列表用 d.keys())。写遍历外部数据的代码时,这条必须当成默认动作。
16 · import 文件的顶层赋值不执行
现象:在 cfg.px 顶层写了 let MAX = 100,主文件里用 MAX 得 null。
根因:被 import 的文件只贡献函数定义,顶层赋值语句不执行。
修法:常量声明放在主文件的全局区,或改成函数 def max_size(): return 100。
21 · 除法返回浮点,下标取整出错
现象:list[i / 2] 报类型错误,或者 to_lower(int(a / b)) 这类嵌套里出现奇怪行为。
根因:/ 返回 float;>>、&、int_to_bytes、下标都要求 int。
修法:一律 int(a / b);位运算前先确认两边都是 int。
错误处理篇
03 · 本该 400 的请求变成 500
现象:客户端传坏 JSON,接口返回 500,服务端日志只有一行 运行时错误 [i_call_builtin]: json: 无法解析。
根因:json_parse() 抛不可捕获错误,0.2.382 仍没有 try/catch,整个脚本死掉。
修法:json_parse_opt(x) + .is_err() / .ok()。同一模式适用于所有 *_opt 内建。
IO 篇
07 · write_all 超 16 MiB 挂死
现象:导出 42 MB 文件时进程卡死,CPU 不高、无输出、不退出。
根因:write_all 每次切「剩余全量」的大切片,超过 16 MiB 直接挂住。
修法:open("/dev/stdout","w") 后按 1 MB 分块 write() 循环。
08 · bytes 不可变,改了没生效
现象:拼 zip 的签名头,写完读出全是 0。
根因:bytes_set(b, i, v) 返回新对象,不改 b。忽略返回值等于什么都没做。
修法:b = bytes_set(b, i, v)。
09 · bytes_find 未命中返回 null
现象:判断「是否包含子串」时,未命中的分支走进了「找到」逻辑。
根因:未命中返回 null,不是 -1;且只接受 2 个参数(没有起点偏移)。
修法:var i = bytes_find(b, sub); if i != null:。
23 · read_file 遇 NUL 截断
现象:读上传的 zip / 图片,长度只有预期的一小段。
根因:read_file() 是文本语义,遇到 NUL 字节截断。
修法:二进制走 read_file_opt("/dev/stdin").ok() 或分块 read;写回用 write_bytes。
时间篇
10 · 日志时间早 8 小时
现象:time_format() 格式化出来的时间比本地时间早 8 小时;更隐蔽的一种:数据库队列的 next_at 永远在未来,队列一条也取不出来,且不报错。
根因:time_format() 输出 UTC;MySQL 连接若带 time_zone='+00:00',连接内 NOW() 是 UTC,而表默认值走服务器时区(CST),两者混用就差 8 小时。
修法:写库的时间统一显式 UTC_TIMESTAMP(),不做表默认值;展示层再做时区转换。
工具链与部署篇
15 · --fast 编译失败不回落,解释轨却照跑
现象(0.2.373–0.2.378):同一个脚本 px run 正常,px run --fast 报 R1001 未定义变量。
根因:那几版的编译缓存路径不解析 import(本地库与 stdlib 同样受影响)。0.2.379 已修,两条轨现在逐字节一致。
现在的新形态:--fast 解析 import 了,但严格。写错的 import(例如把 import std.strings 写成 import "strings.px")会直接 E3005 找不到模块 并不回落解释轨;同一脚本用 px run 只打一行警告、继续跑(因为该错 import 对应的函数恰好是内建)。
修法:把 --fast 当严格检查器用——它报错就是真有 import 写错,别去翻自己的变量名。要强制看解释轨行为:px run --interp x.px。
18 · Web 接口偶发 500,日志只有一行
现象:Ma 托管的 px 接口大多 38–76 ms,偶发 1.1 秒并伴零散 500;日志一行「找不到模块」。
根因:Ma 编译时只把「脚本所在目录」打包,import "../other/lib.px" 跨目录、软链跨目录都会缺;旧版 Ma 对缺失 import 只警告不失败,把坏产物缓存成「编译成功」,于是每次请求回落到解释轨。
修法:每个 pxapp 目录自包含(公共库放实体副本);Ma 0.22.4 起编译前预检跨目录 import、失败不落缓存。
19 · 响应头只剩 Content-Type
现象:px 脚本返回的自定义响应头除 Content-Type 外全部消失。
根因:Ma 的 px 响应头协议要求每行以 \n 结尾;用 \r\n 时解析器丢弃除首个头之外的项。
修法:拼响应头一律用 \n。
04 · print 污染 HTTP 响应
现象:接口返回体第一行变成 [send-code] 已发送…,客户端 JSON 解析失败。
根因:在 px_routes 脚本里 print() 写的就是 stdout,而 Ma 把 stdout 原样当 HTTP 响应(Status: 行起头)。
修法:日志写 stderr —— var f = open("/dev/stderr","w"); write(f, msg + "\n"); close(f)(没有 eprint 内建)。
20 · systemd 下日志失踪
现象:手动跑常驻程序日志正常,systemd 托管的只有一行 Started。
根因:封装日志函数用了 open("/dev/stderr","w"),而 systemd 下 fd 是 journal socket,打不开就静默 return。
修法:常驻进程用 print()(stdout 进 journal);Web 侧仍必须写 stderr(stdout 是响应体)。两者规则相反,别混。
数据库篇
17 · 首次连库 0.4 秒
现象:px 版接口每个请求首次连 MySQL 慢 400–450 ms(其中 tcp_connect 0 ms、SELECT 1 25 ms)。
根因:mysql 包(registry,纯 .px 协议驱动)在解释轨下纯解释开销就有 0.4 秒。
修法:走编译轨(px build / Ma mode: fast);或用连接池常驻进程。
24 · mysql_escape 只收一个参数
现象:mysql_escape(conn, s) 报参数数量错误。
根因:签名是 mysql_escape(s),不接收连接。
修法:防注入不要靠转义函数,用 mysql_query_params(conn, sql, [..]) 预处理绑定。
性能与并发篇
05 · 逐像素画图,解释轨要几秒
现象:132×44 的画布写 6400 次 gfx.set_px,解释轨实测 3.85 秒,编译轨 4 毫秒。
根因:gfx.set_px 内部写的是 c[2][y*w+x] = color——嵌套下标赋值每次都先把内层列表复制一份再写回,6400 次写 = 6400 次整画布复制。line/text/fill_rect 都是它的包装。(这与「列表值语义」无关:PuXian 的列表是引用语义,慢在嵌套赋值这条路径上。)
修法:图形优先拼 SVG 字符串(解释轨 91 ms、3 KB),前端 <img src="data:image/svg+xml;base64,…"> 直接吃。
06 · 同一段循环差约 500 倍
现象:10 万次累加,px run 5182–5928 ms,编译产物 10–12 ms(C 轨 4–5 ms)。
根因:解释轨逐条解释字节码;编译轨先生成代码再链接执行——0.2.382 默认是 VM 字节码轨(px build),--c 才走 C 轨。
修法:循环里干活的代码不进解释轨。Web 场景给路由显式 mode: fast。
22 · actor 状态更新报语法错
现象:在 actor 的消息处理里写 state.count = state.count + 1 报语法错。
根因:处理函数需要多行块并在末尾保留逗号;单表达式赋值会被当表达式解析。
修法:写成块形式:def handle(state, msg): 后接多行缩进语句,末尾逗号不要省。
这些坑的共同形状
| 形状 | 你会怎么误判 | 正确做法 |
|---|---|---|
| 静默失败(不报错、只是少做事) | 「这段肯定没问题,去看别处」 | 对关键分支逐条件打印,别靠读代码推断 |
| 类型严格导致比较失效 | 「0 就是 false」 | 先确认函数返回 bool 还是 int |
| 解释轨 vs 编译轨行为不同 | 「我代码写错了」 | 两轨都跑一遍,差异就是线索 |
| Web 层的隐式协议 | 「响应头没写对」 | 查框架文档的换行/输出约定 |
| 缓存把坏结果固化了 | 「改了没生效」 | 清缓存目录,确认产物时间戳 |