不能直接用 os.file 做 append-only fifo,因其无法安全实现「消费即移除」,手动截断会导致并发冲突或数据丢失;需用分段文件+偏移量管理+sync 保障崩溃一致性。

为什么不能直接用 os.File 做 append-only FIFO
很多人第一反应是开个文件,Write 往末尾追数据、Read 从开头读——但这样无法安全实现 FIFO:读完不删数据,文件会无限膨胀;手动截断(Truncate)又会导致并发读写冲突或数据丢失。真正的持久化 FIFO 必须支持「消费即移除」且不破坏后续数据连续性,同时容忍进程崩溃。
gob + 文件分段不是好选择
用 gob 序列化结构体写入单文件,看似简单,但一旦某条记录损坏(比如写到一半进程被 kill),整个文件可能无法解码;而且 gob 不支持随机跳过损坏项,没法做「跳过坏消息继续读」。更实际的做法是:每条消息独立写入,用固定长度头描述长度,避免耦合解析逻辑与存储格式。
- 每条消息前写 4 字节
uint32表示 payload 长度(小端序) - 写入时先
Write头,再Write数据,两次调用间用file.Sync()保证落盘 - 读取时先读 4 字节,再按长度读 payload;若读头失败或长度超限,视为该条已损坏,可丢弃并尝试下一条
如何安全地「删除已消费消息」而不重写整个文件
磁盘上没有真正的“删除”,只有「标记为已消费」或「移动读写位置」。推荐用两个文件配合偏移量管理:queue.data(只追加)+ queue.offset(记录当前读到哪)。每次读取后,把新偏移量 Sync() 写入 offset 文件;重启时先读 offset,再从 data 文件对应位置开始读。
- offset 文件只需存一个
int64,用binary.Write固定 8 字节,避免文本解析错误 - 写 offset 前必须确保对应消息已成功处理(业务逻辑完成),否则可能重复消费
- 不建议用
Seek修改 data 文件头部——ext4/xfs 对小范围随机写性能差,且增加 fsync 开销
并发读写时必须加的锁和 sync 点
Go 的 os.File 本身不是并发安全的,但更关键的是语义层竞争:比如两个 goroutine 同时调用 Pop,都读到同一条消息。必须用进程内互斥锁(sync.Mutex)保护「读 offset → 读 data → 更新 offset」这一整段逻辑。另外:
- 写入(
Push)也要锁,防止多 goroutine 并发Write导致头/数据错位 - 每个
Write后必须跟file.Sync(),否则断电时可能只写入了长度头没写入数据 - offset 文件的写入也必须
Sync(),且最好用O_CREATE|O_WRONLY|O_TRUNC每次覆盖,避免部分写
真正难的不是代码量,而是想清楚「哪一步失败会导致什么后果」:写 data 失败?丢一条消息。写 offset 失败?重复消费一次。两者都失败但顺序乱了?可能跳过消息。这些边界得在设计时就对齐业务容忍度。











