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 语言中轻松应对绝大多数文件和目录操作的需求。希望这份详细的解析和案例能帮助你构建出健壮、高效且可维护的文件处理程序。
相关推荐
华仔啊2 小时前
Spring 配置混乱?搞懂这两个核心组件,问题真能少一半
java·后端·spring
珍宝商店2 小时前
原生 JavaScript 方法实战指南
开发语言·前端·javascript
喂完待续2 小时前
【序列晋升】45 Spring Data Elasticsearch 实战:3 个核心方案破解索引管理与复杂查询痛点,告别低效开发
java·后端·spring·big data·spring data·序列晋升
神龙斗士2402 小时前
Java 数组的定义与使用
java·开发语言·数据结构·算法
白露与泡影2 小时前
2025互联网大厂高频Java面试真题解析
java·开发语言·面试
forever銳2 小时前
java中如何保证接口幂等性
java·后端
IT_陈寒2 小时前
告别低效!用这5个Python技巧让你的数据处理速度提升300% 🚀
前端·人工智能·后端
gopyer2 小时前
180课时吃透Go语言游戏后端开发2:Go语言中的变量
开发语言·游戏·golang·游戏后端开发
程序员NEO2 小时前
B站油管抖音一键笔记
后端