文件 I/O:Go 的文件操作全攻略

全面掌握 Go 语言的文件操作:读写文件、处理目录、缓冲区、临时文件等

文件 I/O:Go 的文件操作全攻略

几乎所有程序都需要和文件打交道——读配置文件、写日志、处理用户上传的图片、导出数据到 CSV……文件操作是编程中最基础也最实用的技能之一。

Go 语言在文件 I/O 方面设计得非常优雅。它基于 Unix 哲学——“一切皆文件”,用统一的接口(io.Readerio.Writer)处理各种 I/O 操作。这意味着你学会了文件读写,也就学会了网络通信、内存缓冲等等。

今天我们就来全面学习 Go 的文件操作,从最简单的读写开始,一直到高级的缓冲、临时文件、文件监控等话题。

读取文件

一次性读取整个文件

对于小文件,最简单的方式是一次性读取所有内容:

package main

import (
	"fmt"
	"os"
)

func main() {
	// ReadFile 一次性读取整个文件(Go 1.16+)
	data, err := os.ReadFile("example.txt")
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}
	fmt.Println(string(data))
}

⚠️ 注意os.ReadFile 会把整个文件加载到内存中。对于大文件(比如几个 GB 的日志文件),这种方式会让内存爆炸。

逐行读取

处理大文件时,更常见的方式是逐行读取:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		fmt.Printf("%4d: %s\n", lineNum, line)
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("读取错误:", err)
	}
}

bufio.Scanner 是一个行扫描器,默认按换行符分割。它内部使用缓冲区,性能很好。

按字节块读取

对于非常大的文件或者需要精细控制的场景,可以按块读取:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer file.Close()

	buf := make([]byte, 1024)  // 1KB 缓冲区
	totalBytes := 0

	for {
		n, err := file.Read(buf)
		if err != nil {
			if err == io.EOF {
				break  // 文件结束
			}
			fmt.Println("读取错误:", err)
			return
		}
		totalBytes += n
		// 处理 buf[:n] 中的数据
	}

	fmt.Printf("总共读取了 %d 字节\n", totalBytes)
}

随机读取(Seek)

有时候你需要跳到文件的特定位置读取:

file, _ := os.Open("example.txt")
defer file.Close()

// 跳到文件开头后 100 字节的位置
file.Seek(100, io.SeekStart)

// 从当前位置向后跳 50 字节
file.Seek(50, io.SeekCurrent)

// 从文件末尾向前 20 字节
file.Seek(-20, io.SeekEnd)

buf := make([]byte, 10)
file.Read(buf)

Seek 的三个常量:

  • io.SeekStart:从文件开头算起
  • io.SeekCurrent:从当前位置算起
  • io.SeekEnd:从文件末尾算起

写入文件

一次性写入

package main

import (
	"fmt"
	"os"
)

func main() {
	content := []byte("Hello, Go!\n这是第二行\n")

	// WriteFile 一次性写入,权限 0644
	err := os.WriteFile("output.txt", content, 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
	}
}

⚠️ 注意os.WriteFile 会覆盖已有文件的内容。

追加写入

file, err := os.OpenFile("output.txt", os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
	fmt.Println("打开文件失败:", err)
	return
}
defer file.Close()

file.WriteString("追加的内容\n")

文件打开模式

os.OpenFile 的第二个参数是标志位,可以用 | 组合:

标志说明
os.O_RDONLY只读
os.O_WRONLY只写
os.O_RDWR读写
os.O_CREATE如果文件不存在则创建
os.O_APPEND追加模式
os.O_TRUNC打开时截断文件
os.O_EXCL和 O_CREATE 一起使用,文件存在则报错

使用缓冲写入

频繁的磁盘写入性能很差。用 bufio.Writer 可以缓冲写入,提升性能:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("output.txt")
	if err != nil {
		fmt.Println("创建文件失败:", err)
		return
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	defer writer.Flush()  // 重要!确保缓冲区内容写入磁盘

	for i := 0; i < 1000; i++ {
		fmt.Fprintf(writer, "第 %d 行\n", i+1)
	}

	// Flush 在 defer 中调用,确保所有数据都写入
}

⚠️ 重要:使用 bufio.Writer 时,一定要在合适的时候调用 Flush(),否则缓冲区中的数据可能不会被写入磁盘。

文件信息

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	info, err := os.Stat("example.txt")
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("文件不存在")
		} else {
			fmt.Println("获取信息失败:", err)
		}
		return
	}

	fmt.Printf("文件名: %s\n", info.Name())
	fmt.Printf("大小: %d 字节\n", info.Size())
	fmt.Printf("权限: %v\n", info.Mode())
	fmt.Printf("修改时间: %v\n", info.ModTime().Format(time.RFC3339))
	fmt.Printf("是否目录: %v\n", info.IsDir())
}

检查文件是否存在

Go 没有直接的 exists() 函数,但可以通过 os.Stat 判断:

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

func isDir(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}
	return info.IsDir()
}

目录操作

创建目录

// 创建单个目录
os.Mkdir("mydir", 0755)

// 递归创建目录(包括所有不存在的父目录)
os.MkdirAll("a/b/c/d", 0755)

列出目录内容

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println("读取目录失败:", err)
		return
	}

	for _, entry := range entries {
		info, _ := entry.Info()
		if entry.IsDir() {
			fmt.Printf("📁 %s\n", entry.Name())
		} else {
			fmt.Printf("📄 %s (%d bytes)\n", entry.Name(), info.Size())
		}
	}
}

遍历目录

filepath.Walkfilepath.WalkDir(Go 1.16+)可以递归遍历目录:

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	root := "./myproject"

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() {
			fmt.Printf("📁 %s\n", path)
		} else {
			info, _ := d.Info()
			fmt.Printf("📄 %s (%d bytes)\n", path, info.Size())
		}

		return nil
	})

	if err != nil {
		fmt.Println("遍历失败:", err)
	}
}

路径处理

path/filepath 包提供了跨平台的路径处理工具:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	// 拼接路径(跨平台)
	path := filepath.Join("home", "user", "documents", "file.txt")
	fmt.Println(path)  // home/user/documents/file.txt(Linux/macOS)
	                   // home\user\documents\file.txt(Windows)

	// 拆分路径
	dir, file := filepath.Split("/home/user/file.txt")
	fmt.Printf("dir: %s, file: %s\n", dir, file)

	// 获取扩展名
	ext := filepath.Ext("image.jpg")
	fmt.Println(ext)  // .jpg

	// 获取绝对路径
	abs, _ := filepath.Abs("./file.txt")
	fmt.Println(abs)

	// 匹配通配符
	matched, _ := filepath.Match("*.go", "main.go")
	fmt.Println(matched)  // true

	// 清理路径
	cleaned := filepath.Clean("./a/../b/./c")
	fmt.Println(cleaned)  // b/c
}

⚠️ 小贴士:始终使用 filepath.Join 来拼接路径,而不是用字符串拼接。这样可以保证跨平台兼容性。

复制、移动和删除文件

复制文件

func copyFile(src, dst string) error {
	srcFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcFile.Close()

	dstFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer dstFile.Close()

	_, err = io.Copy(dstFile, srcFile)
	return err
}

移动/重命名文件

os.Rename("old.txt", "new.txt")

删除文件

// 删除单个文件
os.Remove("file.txt")

// 递归删除目录及其内容
os.RemoveAll("mydir")

临时文件

os 包和 io/ioutil 包提供了创建临时文件的便捷方式:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 创建临时文件
	tmpFile, err := os.CreateTemp("", "example-*.txt")
	if err != nil {
		fmt.Println("创建临时文件失败:", err)
		return
	}
	defer os.Remove(tmpFile.Name())  // 用完删除
	defer tmpFile.Close()

	fmt.Println("临时文件:", tmpFile.Name())

	tmpFile.WriteString("一些临时数据")

	// 创建临时目录
	tmpDir, err := os.MkdirTemp("", "example-*")
	if err != nil {
		fmt.Println("创建临时目录失败:", err)
		return
	}
	defer os.RemoveAll(tmpDir)

	fmt.Println("临时目录:", tmpDir)
}

实战:文本文件分析器

让我们把所有知识综合起来,写一个文本文件分析工具:

package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"
	"unicode"
)

// FileStats 文件统计信息
type FileStats struct {
	Lines       int
	Words       int
	Chars       int
	Bytes       int
	TopWords    []WordCount
}

type WordCount struct {
	Word  string
	Count int
}

func analyzeFile(path string) (*FileStats, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		return nil, err
	}

	stats := &FileStats{
		Bytes: int(info.Size()),
	}

	wordCounts := make(map[string]int)
	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		stats.Lines++
		line := scanner.Text()
		stats.Chars += len([]rune(line))

		// 分词(简单版)
		words := strings.FieldsFunc(line, func(r rune) bool {
			return !unicode.IsLetter(r) && !unicode.IsDigit(r)
		})

		for _, word := range words {
			stats.Words++
			word = strings.ToLower(word)
			wordCounts[word]++
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	// 统计 Top 10 单词
	var wc []WordCount
	for word, count := range wordCounts {
		wc = append(wc, WordCount{word, count})
	}
	sort.Slice(wc, func(i, j int) bool {
		return wc[i].Count > wc[j].Count
	})

	limit := 10
	if len(wc) < limit {
		limit = len(wc)
	}
	stats.TopWords = wc[:limit]

	return stats, nil
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("用法: analyzer <文件路径>")
		os.Exit(1)
	}

	path := os.Args[1]
	stats, err := analyzeFile(path)
	if err != nil {
		fmt.Println("分析失败:", err)
		os.Exit(1)
	}

	fmt.Printf("\n📊 文件分析: %s\n", path)
	fmt.Println(strings.Repeat("-", 40))
	fmt.Printf("字节数: %d\n", stats.Bytes)
	fmt.Printf("字符数: %d\n", stats.Chars)
	fmt.Printf("单词数: %d\n", stats.Words)
	fmt.Printf("行数:   %d\n", stats.Lines)

	fmt.Printf("\n🔝 出现最多的单词:\n")
	for i, wc := range stats.TopWords {
		fmt.Printf("%2d. %-20s %d 次\n", i+1, wc.Word, wc.Count)
	}
}

小结

今天我们全面学习了 Go 的文件 I/O 操作:

  1. 读取ReadFileScannerReadSeek
  2. 写入WriteFileWriteString、缓冲写入
  3. 文件信息Stat、权限、修改时间
  4. 目录操作:创建、列出、遍历
  5. 路径处理filepath 包的跨平台工具
  6. 临时文件CreateTempMkdirTemp

Go 的文件 I/O 基于统一的 io.Readerio.Writer 接口,这让文件操作和网络操作、内存操作等使用同样的模式,学习曲线很平滑。

练习时间

  1. 日志文件分析器:统计日志文件中各级别(INFO/WARN/ERROR)的数量
  2. 文件备份工具:实现一个工具,把指定目录下的所有 .go 文件打包备份
  3. CSV 处理:读取 CSV 文件,统计某一列的数据分布
  4. 目录树打印:用递归打印一个漂亮的目录树(类似 tree 命令)
  5. 文件比较器:比较两个文件是否内容相同,如果不同,输出差异位置

下一篇预告

下一篇文章,我们将学习 JSON 处理。JSON 已经成为当今 Web 开发的事实标准,Go 的 encoding/json 包提供了强大的 JSON 处理能力。我们会学习:

  • 序列化和反序列化
  • 结构体标签
  • 自定义序列化
  • 流式处理
  • 常见陷阱

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页