Menu

Golang 读写文件:os、bufio 与目录操作

如何在 Go 中读写文件:os.ReadFile 和 os.WriteFile、用 bufio.Scanner 逐行读取、用 os.OpenFile 追加内容、检查文件是否存在,以及操作目录。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

读写整个文件

os.WriteFile 和 os.ReadFile 能满足大部分需求。它们一次调用就完成打开、写入或读取,以及关闭。

本页的示例都在 os.MkdirTemp 创建的临时目录里操作,并用 defer os.RemoveAll(dir) 删除它,所以不会留下任何东西。在你自己的代码中,像 "config.json" 这样的相对路径是相对于进程的工作目录解析的,而工作目录不一定是源文件或二进制文件所在的目录。

os.WriteFile 会在需要时创建文件,如果文件已存在则会清空它。第三个参数是新建文件的 Unix 权限:0o644 表示所有者可读可写,其他人只读。对已经存在的文件,这个参数会被忽略,而且进程的 umask 可能会去掉某些权限位。

os.ReadFile 会把所有内容读进内存。这对配置文件和小规模输入很合适,对几 GB 的日志就不合适了。

用 bufio.Scanner 逐行读取

对于大文件,或者你本来就想按行处理时,用 bufio.Scanner。它分块读取,每次交给你一行,不含换行符。

三个细节:

  • 循环结束后检查 sc.Err()。 Scan 在文件结束和出错时都会返回 false,只有 Err 能区分这两种情况。
  • 64 KB 的行长限制。 默认情况下,单行超过 64 KB 会让 scanner 以 bufio.Scanner: token too long 停止。对于有长行的文件(压缩过的 JSON、某些日志),在循环之前提高上限:sc.Buffer(make([]byte, 1024*1024), 10*1024*1024)。
  • 其他切分单位。 sc.Split(bufio.ScanWords) 按单词切分;bufio.ScanRunes 按字符切分。

如果要按固定大小的块而不是按行读取数据流,可以在循环中使用 f.Read(buf),或者用 io.Copy 复制到另一个 writer。

写入:os.Create、os.OpenFile 与追加

os.Create(name) 以写入方式打开文件,文件不存在就创建,存在就清空。os.OpenFile 则通过标志位提供完全的控制:

标志含义
os.O_RDONLY、os.O_WRONLY、os.O_RDWR以只读、只写或读写方式打开(三选一)
os.O_CREATE文件不存在时创建它
os.O_TRUNC打开时清空文件
os.O_APPEND每次写入都写到末尾
os.O_EXCL与 O_CREATE 一起使用:文件已存在则失败

os.Open(name) 等同于 OpenFile(name, O_RDONLY, 0)。os.Create(name) 等同于 OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666)。

写入时,Close 返回的错误很重要。有些文件系统要到关闭时才报告写入失败,所以只写 defer f.Close() 可能会掩盖数据丢失。对于你写入的文件,要像 appendLine 那样显式检查 Close。对于只读取的文件,defer f.Close() 就足够了。

fmt.Fprintln 以及其他 fmt 打印函数 都接受任意 io.Writer,文件也不例外。bufio.Writer 会先在内存中攒下零碎的小写入。忘记调用 w.Flush() 是一个经典 bug:程序正常退出,但最后几 KB 永远没有写进文件。

文件是否存在?

Go 没有 os.Exists。调用 os.Stat 并检查错误:

errors.Is(err, fs.ErrNotExist) 是现在的惯用写法。它取代了旧的 os.IsNotExist(err),后者看不穿被包装的错误。

在打开之前先检查往往没有必要,还会产生竞态:在检查和打开之间,文件可能出现也可能消失。通常直接打开它,再处理打开时返回的 fs.ErrNotExist 即可。如果只想在文件尚不存在时创建它,用 O_CREATE|O_EXCL,它把检查和创建合并成一个原子步骤。

目录

任务函数
创建一个目录os.Mkdir(path, 0o755)
连同父目录一起创建路径os.MkdirAll(path, 0o755)
列出目录内容os.ReadDir(path)
遍历目录树filepath.WalkDir(root, fn)
删除文件或空目录os.Remove(path)
删除整个目录树os.RemoveAll(path)
重命名或移动os.Rename(old, new)
临时文件或目录os.CreateTemp("", "prefix-*")、os.MkdirTemp("", "prefix")
拼接路径filepath.Join(a, b, c)

文件系统路径请用 path/filepath:它会使用操作系统对应的分隔符(Windows 上是 \)。path 包用于 URL 这类以斜杠分隔的路径。

Go 1.24 还新增了 os.Root(os.OpenRoot(dir)),它只允许打开某一个目录内的文件,并拒绝通过 .. 或符号链接逃出该目录的路径。文件名来自用户时就用它。

常见错误

  • 不检查错误。 这些调用每一个都可能失败。打开失败的文件是 nil,之后对它的每次 Read、Write 或 Close 都会返回 invalid argument,从而掩盖真正的原因(文件不存在,或者权限被拒绝)。
  • 扫描循环结束后忘了检查 sc.Err()。 读取错误看起来就像文件结束。
  • 忘了对 bufio.Writer 调用 Flush。 文件末尾会缺一截。
  • 写入后忽略 Close 返回的错误。 写入错误可能只会在这里暴露出来。
  • 在遍历大量文件的循环里写 defer f.Close()。 文件要等到函数返回才会关闭,可能会耗尽文件描述符。把循环体移到一个函数里,让每个文件在每次迭代中关闭。
  • 把权限写成十进制。 644 不是 0o644。Go 把 644 当作十进制数,也就是 0o1204,会设置一些奇怪的权限位。

常见问题

在 Go 中怎样把整个文件读成字符串?

data, err := os.ReadFile("notes.txt") 以 []byte 返回文件内容,再用 string(data) 转换。它会替你打开、读取并关闭文件。适合能轻松放进内存的文件;大文件请用 bufio.Scanner 逐行读取。

在 Go 中怎样逐行读取文件?

用 os.Open 打开文件,defer f.Close(),用 bufio.NewScanner(f) 包装它,用 for sc.Scan() { line := sc.Text() } 循环,循环结束后检查 sc.Err()。超过 64 KB 的行会让 scanner 以 token too long 失败,除非你用 sc.Buffer 扩大它的缓冲区。

在 Go 中怎样向文件追加内容?

用 os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) 打开文件,写入,并检查 Close 返回的错误。O_CREATE 会在文件不存在时创建它,O_APPEND 让每次写入都写到文件末尾。

在 Go 中怎样判断文件是否存在?

调用 os.Stat(path),再用 errors.Is(err, fs.ErrNotExist) 检查错误。错误为 nil 表示文件存在。其他任何错误(比如权限被拒绝)都意味着你无法确定,所以要单独处理,而不是把它当作“不存在”。

Coddy 编程语言插图

和 Coddy 一起学编程

开始