Go基础:文件与文件夹操作详解
文章目录
- 一、文件基本操作
- 1.1 创建和写入文件
- 1.2 读取文件
- 1.3 路径操作
- 二、目录基本操作
- 2.1 创建和删除目录
- 2.2 遍历目录
- 三、高级文件操作
- 3.1 文件重命名与移动
- 3.2 获取文件信息
- 3.3 `bufio` 包:带缓冲的读写
- 四、综合案例
- 4.1 简单的日志分析工具
文件和目录操作是编程中非常基础且重要的部分,无论是日志记录、配置管理、数据处理还是系统工具开发,都离不开它们。Go 语言提供了两个核心包来处理文件和目录操作:
os
包:提供了与操作系统交互的平台无关接口。它包含了创建、打开、读写、删除文件和目录等基本功能。这是进行文件操作最核心的包。io/ioutil
包:在 Go 1.16 之前,这个包提供了一些非常方便的实用函数,如ReadFile
,WriteFile
,ReadDir
。从 Go 1.16 开始,这些函数被移动到了os
和io
包中,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
。
案例代码
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)
:一个高级函数,它会打开文件、读取全部内容到内存,并自动关闭文件。非常适合读取小文件。
案例代码
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)
:获取从basepath
到targpath
的相对路径。
案例如下:
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, ExtfullPath := "/home/user/go/project/main.go"fmt.Printf("完整路径: %s\n", fullPath)fmt.Printf("文件名: %s\n", filepath.Base(fullPath)) // 输出: main.gofmt.Printf("目录路径: %s\n", filepath.Dir(fullPath)) // 输出: /home/user/go/projectfmt.Printf("文件扩展名: %s\n", filepath.Ext(fullPath)) // 输出: .go// filepath.IsAbsabsPath := "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.Relbase := "/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
:删除路径及其所有子目录和文件。功能非常强大,请谨慎使用!
案例代码
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
包含了文件名和基本信息。
案例代码
package main
import ("fmt""os""path/filepath"
)
func main() {// 准备一个测试目录结构// ./test_dir/// ├── file1.txt// ├── file2.log// └── sub_dir/// └── file_in_sub.txtos.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
。如果newpath
和oldpath
在同一个文件系统上,这是一个原子操作。如果在不同文件系统上,它可能需要复制和删除,行为取决于操作系统。
案例代码
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{}
:底层数据源(不常用)。
案例代码
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.Read
或 file.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()
,部分数据可能仍然在内存中,未被写入磁盘。
案例代码
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 := 1for {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
,我们需要:
- 创建一个备份目录
backups
。 - 将
app.log
复制到backups
目录,并命名为app_YYYYMMDD_HHMMSS.log
。 - 读取原始
app.log
,统计包含 “ERROR” 关键字的行数。 - 将统计结果写入一个新的报告文件
report.txt
。
准备app.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 代码实现:
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 := 0for 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))
}
案例总结:
- 总是处理错误:Go 的文件操作函数几乎都返回
error
。永远不要忽略它们,否则你的程序在遇到意外情况(如权限不足、磁盘已满、文件不存在)时会崩溃或产生不可预测的行为。 - 使用
defer
关闭文件:一旦你成功打开了一个文件,立即使用defer file.Close()
。这能确保无论函数后续是正常返回还是因错误提前返回,文件资源都会被正确释放,避免资源泄漏。 - 使用
filepath
包处理路径:不要硬编码路径分隔符(/
或\
)。使用filepath.Join()
来构建路径,使用filepath.Ext()
来获取扩展名等,这能保证你的代码在 Windows, Linux, macOS 上都能正常工作。 - 选择合适的读写方式:
- 小文件:优先使用
os.ReadFile
和os.WriteFile
,代码简洁且不易出错。 - 大文件或流式处理:使用
os.Open
/os.Create
结合bufio.NewReader
/bufio.NewWriter
进行逐行或按块读写,避免内存耗尽。 - 文件复制:
io.Copy
是最高效、最简洁的方式。
- 小文件:优先使用
- 记得
Flush()
:当使用bufio.Writer
时,写入操作是写入内存缓冲区的。在完成所有写入后,或在需要确保数据落盘的关键点,必须调用writer.Flush()
。 - 谨慎使用
RemoveAll
:os.RemoveAll
会递归删除所有内容,威力巨大。在使用前,请确保路径是正确的,或者有额外的确认机制。 - 善用
os.Stat
检查文件是否存在:在尝试打开或删除一个文件前,可以用os.Stat
检查它是否存在。更地道的 Go 风格是直接尝试操作(如os.Open
),然后检查返回的错误是否为os.IsNotExist(err)
。
通过掌握os
,filepath
,io
,bufio
这些包,你就可以在 Go 语言中轻松应对绝大多数文件和目录操作的需求。希望这份详细的解析和案例能帮助你构建出健壮、高效且可维护的文件处理程序。