13. 文件 I/O
文件 I/O(输入/输出)是几乎所有应用程序的基础功能。无论是读取配置文件、处理日志数据,还是存储用户上传的内容,都离不开对文件系统的操作。Go 语言通过其标准库提供了强大且优雅的文件操作能力。本章将带你系统学习 Go 中的文件读写,从最基础的文件打开与关闭,到各种读写模式,再到一些进阶技巧。文件 I/O 也是编程语言中很重要的一个部分,在 Go 语言中,标准库提供了 os、io、bufio、io/fs 等包,可以非常简洁高效地处理文件 I/O。
13.1. 文件 I/O 常用的库
在 Go 语言中,文件操作主要通过 os、io 和 bufio 这几个标准库包来完成。
os包:负责与操作系统进行交互,提供打开、创建、删除文件,以及获取文件属性等基础功能。io包:提供了 I/O 操作的抽象接口,其中最核心的是io.Reader和io.Writer。任何实现了这两个接口的类型,都可以使用统一的读写方法。bufio包:在io的基础上提供了带缓冲区的读写功能。通过减少底层的系统调用次数,可以大幅提升读写性能,尤其适合处理大文件。
13.2. 文件的打开与关闭
打开文件是操作文件的第一步,在 Go 语言的标准库 os 中,提供了 Open() 方法以只读模式打开文件,打开文件后的文件对象 *os.File 可以通过 Close() 方法关闭文件。
file, err := os.Open("data.txt") // 只读
defer file.Close() // 确保关闭
除了使用 Open() 方法以只读模式打开文件之外,还可以使用 OpenFile() 方法,该方法能提供更细致的控制,方法的定义如下:
func OpenFile(name string, flag int, perm FileMode) (*File, error)
其中,name 为要打开的文件的路径;flag 为打开文件的模式(标志位),通过位运算符 | 组合;perm 为文件的权限模式。这里的 flag 参数决定了以什么样的方式打开文件,常用的方式如下:
os.O_RDONLY:只读模式打开文件os.O_WRONLY:只写模式打开文件os.O_RDWR:读写模式打开文件os.O_APPEND:写入时,将数据追加到文件末尾os.O_CREATE:如果文件不存在则创建它
```perm参数主要是用于设置文件的读取权限,如会设置0644`,即文件所有者可读写,其他用户只读。具体的打开文件的代码如下:
// 更精细的控制:只写、创建、追加等
file, err := os.OpenFile("data.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
或者可以使用 os.Create() 函数创建一个新文件。
file, err := os.Create("output.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
注意:
os.Create在文件已存在时会将其内容清空。
这里的 defer 关键字在前面的章节中也有介绍到,用于延迟执行一个函数调用,直到包含它的那个函数(外层函数)即将返回时再执行。在打开文件后,必须在操作完成后关闭它,以释放系统资源。使用 defer 语句可以确保无论在函数执行过程中发生何种情况,文件都会在函数返回前被关闭。
13.3. 读取文件
Go 提供了多种读取文件的方式,适用于不同的场景。
13.3.1. 一次性读取全部
对于配置文件或小型文件,最简单的方式是使用 os.ReadFile 函数将整个文件内容一次性读入内存,代码如下:
package main
import (
"fmt"
"os"
)
func main() {
content, err := os.ReadFile("test.txt")
if err != nil {
fmt.Println("读取文件失败:", err)
return
}
fmt.Println(string(content))
}
```os.ReadFile返回一个[]byte` 类型的数据,可以直接转换为字符串。
13.3.2. 逐行读取
文件的按行读取是非常常见的需求,可以使用 bufio.Scanner 来实现,代码如下:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
// 逐行扫描
for scanner.Scan() {
fmt.Println(scanner.Text()) // 获取当前行的文本
}
// 检查扫描过程中是否发生错误
if err := scanner.Err(); err != nil {
fmt.Println("读取文件出错:", err)
}
}
```bufio.Scanner` 默认按行分割,非常适合处理大文件,因为它不会一次性将整个文件加载到内存中
13.3.3. 带缓冲读取
如果需要更灵活地控制读取的字节数,或者处理二进制文件,可以使用 bufio.NewReader的 Read 方法,代码如下:
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
// 创建一个大小为 1024 字节的缓冲区
buf := make([]byte, 1024)
for {
n, err := reader.Read(buf) // 读取数据填充到 buf
if err == io.EOF { // 文件读取完毕
break
}
if err != nil {
fmt.Println("读取文件出错:", err)
return
}
// 处理实际读取到的 n 个字节的数据
fmt.Print(string(buf[:n]))
}
}
```Read方法会尽可能多地读取数据填满缓冲区,并返回实际读取的字节数。当读到文件末尾时,会返回io.EOF` 错误
13.4. 写入文件
写入文件也有多种方式,可以根据需要选择覆盖写入、追加写入或使用缓冲写入。
13.4.1. 直接写入字符串或者字节
最直接的写入方式是使用 os.File 对象的 WriteString 或 Write 方法,代码如下:
package main
import (
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 写入字符串
_, err = file.WriteString("Hello, Go!\n")
if err != nil {
panic(err)
}
// 写入字节切片
_, err = file.Write([]byte("Second line\n"))
if err != nil {
panic(err)
}
}
13.4.2. 带缓冲写入
与 bufio.NewReader 对应,bufio.NewWriter 提供了带缓冲的写入功能,可以显著提升写入性能,代码如下:
package main
import (
"bufio"
"os"
)
func main() {
file, err := os.Create("buffered.txt")
if err != nil {
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(file)
// 数据先写入缓冲区
writer.WriteString("Buffered write 1\n")
writer.WriteString("Buffered write 2\n")
// 必须调用 Flush,将缓冲区中的数据真正写入文件[reference:20]
err = writer.Flush()
if err != nil {
panic(err)
}
}
注意:使用
bufio.NewWriter后,必须调用Flush()方法,才能确保缓冲区中的数据被写入底层的文件。
13.5. 其他常用操作
除了文件的读写,涉及到文件的还有其他的一些操作,如复制文件,操作目录,检查文件或者目录是否存在等。
13.5.1. 复制文件
使用 io.Copy 函数可以高效地将一个文件的内容复制到另一个文件,尤其适合大文件,因为它是以流式方式传输,不会将整个文件加载到内存中。代码如下:
package main
import (
"io"
"os"
)
func main() {
src, err := os.Open("source.txt")
if err != nil {
panic(err)
}
defer src.Close()
dst, err := os.Create("dest.txt")
if err != nil {
panic(err)
}
defer dst.Close()
// 将 src 的内容复制到 dst
_, err = io.Copy(dst, src)
if err != nil {
panic(err)
}
}
13.5.2. 检查文件或目录是否存在
Go 没有提供直接的 Exists 函数,通常通过 os.Stat 来检查。代码如下:
func fileExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true // 文件或目录存在
}
if os.IsNotExist(err) {
return false // 不存在
}
// 其他错误(如权限问题),这里简单返回 false
return false
}
13.6. 本章小结
掌握文件 I/O 是编写实用 Go 程序的基础。本章介绍的各种模式和方法,足以应对日常开发中的绝大多数场景。在实际编码时,请始终牢记错误处理和资源管理,这是写出健壮、可靠代码的关键。