Go基础:文件与文件夹操作详解

文章目录

    • 一、文件基本操作
      • [1.1 创建和写入文件](#1.1 创建和写入文件)
      • [1.2 读取文件](#1.2 读取文件)
      • [1.3 路径操作](#1.3 路径操作)
    • 二、目录基本操作
      • [2.1 创建和删除目录](#2.1 创建和删除目录)
      • [2.2 遍历目录](#2.2 遍历目录)
    • 三、高级文件操作
      • [3.1 文件重命名与移动](#3.1 文件重命名与移动)
      • [3.2 获取文件信息](#3.2 获取文件信息)
      • [3.3 `bufio` 包:带缓冲的读写](#3.3 bufio 包:带缓冲的读写)
    • 四、综合案例
      • [4.1 简单的日志分析工具](#4.1 简单的日志分析工具)

文件和目录操作是编程中非常基础且重要的部分,无论是日志记录、配置管理、数据处理还是系统工具开发,都离不开它们。Go 语言提供了两个核心包来处理文件和目录操作:

  1. os:提供了与操作系统交互的平台无关接口。它包含了创建、打开、读写、删除文件和目录等基本功能。这是进行文件操作最核心的包。
  2. io/ioutil :在 Go 1.16 之前,这个包提供了一些非常方便的实用函数,如 ReadFile, WriteFile, ReadDir。从 Go 1.16 开始,这些函数被移动到了 osio 包中,ioutil 包被标记为废弃。因此,我们将重点学习 os 包中的新用法。
    此外,filepath 包用于处理平台相关的路径问题(如路径分隔符),bufio 包用于带缓冲的 I/O 操作,io 包提供了基础的 I/O 接口,这些也将在本文中涉及。

一、文件基本操作

1.1 创建和写入文件

  • os.Create(name string) (*os.File, error):创建一个新文件。如果文件已存在,会清空其内容。返回一个文件对象和可能的错误。
  • file.Write(b []byte) (n int, err error):向文件写入一个字节切片。
  • file.WriteString(s string) (n int, err error):向文件写入一个字符串。
  • os.WriteFile(name string, data []byte, perm fs.FileMode) error:一个更高级的函数,它会创建文件(如果需要)、写入数据,并自动关闭文件perm 是文件权限,例如 0644

案例代码

go 复制代码
package main
import (
	"fmt"
	"os"
)
func main() {
	// --- 方法 1: 使用 os.Create 和 file.WriteString ---
	// 适合需要多次写入或对文件有更多控制的情况
	fmt.Println("--- 方法 1: 使用 os.Create ---")
	file, err := os.Create("example1.txt")
	if err != nil {
		fmt.Println("创建文件失败:", err)
		return
	}
	// 非常重要:使用 defer 确保文件在函数结束时被关闭
	defer file.Close()
	content := "这是第一行。\n这是第二行。"
	n, err := file.WriteString(content)
	if err != nil {
		fmt.Println("写入文件失败:", err)
		return
	}
	fmt.Printf("成功写入 %d 个字节到 example1.txt\n", n)
	// --- 方法 2: 使用 os.WriteFile (推荐用于一次性写入) ---
	// 更简洁,自动处理文件的打开、写入和关闭
	fmt.Println("\n--- 方法 2: 使用 os.WriteFile ---")
	fileName := "example2.txt"
	data := []byte("这是使用 os.WriteFile 写入的内容。\n简单又方便。")
	
	// 0644 表示:所有者可读写,组用户和其他用户只读
	err = os.WriteFile(fileName, data, 0644)
	if err != nil {
		fmt.Println("写入文件失败:", err)
		return
	}
	fmt.Printf("成功写入内容到 %s\n", fileName)
}

1.2 读取文件

  • os.Open(name string) (*os.File, error):打开一个文件用于读取。返回文件对象和错误。
  • file.Read(b []byte) (n int, err error):从文件中读取数据到字节切片 b 中。需要循环读取直到文件末尾(io.EOF 错误)。
  • os.ReadFile(name string) ([]byte, error):一个高级函数,它会打开文件、读取全部内容到内存,并自动关闭文件。非常适合读取小文件。

案例代码

go 复制代码
package main
import (
	"fmt"
	"io"
	"os"
)
func main() {
	// 假设 example2.txt 已经存在并包含内容
	fileName := "example2.txt"
	// --- 方法 1: 使用 os.Open 和 file.Read (底层读取方式) ---
	// 适合流式处理大文件,或需要精确控制读取过程
	fmt.Println("--- 方法 1: 使用 os.Open 和 file.Read ---")
	file, err := os.Open(fileName)
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer file.Close()
	// 创建一个缓冲区来存储读取的数据
	buf := make([]byte, 32) // 每次读取 32 字节
	for {
		n, err := file.Read(buf)
		if n > 0 {
			fmt.Printf("读取了 %d 字节: %s\n", n, string(buf[:n]))
		}
		if err != nil {
			if err == io.EOF {
				fmt.Println("已到达文件末尾。")
				break // 正常结束
			}
			fmt.Println("读取文件时发生错误:", err)
			return
		}
	}
	// --- 方法 2: 使用 os.ReadFile (推荐用于一次性读取) ---
	// 非常简洁,一次性读取整个文件
	fmt.Println("\n--- 方法 2: 使用 os.ReadFile ---")
	content, err := os.ReadFile(fileName)
	if err != nil {
		fmt.Println("读取文件失败:", err)
		return
	}
	fmt.Printf("文件 '%s' 的全部内容:\n%s\n", fileName, string(content))
}

1.3 路径操作

在处理文件路径时,直接使用字符串拼接是不可取的,因为不同操作系统的路径分隔符不同(Windows 是 \,Linux/macOS 是 /)。filepath 包提供了处理路径的跨平台方法。核心函数如下:

  • filepath.Join(elements ...string) string:智能地拼接路径元素,自动添加正确的分隔符。
  • filepath.Base(path string) string:获取路径中的最后一个元素(文件名或目录名)。
  • filepath.Dir(path string) string:获取路径中除最后一个元素外的目录路径。
  • filepath.Ext(path string) string:获取文件名的扩展名(包含点 .)。
  • filepath.IsAbs(path string) bool:判断路径是否是绝对路径。
  • filepath.Rel(basepath, targpath string) (string, error):获取从 basepathtargpath 的相对路径。

案例如下:

go 复制代码
package main
import (
	"fmt"
	"path/filepath"
	"runtime" // 用于获取当前操作系统
)
func main() {
	// filepath.Join: 智能拼接路径
	// 无论在什么系统下,都能生成正确的路径
	path := filepath.Join("dir1", "dir2", "myfile.txt")
	fmt.Printf("拼接后的路径: %s\n", path) // 输出: dir1/dir2/myfile.txt (在 Linux/macOS) 或 dir1\dir2\myfile.txt (在 Windows)
	// filepath.Base, Dir, Ext
	fullPath := "/home/user/go/project/main.go"
	fmt.Printf("完整路径: %s\n", fullPath)
	fmt.Printf("文件名: %s\n", filepath.Base(fullPath))       // 输出: main.go
	fmt.Printf("目录路径: %s\n", filepath.Dir(fullPath))     // 输出: /home/user/go/project
	fmt.Printf("文件扩展名: %s\n", filepath.Ext(fullPath))   // 输出: .go
	// filepath.IsAbs
	absPath := "C:\\Windows\\System32" // Windows 示例
	if runtime.GOOS == "windows" {
		fmt.Printf("'%s' 是绝对路径吗? %t\n", absPath, filepath.IsAbs(absPath)) // 输出: true
	}
	fmt.Printf("'%s' 是绝对路径吗? %t\n", "dir1/myfile.txt", filepath.IsAbs("dir1/myfile.txt")) // 输出: false
	// filepath.Rel
	base := "/a/b/c"
	target := "/a/b/c/d/e/file.txt"
	relPath, err := filepath.Rel(base, target)
	if err != nil {
		fmt.Println("计算相对路径出错:", err)
	} else {
		fmt.Printf("从 '%s' 到 '%s' 的相对路径是: %s\n", base, target, relPath) // 输出: d/e/file.txt
	}
}

二、目录基本操作

2.1 创建和删除目录

  • os.Mkdir(name string, perm fs.FileMode) error:创建单个目录。如果父目录不存在,会报错。
  • os.MkdirAll(path string, perm fs.FileMode) error:创建多级目录(包括所有不存在的父目录)。这是更常用、更安全的方法。
  • os.Remove(name string) error:删除一个文件或空目录
  • os.RemoveAll(path string) error:删除路径及其所有子目录和文件。功能非常强大,请谨慎使用!

案例代码

go 复制代码
package main
import (
	"fmt"
	"os"
)
func main() {
	// --- 创建目录 ---
	// os.Mkdir: 创建单级目录
	fmt.Println("--- 创建目录 ---")
	err := os.Mkdir("mydir", 0755) // 0755: 所有者可读写执行,其他用户可读执行
	if err != nil {
		// 如果目录已存在,会报错
		fmt.Println("创建单级目录 mydir 失败:", err)
	} else {
		fmt.Println("成功创建单级目录 mydir")
	}
	// os.MkdirAll: 创建多级目录 (推荐)
	// 即使 parent_dir 或 subdir 已存在,也不会报错
	err = os.MkdirAll("parent_dir/subdir", 0755)
	if err != nil {
		fmt.Println("创建多级目录失败:", err)
	} else {
		fmt.Println("成功创建多级目录 parent_dir/subdir")
	}
	// --- 删除目录 ---
	// os.Remove: 删除空目录
	fmt.Println("\n--- 删除目录 ---")
	err = os.Remove("mydir")
	if err != nil {
		fmt.Println("删除空目录 mydir 失败:", err)
	} else {
		fmt.Println("成功删除空目录 mydir")
	}
	// os.RemoveAll: 删除目录及其所有内容 (谨慎使用!)
	// 我们先在 parent_dir/subdir 中创建一个文件
	os.WriteFile("parent_dir/subdir/temp.txt", []byte("test"), 0644)
	
	err = os.RemoveAll("parent_dir")
	if err != nil {
		fmt.Println("删除目录树 parent_dir 失败:", err)
	} else {
		fmt.Println("成功删除目录树 parent_dir 及其所有内容")
	}
}

2.2 遍历目录

  • os.ReadDir(dirname string) ([]os.DirEntry, error):读取一个目录的内容,返回一个 os.DirEntry 切片。os.DirEntry 包含了文件名和基本信息。

案例代码

go 复制代码
package main
import (
	"fmt"
	"os"
	"path/filepath"
)
func main() {
	// 准备一个测试目录结构
	// ./test_dir/
	// ├── file1.txt
	// ├── file2.log
	// └── sub_dir/
	//     └── file_in_sub.txt
	os.MkdirAll("test_dir/sub_dir", 0755)
	os.WriteFile("test_dir/file1.txt", []byte("content1"), 0644)
	os.WriteFile("test_dir/file2.log", []byte("content2"), 0644)
	os.WriteFile("test_dir/sub_dir/file_in_sub.txt", []byte("content3"), 0644)
	defer os.RemoveAll("test_dir") // 程序结束时清理
	fmt.Println("--- 遍历目录 ---")
	dirToRead := "test_dir"
	entries, err := os.ReadDir(dirToRead)
	if err != nil {
		fmt.Println("读取目录失败:", err)
		return
	}
	fmt.Printf("目录 '%s' 下的内容:\n", dirToRead)
	for _, entry := range entries {
		// entry.Name() 获取文件/目录名
		// entry.IsDir() 判断是否是目录
		info, _ := entry.Info() // 获取更详细的文件信息
		fmt.Printf("  - 名称: %-20s | 是目录: %-5v | 大小: %d bytes\n", entry.Name(), entry.IsDir(), info.Size())
	}
}

三、高级文件操作

3.1 文件重命名与移动

在 Go 中,重命名和移动文件是同一个操作:os.Rename

  • os.Rename(oldpath, newpath string) error:将 oldpath 重命名为 newpath。如果 newpatholdpath 在同一个文件系统上,这是一个原子操作。如果在不同文件系统上,它可能需要复制和删除,行为取决于操作系统。

案例代码

go 复制代码
package main
import (
	"fmt"
	"os"
)
func main() {
	// 准备一个源文件
	sourceFile := "source.txt"
	os.WriteFile(sourceFile, []byte("This is the source file."), 0644)
	defer os.Remove(sourceFile) // 清理
	destFile := "destination.txt"
	// 重命名(移动)文件
	fmt.Printf("将文件 '%s' 重命名为 '%s'\n", sourceFile, destFile)
	err := os.Rename(sourceFile, destFile)
	if err != nil {
		fmt.Println("重命名文件失败:", err)
		return
	}
	fmt.Println("重命名成功!")
	// 验证新文件是否存在
	_, err = os.Stat(destFile)
	if err == nil {
		fmt.Printf("文件 '%s' 确实存在。\n", destFile)
	}
	defer os.Remove(destFile) // 清理
}

3.2 获取文件信息

  • os.Stat(name string) (os.FileInfo, error):获取文件或目录的详细信息,返回一个 os.FileInfo 接口。
  • os.Lstat(name string) (os.FileInfo, error):与 Stat 类似,但如果文件是符号链接,它返回链接本身的信息,而不是链接指向的文件信息。
    os.FileInfo 接口提供了以下方法:
  • Name() string:文件名。
  • Size() int64:文件大小(字节)。
  • Mode() FileMode:文件模式(权限和类型)。
  • ModTime() time.Time:修改时间。
  • IsDir() bool:是否是目录。
  • Sys() interface{}:底层数据源(不常用)。

案例代码

go 复制代码
package main
import (
	"fmt"
	"os"
	"time"
)
func main() {
	fileName := "info_test.txt"
	content := "Some content for the file."
	os.WriteFile(fileName, []byte(content), 0644)
	defer os.Remove(fileName)
	info, err := os.Stat(fileName)
	if err != nil {
		fmt.Println("获取文件信息失败:", err)
		return
	}
	fmt.Printf("--- 文件 '%s' 的详细信息 ---\n", fileName)
	fmt.Printf("名称: %s\n", info.Name())
	fmt.Printf("大小: %d bytes\n", info.Size())
	fmt.Printf("是否是目录: %t\n", info.IsDir())
	fmt.Printf("权限模式: %v\n", info.Mode()) // 例如 -rw-r--r--
	fmt.Printf("修改时间: %s\n", info.ModTime().Format(time.RFC1123))
}

3.3 bufio 包:带缓冲的读写

直接使用 file.Readfile.Write 每次都直接访问磁盘,效率较低。bufio 包在内存中创建一个缓冲区,将多次小的读写操作合并成一次大的磁盘操作,从而显著提高 I/O 性能,尤其是在处理文本文件时。

  • bufio.NewReader(rd io.Reader) *bufio.Reader:创建一个带缓冲的读取器。
  • bufio.NewWriter(wr io.Writer) *bufio.Writer:创建一个带缓冲的写入器。
  • reader.ReadString(delim byte) (string, error):读取直到遇到分隔符 delim(常用 \n 来按行读取)。
  • writer.WriteString(s string) (int, error):写入字符串到缓冲区。
  • writer.Flush() error非常重要 :将缓冲区中所有未写入的数据写入到底层的 io.Writer(即文件)。如果不调用 Flush(),部分数据可能仍然在内存中,未被写入磁盘。

案例代码

go 复制代码
package main
import (
	"bufio"
	"fmt"
	"os"
)
func main() {
	// --- 带缓冲的写入 ---
	fmt.Println("--- 带缓冲的写入 ---")
	fileName := "bufio_example.txt"
	file, err := os.Create(fileName)
	if err != nil {
		fmt.Println("创建文件失败:", err)
		return
	}
	defer file.Close()
	writer := bufio.NewWriter(file)
	
	// 写入多行数据到缓冲区
	writer.WriteString("这是第一行。\n")
	writer.WriteString("这是第二行,使用 bufio 写入。\n")
	writer.WriteString("这是第三行。\n")
	// 必须调用 Flush() 将缓冲区内容刷入文件
	fmt.Println("数据已写入缓冲区,正在调用 Flush()...")
	err = writer.Flush()
	if err != nil {
		fmt.Println("Flush 失败:", err)
		return
	}
	fmt.Println("Flush 成功,数据已写入文件。")
	// --- 带缓冲的读取 (按行读取) ---
	fmt.Println("\n--- 带缓冲的读取 ---")
	readFile, err := os.Open(fileName)
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer readFile.Close()
	reader := bufio.NewReader(readFile)
	fmt.Printf("文件 '%s' 的内容(按行读取):\n", fileName)
	lineCount := 1
	for {
		line, err := reader.ReadString('\n') // 读取直到换行符
		if err != nil {
			if err.Error() == "EOF" { // 更标准的做法是 `if err == io.EOF`
				break // 文件结束
			}
			fmt.Println("读取行时出错:", err)
			return
		}
		fmt.Printf("  行 %d: %s", lineCount, line)
		lineCount++
	}
}

四、综合案例

4.1 简单的日志分析工具

假设我们有一个日志文件 app.log,我们需要:

  1. 创建一个备份目录 backups
  2. app.log 复制到 backups 目录,并命名为 app_YYYYMMDD_HHMMSS.log
  3. 读取原始 app.log,统计包含 "ERROR" 关键字的行数。
  4. 将统计结果写入一个新的报告文件 report.txt
    准备 app.log 文件:
log 复制代码
INFO: Application started.
INFO: User logged in.
ERROR: Failed to connect to database.
INFO: Processing request.
ERROR: Invalid user input.
INFO: Request processed successfully.

Go 代码实现:

go 复制代码
package main
import (
	"bufio"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)
func main() {
	logFileName := "app.log"
	backupDir := "backups"
	reportFileName := "report.txt"
	// 0. 准备环境:确保 app.log 存在
	// 在实际应用中,这个文件应该已经存在
	if _, err := os.Stat(logFileName); os.IsNotExist(err) {
		fmt.Printf("错误: 日志文件 '%s' 不存在。请先创建它。\n", logFileName)
		// 为了让示例可运行,我们创建一个假的日志文件
		content := `INFO: Application started.
INFO: User logged in.
ERROR: Failed to connect to database.
INFO: Processing request.
ERROR: Invalid user input.
INFO: Request processed successfully.`
		os.WriteFile(logFileName, []byte(content), 0644)
		fmt.Printf("已创建示例日志文件 '%s'。\n", logFileName)
	}
	
	// 清理函数,确保每次运行都是干净的环境
	defer func() {
		os.Remove(reportFileName)
		os.RemoveAll(backupDir)
	}()
	// 1. 创建备份目录
	fmt.Println("步骤 1: 创建备份目录...")
	err := os.MkdirAll(backupDir, 0755)
	if err != nil {
		fmt.Printf("创建备份目录失败: %v\n", err)
		return
	}
	fmt.Printf("成功创建目录: %s\n", backupDir)
	// 2. 复制日志文件到备份目录
	fmt.Println("\n步骤 2: 复制日志文件到备份目录...")
	// 生成带时间戳的备份文件名
	timestamp := time.Now().Format("20060102_150405")
	backupFileName := fmt.Sprintf("app_%s.log", timestamp)
	backupPath := filepath.Join(backupDir, backupFileName)
	// 打开源文件和目标文件
	srcFile, err := os.Open(logFileName)
	if err != nil {
		fmt.Printf("打开源日志文件失败: %v\n", err)
		return
	}
	defer srcFile.Close()
	destFile, err := os.Create(backupPath)
	if err != nil {
		fmt.Printf("创建备份文件失败: %v\n", err)
		return
	}
	defer destFile.Close()
	// 使用 io.Copy 进行高效的文件复制
	bytesCopied, err := io.Copy(destFile, srcFile)
	if err != nil {
		fmt.Printf("复制文件失败: %v\n", err)
		return
	}
	fmt.Printf("成功复制 '%s' 到 '%s' (%d bytes)\n", logFileName, backupPath, bytesCopied)
	// 3. 统计 ERROR 行数
	fmt.Println("\n步骤 3: 统计 ERROR 行数...")
	srcFile.Seek(0, 0) // 将文件指针重置到文件开头,以便重新读取
	scanner := bufio.NewScanner(srcFile)
	errorCount := 0
	for scanner.Scan() {
		if strings.Contains(scanner.Text(), "ERROR") {
			errorCount++
		}
	}
	if err := scanner.Err(); err != nil {
		fmt.Printf("扫描文件时出错: %v\n", err)
		return
	}
	fmt.Printf("在 '%s' 中找到 %d 条 ERROR 记录。\n", logFileName, errorCount)
	// 4. 写入报告文件
	fmt.Println("\n步骤 4: 写入报告文件...")
	reportContent := fmt.Sprintf("日志分析报告\n生成时间: %s\n分析文件: %s\nERROR 关键字出现次数: %d\n",
		time.Now().Format(time.RFC1123), logFileName, errorCount)
	
	err = os.WriteFile(reportFileName, []byte(reportContent), 0644)
	if err != nil {
		fmt.Printf("写入报告文件失败: %v\n", err)
		return
	}
	fmt.Printf("成功生成报告文件: %s\n", reportFileName)
	// 打印报告内容以供验证
	fmt.Println("\n--- 报告内容预览 ---")
	reportData, _ := os.ReadFile(reportFileName)
	fmt.Println(string(reportData))
}

案例总结:

  1. 总是处理错误 :Go 的文件操作函数几乎都返回 error。永远不要忽略它们,否则你的程序在遇到意外情况(如权限不足、磁盘已满、文件不存在)时会崩溃或产生不可预测的行为。
  2. 使用 defer 关闭文件 :一旦你成功打开了一个文件,立即使用 defer file.Close()。这能确保无论函数后续是正常返回还是因错误提前返回,文件资源都会被正确释放,避免资源泄漏。
  3. 使用 filepath 包处理路径 :不要硬编码路径分隔符(/\)。使用 filepath.Join() 来构建路径,使用 filepath.Ext() 来获取扩展名等,这能保证你的代码在 Windows, Linux, macOS 上都能正常工作。
  4. 选择合适的读写方式
    • 小文件 :优先使用 os.ReadFileos.WriteFile,代码简洁且不易出错。
    • 大文件或流式处理 :使用 os.Open/os.Create 结合 bufio.NewReader/bufio.NewWriter 进行逐行或按块读写,避免内存耗尽。
    • 文件复制io.Copy 是最高效、最简洁的方式。
  5. 记得 Flush() :当使用 bufio.Writer 时,写入操作是写入内存缓冲区的。在完成所有写入后,或在需要确保数据落盘的关键点,必须调用 writer.Flush()
  6. 谨慎使用 RemoveAllos.RemoveAll 会递归删除所有内容,威力巨大。在使用前,请确保路径是正确的,或者有额外的确认机制。
  7. 善用 os.Stat 检查文件是否存在 :在尝试打开或删除一个文件前,可以用 os.Stat 检查它是否存在。更地道的 Go 风格是直接尝试操作(如 os.Open),然后检查返回的错误是否为 os.IsNotExist(err)
    通过掌握 os, filepath, io, bufio 这些包,你就可以在 Go 语言中轻松应对绝大多数文件和目录操作的需求。希望这份详细的解析和案例能帮助你构建出健壮、高效且可维护的文件处理程序。
相关推荐
xurime14 分钟前
Excelize 开源十周年,发布 2.11.0 版本
golang·开源·github·excel·导出·导入·excelize·基础库
咖啡八杯21 分钟前
GoF设计模式——模板方法模式
java·后端·spring·设计模式
码事漫谈1 小时前
Loop 正在排斥人为操作
后端
爱勇宝1 小时前
办公资料反复修改、补传、交接混乱,我做了个桌面工具来解决这件事
前端·后端·程序员
2zcode1 小时前
基于MATLAB图像处理的饮料瓶灌装液位检测系统设计与实现
开发语言·图像处理·matlab
庄周de蝴蝶2 小时前
梅开二度,这次拿下系统分析师
后端·程序员
必须得开心呀3 小时前
QT解决中文乱码问题
开发语言·qt
林小帅3 小时前
NestJS v11 + Prisma v7 ESM 迁移配置解析
前端·javascript·后端
IT_陈寒3 小时前
React的setState竟然不是立刻生效的,害我调试半天
前端·人工智能·后端
这不小天嘛3 小时前
JAVA八股——redis篇
java·开发语言·redis