惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
B
Blog RSS Feed
U
Unit 42
阮一峰的网络日志
阮一峰的网络日志
量子位
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
云风的 BLOG
云风的 BLOG
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
IT之家
IT之家
V
Visual Studio Blog
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
I
InfoQ
D
Docker
V
V2EX

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器
给silly实现了一个ernro模块 | 重归混沌的BLOG
重归混沌 · 2026-05-03 · via 重归混沌的BLOG

去年重构 silly.net.tcp 模块时,为了识别 read 是否已经读到连接关闭,我参考了 Berkeley Sockets APIread 返回 0 的语义,并以返回空字符串来表示这一状态。

但由于设计的是 Full Read 模式,为了判断本次 read 是否获取到完整数据,我在返回 "" 的同时,将错误设置为 end of file

这样,当不关心具体错误原因时,只需判断 err 是否为 nil,即可得知当前读取的数据是否完整。

如果需要判断是否为 end of file,则可以在 err ~= nil 的前提下,检查返回的数据是否为空字符串;若为空,则表示读到了 end of file

而判断是否为 read timeout,则可以在发生错误时,通过 conn:isalive() 判断连接是否仍然存活,因为只有 read timeout 不会导致连接断开。

这里存在竞态场景,例如刚发生超时,随后连接又被关闭。此时 read 返回 nil, "read timeout",但 isalive() 却返回 false。不过连接此时本就已经断开,因此影响相对有限。

之所以没有将 end of fileread timeout 设计得更直观,是因为在现有抽象中,string 形式的 error 不具备可比较性。无法保证在其他上下文中不会返回语义不同但字面相同的 end of file。例如在 http 连接复用场景下,只要读满 content-length,也可能返回 end of file,但这并不意味着连接关闭。

最初设计用这两个返回值表达三种状态(正常、失败、end of file)时,我还自豪的认为已经将 2bit 的表达能力利用到了极致。

然而在随后几个月的实践中,即使作为作者,我也多次踩坑,经常不自觉地写出如下代码:


local conn = tcp.connect("127.0.0.1:6379")
while true do
    local dat, err = conn:read("\n")
    if not dat then
        break
    end
    ... do something with dat
end

这段代码在遇到 end of file 时会陷入死循环。

根本原因就在于,这种使用方式不直观,也不符合 Lua 的常见习惯,而且会带来额外的心智负担。

此外,由于向已关闭连接执行 write 时,返回的错误可能是 broken pipe 而非 end of file,因此该死循环并非必现,更增加了排查难度。

在之后使用 Claude 编写相关代码时,也多次遇到同样的问题,这促使我重新审视「符合直觉」的重要性。

-----------------

经过这两个月的反复权衡,我最终决定引入 silly.errno,并建立清晰的抽象边界来解决这一问题。

在当前的 silly 实现中,所有来自 C 层的错误码,在进入 Lua 层之前都会被转换为 string。这样做是因为Lua的字符串既具备良好的可读性,又不会带来额外的判断开销。

接下来需要明确一个边界:哪些 string 类型的 error 可以比较,哪些不应比较。

观察后发现,需要进行比较的错误,通常来自 C 底层,或是对其进行薄封装的模块,例如 silly.netsilly.net.tcpsilly.net.tlssilly.net.udp。

而在其之上的模块,一旦发生错误,要么是协议层自定义的错误(如 mysql/redis/http),要么属于不可恢复错误,本身不适合参与比较。

基于此,我引入了 silly.errno 类型:silly.netsilly.net.tcpsilly.net.tlssilly.net.udp 返回的 error 均为 silly.errno;其他模块则继续返回 stringsilly.errno 的值是可比较的,可以与 silly.errno.XXX 进行判断;而普通 string 类型的 error 不具备这一特性。

从类型关系上看,silly.errno 派生自 string,即它是 string 的子类型。这样设计的原因在于:当上层模块需要返回 string 类型的 error 时,可以将底层返回的 silly.errno 进行 upcast 后继续向上传递。一旦发生 upcast,即意味着该错误丧失了可比较性。

这一设计也预留了扩展空间。目前用于返回与比较的字符串均由 silly.errno 模块导出。若未来 string 难以支撑更复杂的需求,可以在不破坏兼容性的前提下,将 silly.errno 扩展为带有 __tostring 元方法的 table 类型。

ps. 由于我在标准库 errno.h 的错误码基础上扩展了一部分自定义错误码,为避免这些错误码对应的字符串与系统错误发生意外冲突,我在 Lua 层对错误字符串的格式做了轻微调整,类似这样:snprintf(buf, sizeof(buf), "%s (%d)", strerror(errno), errno);。

pps. 在设计该模块之初,一直问自己:是否有必要仅为 end of filetimeout 的可比较性,就重构整套 errno 机制。最终我的答案是:符合直觉应当高于一切,这也更符合最小知识依赖原则。