Go 文件操作

Go 文件操作

文件操作是每个程序员都绕不开的基本功。无论是读取配置文件、写入日志,还是处理用户上传的数据,都离不开对文件系统的操作。Go 语言标准库提供了 osiofs 三个核心包,它们层次分明、功能完整,既适合快速上手,也能应对复杂场景。本文将从打开文件开始,系统地讲解 Go 中各种文件操作的方法和注意事项。

一、文件的打开方式

Go 中打开文件最常用的两个函数是 os.Openos.OpenFile

1.1 os.Open:简单只读

os.Open 是最直接的打开方式,它返回一个文件指针和一个错误:

复制代码
file, err := os.Open("README.txt")

默认只读,适合读取已存在的文件。当文件不存在时,会返回明确的错误信息。

结合 os.IsNotExist 可以处理文件不存在的情况:

复制代码
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
    fmt.Println("文件不存在")
} else if err != nil {
    fmt.Println("文件访问异常")
} else {
    fmt.Println("文件打开成功", file.Name())
    file.Close()
}

1.2 os.OpenFile:细粒度控制

如果需要对文件进行写入或追加操作,就需要使用 os.OpenFile。它的签名是:

复制代码
func OpenFile(name string, flag int, perm FileMode) (*File, error)

flag 参数控制打开模式,常用选项包括:

标志 含义
os.O_RDONLY 只读
os.O_WRONLY 只写
os.O_RDWR 读写
os.O_APPEND 追加到末尾
os.O_CREATE 不存在则创建
os.O_TRUNC 打开时清空内容

perm 参数指定文件权限,如 0666 表示所有人可读写。

例如以读写方式打开文件,不存在则创建:

复制代码
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)

1.3 获取文件信息:os.Stat

如果不打算读取文件内容,只是想知道文件是否存在或获取基本信息,可以用 os.Stat

复制代码
info, err := os.Stat("README.txt")
if err == nil {
    fmt.Println("文件大小:", info.Size())
    fmt.Println("修改时间:", info.ModTime())
}

1.4 务必关闭文件

打开的文件是系统资源,用完必须释放。常见的做法是用 defer 在打开后立即注册关闭:

复制代码
file, err := os.Open("README.txt")
if err != nil {
    return
}
defer file.Close()
// 后续操作...

defer 能保证函数退出时文件被关闭,即使中间发生了 panic

二、读取文件

打开文件之后,最自然的需求就是把内容读出来。Go 提供了多种读取方式,从底层缓冲区操作到一键读取应有尽有。

2.1 底层读取:file.Read

*os.FileRead 方法是最底层的读取方式,需要手动管理缓冲区:

复制代码
func (f *File) Read(b []byte) (n int, err error)

典型的读取循环写法:

复制代码
buf := make([]byte, 1024)
for {
    n, err := file.Read(buf)
    if n > 0 {
        // 处理 buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        return err
    }
}

2.2 便捷函数:os.ReadFile

如果文件不大,更推荐用 os.ReadFile,它直接返回文件所有内容的字节切片:

复制代码
data, err := os.ReadFile("README.txt")
if err == nil {
    fmt.Println(string(data))
}

这个函数简单粗暴,适合一次性读取小文件。

2.3 从任意 io.Reader 读取:io.ReadAll

io.ReadAll 更通用,它接受任何实现了 io.Reader 接口的对象,包括 *os.File、网络连接等:

复制代码
file, _ := os.Open("README.txt")
data, err := io.ReadAll(file)

三、写入文件

与读取类似,写入也有多种方式可选。

3.1 底层写入:WriteWriteString

*os.File 提供了 Write(写入字节切片)和 WriteString(写入字符串):

复制代码
n, err := file.Write([]byte("hello"))
n, err := file.WriteString("hello")

注意写入模式由 OpenFileflag 决定:

  • os.O_TRUNC 会清空原内容

  • os.O_APPEND 会追加到末尾

3.2 便捷函数:os.WriteFile

os.WriteFile 将整个字节切片写入文件,覆盖原有内容:

复制代码
err := os.WriteFile("README.txt", []byte("hello"), 0666)

3.3 向任意 io.Writer 写入:io.WriteString

io.WriteString 可以向任何实现了 io.Writer 接口的目标写入字符串:

复制代码
n, err := io.WriteString(file, "hello world!")

四、复制文件

复制文件的本质是:从一个地方读取,写入另一个地方。

4.1 两步法:读出来再写进去

先用 os.ReadFile 读取完整内容,再用 os.WriteFile 写入新文件:

复制代码
data, _ := os.ReadFile("source.txt")
os.WriteFile("dest.txt", data, 0666)

这种方法把整个文件载入内存,不适合大文件。

4.2 用 io.Copy 流式复制

io.Copy 采用缓冲区流式复制,边读边写,适合任意大小的文件:

复制代码
src, _ := os.Open("source.txt")
dst, _ := os.Create("dest.txt")
defer src.Close()
defer dst.Close()

written, err := io.Copy(dst, src)

io.Copy 内部使用 32KB 的缓冲区,也可以使用 io.CopyBuffer 自定义缓冲区大小。

4.3 *os.FileReadFrom 方法

*os.File 直接实现了 ReadFrom 方法,可以快速将一个 io.Reader 的内容写入文件:

复制代码
target.ReadFrom(source)

但这种方式同样要求源文件全部可读。

五、文件重命名与删除

5.1 重命名:os.Rename

os.Rename 既可以重命名文件,也可以移动文件到其他目录:

复制代码
os.Rename("README.txt", "readme.md")

5.2 删除单个文件:os.Remove

复制代码
os.Remove("README.txt")

5.3 删除整个目录树:os.RemoveAll

复制代码
os.RemoveAll("temp") // 删除 temp 目录及其所有子内容

六、强制落盘:Sync

现代操作系统通常会对 IO 操作做缓存,以提高性能。但在某些场景(如数据库日志)中,我们需要确保数据真正写入物理磁盘。(*os.File).Sync() 可以做到这一点:

复制代码
file.Write([]byte("critical data"))
file.Sync() // 强制落盘

七、文件夹(目录)操作

对目录的操作逻辑与文件类似,但有其独特性。

7.1 读取目录内容

os.ReadDir 直接读取目录并返回 DirEntry 切片:

复制代码
entries, err := os.ReadDir(".")
for _, entry := range entries {
    fmt.Println(entry.Name())
}

也可以通过 *os.File.ReadDir 实现类似功能,控制读取数量:

复制代码
dir, _ := os.Open(".")
entries, _ := dir.ReadDir(-1) // -1 表示全部读取

7.2 创建目录

os.Mkdir 创建一个单级目录,os.MkdirAll 则会自动创建路径中所有缺失的父目录:

复制代码
os.Mkdir("data", 0755)
os.MkdirAll("data/2024/logs", 0755) // 会递归创建

7.3 递归复制目录

要复制整个目录,需要递归遍历源目录结构,并在目标位置创建对应目录和文件。使用 filepath.Walk 可以简化这个过程:

复制代码
func CopyDir(src, dst string) error {
    return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
        rel, _ := filepath.Rel(src, path)
        destPath := filepath.Join(dst, rel)

        if info.IsDir() {
            return os.MkdirAll(destPath, info.Mode())
        }

        // 复制文件
        srcFile, _ := os.Open(path)
        dstFile, _ := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode())
        defer srcFile.Close()
        defer dstFile.Close()

        _, err = io.Copy(dstFile, srcFile)
        return err
    })
}

filepath.Walk 会自动遍历所有子目录,对每个文件或目录调用回调函数,大大简化了递归逻辑。

八、总结

Go 的文件处理设计遵循了简洁实用的原则,os 包负责系统调用层面的操作,io 包提供接口抽象,两者配合可以覆盖几乎所有文件处理需求。

操作 推荐函数
打开文件 os.Open / os.OpenFile
读取全部内容 os.ReadFile
写入文件 os.WriteFile
流式复制 io.Copy
重命名 os.Rename
删除 os.Remove / os.RemoveAll
读取目录 os.ReadDir
强制落盘 (*os.File).Sync