文件 I/O:Go 的文件操作全攻略
几乎所有程序都需要和文件打交道——读配置文件、写日志、处理用户上传的图片、导出数据到 CSV……文件操作是编程中最基础也最实用的技能之一。
Go 语言在文件 I/O 方面设计得非常优雅。它基于 Unix 哲学——“一切皆文件”,用统一的接口(io.Reader 和 io.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.Walk 和 filepath.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 操作:
- 读取:
ReadFile、Scanner、Read、Seek - 写入:
WriteFile、WriteString、缓冲写入 - 文件信息:
Stat、权限、修改时间 - 目录操作:创建、列出、遍历
- 路径处理:
filepath包的跨平台工具 - 临时文件:
CreateTemp、MkdirTemp
Go 的文件 I/O 基于统一的 io.Reader 和 io.Writer 接口,这让文件操作和网络操作、内存操作等使用同样的模式,学习曲线很平滑。
练习时间
- 日志文件分析器:统计日志文件中各级别(INFO/WARN/ERROR)的数量
- 文件备份工具:实现一个工具,把指定目录下的所有
.go文件打包备份 - CSV 处理:读取 CSV 文件,统计某一列的数据分布
- 目录树打印:用递归打印一个漂亮的目录树(类似
tree命令) - 文件比较器:比较两个文件是否内容相同,如果不同,输出差异位置
下一篇预告
下一篇文章,我们将学习 JSON 处理。JSON 已经成为当今 Web 开发的事实标准,Go 的 encoding/json 包提供了强大的 JSON 处理能力。我们会学习:
- 序列化和反序列化
- 结构体标签
- 自定义序列化
- 流式处理
- 常见陷阱
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。