Go 语言的 I/O 编程避坑指南
wxk1991 Lv5

Go 语言的 I/O 编程避坑指南

Go 写 I/O 很顺手,但也正因为顺手,很多坑会被写得很自然:一次性读大文件、忘记关闭文件、缓冲使用不当、错误被吞掉、路径拼接不跨平台。

这篇文章整理 Go 项目里最常见的 I/O 问题。


一、不要默认一次性读取大文件

小配置文件可以直接读:

1
2
3
4
data, err := os.ReadFile("config.yaml")
if err != nil {
return err
}

但日志、CSV、导出文件可能非常大,不应该随手 os.ReadFile。更稳的方式是流式读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
file, err := os.Open("access.log")
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}

if err := scanner.Err(); err != nil {
return err
}

这样内存不会被一个大文件直接打满。


二、Scanner 有默认长度限制

bufio.Scanner 很方便,但它默认单行最大 token 大约 64KB。读取超长日志行或大 JSON 行时,可能报 token too long

可以手动调大缓冲:

1
2
3
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 1024*1024)
scanner.Buffer(buf, 10*1024*1024)

如果要处理真正的大块数据,bufio.Reader 往往比 Scanner 更合适。


三、写文件要注意缓冲和 flush

频繁小块写入时,推荐使用 bufio.Writer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
file, err := os.Create("report.txt")
if err != nil {
return err
}
defer file.Close()

writer := bufio.NewWriter(file)
for _, row := range rows {
if _, err := fmt.Fprintln(writer, row); err != nil {
return err
}
}

if err := writer.Flush(); err != nil {
return err
}

Flush 不能省。否则数据可能还停在用户态缓冲区里,错误也不会及时暴露。


四、defer Close 也要关注错误

很多人会这样写:

1
defer file.Close()

读文件通常问题不大。但写文件时,关闭阶段也可能发生错误,比如网络文件系统异常或磁盘同步失败。

更严谨的写法是:

1
2
3
if err := file.Close(); err != nil {
return err
}

如果已经用了 defer,至少在关键写入流程里确保 FlushSync 或最终关闭错误被处理。


五、路径不要手动拼接

不要这样写:

1
path := dir + "/" + filename

跨平台路径应该使用 filepath.Join

1
path := filepath.Join(dir, filename)

Go 标准库里的 path 主要用于 URL 风格路径,文件系统路径应该优先用 path/filepath


六、临时文件再替换更安全

直接覆盖配置或缓存文件,程序中途崩溃可能留下半个文件。更稳的方式是先写临时文件,再原子替换:

1
2
3
4
5
6
7
8
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
return err
}

if err := os.Rename(tmp, path); err != nil {
return err
}

关键状态文件、配置文件、索引文件,都值得用这种方式处理。


七、实践建议

Go 的 I/O 代码要特别关注三个问题:文件大小、错误处理、资源释放。

小文件可以简单写,大文件要流式处理。写文件要 Flush,打开文件要 Close,路径要用 filepath。这些都不是高级技巧,但能避免很多线上问题。