30·标准库入门

文件操作

osfilereadwriteopen

Go 并发编程 - 第30课:文件操作

所属模块: 标准库 | 难度: Beginner | 标签: os, file, read, write, open 上一课: IO 基础:Reader/Writer | 下一课: 缓冲 IO 与 bufio


1. 学习目标

完成本课学习后,你将能够:

  • 使用 os 包中的函数安全地打开、创建和关闭文件。
  • 掌握从文件中读取数据和向文件中写入数据的基本方法。
  • 理解文件操作中常见的模式,如使用 defer 确保资源释放。
  • 处理文件操作中可能出现的常见错误。

2. 核心概念

在Go语言中,文件操作主要通过标准库的 os 包来完成。文件在操作系统层面被抽象为一个“文件描述符”,而在Go中,我们操作的是一个 *os.File 类型的文件对象。

关键函数与方法:

  • os.Open(name string) (*File, error):以只读模式打开一个文件,如果文件不存在则会返回错误。
  • os.Create(name string) (*File, error):创建或截断一个文件用于写入。如果文件已存在,其内容将被清空。
  • os.OpenFile(name string, flag int, perm FileMode) (*File, error):更通用的打开方式,可以通过标志(flag)指定读写模式、是否创建、是否追加等,以及设置文件权限(perm)。
  • File.Close() error:关闭文件,释放相关的系统资源。必须在文件使用完毕后调用。
  • File.Read(b []byte) (n int, err error):从文件中读取数据到字节切片 b 中。
  • File.Write(b []byte) (n int, err error):将字节切片 b 中的数据写入文件。

一个重要原则:资源管理 操作系统能同时打开的文件数是有限的。忘记关闭文件(文件句柄泄漏)会导致程序逐渐耗尽系统资源,最终出错。Go的 defer 语句是管理文件生命周期的最佳实践。


3. 代码示例

示例1:安全地读取整个文件

package main

import (
	"fmt"
	"os"
)

func main() {
	// 1. 打开文件(只读模式)
	file, err := os.Open("data.txt")
	if err != nil {
		// 如果文件不存在或其他错误,打印错误并退出
		fmt.Println("打开文件失败:", err)
		return
	}
	// 2. 使用 defer 确保在 main 函数结束前关闭文件
	// defer 语句会将其后的函数调用推迟到外层函数返回前执行
	defer file.Close()

	// 3. 创建一个足够大的缓冲区来读取文件内容
	// 对于小文件,我们可以一次性读取。
	// 注意:读取大文件时,应分块读取。
	buf := make([]byte, 1024) // 1KB 缓冲区
	n, err := file.Read(buf)
	if err != nil {
		fmt.Println("读取文件失败:", err)
		return
	}

	// 4. 将实际读取到的字节(切片 buf[0:n])转换为字符串并打印
	fmt.Printf("读取了 %d 个字节,内容如下:\n", n)
	fmt.Println(string(buf[:n]))
}

示例2:创建并写入文件

package main

import (
	"fmt"
	"os"
)

func main() {
	// 1. 创建或打开一个文件用于写入(文件存在则截断)
	file, err := os.Create("output.txt")
	if err != nil {
		fmt.Println("创建文件失败:", err)
		return
	}
	defer file.Close()

	// 准备要写入的数据
	data := []byte("Hello, Go!\n这是写入文件的内容。")

	// 2. 写入数据
	n, err := file.Write(data)
	if err != nil {
		fmt.Println("写入文件失败:", err)
		return
	}
	fmt.Printf("成功写入了 %d 个字节到文件 output.txt\n", n)

	// 3. 也可以使用 WriteString 方法写入字符串
	_, err = file.WriteString("直接写入一个字符串。\n")
	if err != nil {
		fmt.Println("写入字符串失败:", err)
		return
	}
	fmt.Println("额外内容追加成功。")
}

示例3:使用 OpenFile 进行追加写入

package main

import (
	"fmt"
	"os"
)

func main() {
	// 以只写、追加模式打开文件,如果文件不存在则创建它
	// os.O_WRONLY: 只写
	// os.O_APPEND: 追加模式,写入的数据会添加到文件末尾
	// os.O_CREATE: 如果文件不存在则创建
	// 0644: 文件权限 (Unix-like系统下,所有者可读写,其他用户只读)
	file, err := os.OpenFile("append.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
	if err != nil {
		fmt.Println("打开/创建日志文件失败:", err)
		return
	}
	defer file.Close()

	// 模拟日志追加
	logEntry := "[2023-10-05 14:30:00] 一条日志记录\n"
	_, err = file.WriteString(logEntry)
	if err != nil {
		fmt.Println("追加日志失败:", err)
		return
	}
	fmt.Println("日志已成功追加到 append.log 文件。")
}

4. 实践练习

练习1:基础读写 编写一个程序,完成以下任务:

  1. 创建一个名为 notes.txt 的文件,并写入你今天的三个学习笔记。
  2. 关闭文件。
  3. 重新打开 notes.txt 文件,并将其内容打印到控制台。
  4. 确保程序在任何情况下(包括出错时)都能正确关闭文件。

预期输出示例:

文件写入成功。
文件内容如下:
1. 学习了Go的文件操作。
2. 理解了defer的重要性。
3. 练习了读写文件。

练习2:简单日志记录器 编写一个程序,使其每次运行时,都在 app.log 文件末尾追加一条带有当前时间戳的日志。格式为:[YYYY-MM-DD HH:MM:SS] 你的消息

  • 使用 os.OpenFileos.O_APPEND 标志。
  • 使用 time.Now() 获取当前时间。
  • 提示:使用 time.Now().Format("2006-01-02 15:04:05") 格式化时间。

练习3:文件复制(进阶) 编写一个函数 copyFile(src, dst string) error,将源文件 src 的内容复制到目标文件 dst

  • 要求分块复制,不要一次性将整个文件读入内存(例如,使用1024字节的缓冲区循环读取和写入)。
  • 处理好所有的错误情况。
  • main 函数中测试它:复制当前目录下的 data.txt 文件到 data_backup.txt

5. 常见错误

  1. 忘记关闭文件:这是最常见的问题。会导致文件描述符泄漏,可能使程序在长时间运行后崩溃。务必使用 defer file.Close()
    file, _ := os.Open("data.txt")
    // ... 使用文件 ...
    // 忘记 file.Close()!
    
  2. 错误处理不完整:在文件操作中,err 的值必须被检查。忽略错误是危险的。
    file, err := os.Open("data.txt")
    // 错误的做法:仅打印错误,但程序继续使用可能为 nil 的 file
    if err != nil {
        fmt.Println("Error:", err)
    }
    buf := make([]byte, 100)
    file.Read(buf) // 如果 file 是 nil,这里会 panic
    
  3. 文件路径问题:使用相对路径时,路径是相对于当前工作目录(即运行程序时所在的目录)。在不同的运行环境下,这可能不是你期望的目录。
  4. 大文件一次性读入内存:对于很大的文件,使用 ioutil.ReadFile 或一次性 Read 会导致内存耗尽。应采用循环分块读取的方式。
  5. 文件权限误解:在类Unix系统上,os.Create 创建的文件权限默认是 0666(但会受 umask 影响)。使用 os.OpenFile 可以更精确地控制权限(如 0644)。

6. 小结

本课我们学习了Go语言中进行文件操作的基础知识:

  • 核心包os 包提供了文件创建、打开、读写、关闭等核心功能。
  • 生命周期管理os.Open (只读)、os.Create (创建/截断)、os.OpenFile (自定义模式) 用于获取文件对象,必须配合 defer file.Close() 来确保资源释放。
  • 读写操作:通过 file.Read()file.Write() 方法与文件内容进行字节层面的交互。
  • 最佳实践:始终检查错误、使用 defer、根据文件大小选择合适的读写策略(一次性读取小文件,分块处理大文件)。
  • 常见陷阱:资源泄漏、路径错误、权限问题和忽略错误处理。

掌握这些基本操作后,你就能为程序添加持久化存储、配置文件读取、日志记录等重要功能。下一课,我们将学习 bufio 包,它能通过缓冲机制显著提升频繁进行小块读写操作的性能。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「缓冲 IO 与 bufio」 以巩固所学知识。