
本文介绍使用 Go 标准库 bufio.Scanner 逐行读取文本文件的正确方法,并重点解决“读一行、处理一行、删除该行”的实际需求,同时规避并发修改、内存占用和文件一致性等常见陷阱。
本文介绍使用 go 标准库 `bufio.scanner` 逐行读取文本文件的正确方法,并重点解决“读一行、处理一行、删除该行”的实际需求,同时规避并发修改、内存占用和文件一致性等常见陷阱。
在 Go 中,无法像 Shell 中 sed -i 1d 那样原子性地“原地删除首行”——文件系统不支持直接截断或移除开头内容而不重写整个文件。因此,“读—处理—删除”这一流程需转换为更健壮的模式:流式读取 + 延迟写入 + 原子替换。
✅ 推荐做法:读写分离 + 原子覆盖
核心思路是:
- 使用 bufio.Scanner 安全逐行读取(自动处理换行、内存友好);
- 将未处理的行(即跳过已消费行)写入临时文件;
- 处理完成后,用临时文件原子替换原文件。
以下是一个完整、生产可用的示例:
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
)
// processLine 模拟业务逻辑:打印并返回是否应保留该行(false = 删除)
func processLine(line string) bool {
fmt.Printf("Processing: %s\n", line)
// 示例:跳过空行和注释行,其余保留
return line != "" && !filepath.IsAbs(line) // 简化逻辑,按需替换
}
func main() {
filename := "data.txt"
// 1. 打开源文件读取
f, err := os.Open(filename)
if err != nil {
panic(fmt.Sprintf("failed to open %s: %v", filename, err))
}
defer f.Close()
scanner := bufio.NewScanner(f)
tempFile := filename + ".tmp"
// 2. 创建临时文件写入剩余行
out, err := os.Create(tempFile)
if err != nil {
panic(fmt.Sprintf("failed to create temp file: %v", err))
}
defer out.Close()
writer := bufio.NewWriter(out)
defer writer.Flush()
// 3. 逐行扫描、处理、选择性写入
for scanner.Scan() {
line := scanner.Text()
// ✅ 关键:在此处执行你的业务逻辑
if !processLine(line) {
continue // 跳过,即“逻辑删除”
}
// 保留该行 → 写入临时文件
if _, err := fmt.Fprintln(writer, line); err != nil {
panic(fmt.Sprintf("write failed: %v", err))
}
}
if err := scanner.Err(); err != nil {
panic(fmt.Sprintf("scan error: %v", err))
}
// 4. 原子替换:用临时文件覆盖原文件(Linux/macOS 安全;Windows 下 os.Rename 也保证原子性)
if err := os.Rename(tempFile, filename); err != nil {
panic(fmt.Sprintf("atomic replace failed: %v", err))
}
fmt.Println("✅ Processing complete. Original file updated.")
}⚠️ 重要注意事项
- 不要尝试 os.Truncate(0) 后重写:若写入中途失败,原文件将被清空,造成数据丢失;
- 避免 ioutil.ReadFile + 全量重写:对大文件内存压力大,且无法实现“边读边删”的流式语义;
- 并发安全:若其他进程可能同时写入原文件,应在外部加锁(如 flock),Go 本身不提供跨进程文件锁;
- 换行符兼容性:scanner.Text() 自动剥离 \n 或 \r\n,写入时 fmt.Fprintln 会补回平台默认换行符(Unix/Linux 为 \n);
- 错误处理不可省略:示例中使用 panic 仅为简洁,生产环境应返回错误并做回滚(如删除临时文件)。
? 补充:若需真正“实时删除首行”(如队列场景)
可考虑替代方案:
- 使用 SQLite 或 BoltDB 存储行数据,用 DELETE LIMIT 1 实现原子出队;
- 使用消息队列(如 Redis List)替代文件;
- 文件仅作日志归档,处理逻辑由独立服务消费后标记 processed=true。
总之,Go 的哲学是 显式优于隐式,安全优于便捷。用临时文件+原子重命名,既符合 POSIX 语义,又兼顾跨平台可靠性,是处理此类需求最推荐的实践。

















