文件IO
第20课:文件IO
生活类比
想象你是一个图书管理员:
- 打开文件 = 从书架上取下一本书,翻开阅读
- 创建文件 = 拿一本空白笔记本,开始写入内容
- 读取文件 = 逐页翻阅书中的文字
- 写入文件 = 用笔在纸上写下新内容
- 关闭文件 = 合上书本,放回书架
- 删除文件 = 把书从书架上永久移除
- 遍历目录 = 巡视整个图书馆的各个书架和分区
就像图书馆有严格的借阅规则一样,操作系统对文件也有权限控制。你需要"借书证"(权限)才能读写文件,用完后要及时"归还"(关闭),否则其他读者就无法使用。
核心概念
Go 语言通过标准库提供了丰富的文件 IO 能力,主要涉及以下包:
| 包 | 用途 |
|---|---|
os |
底层文件操作:打开、创建、删除、重命名 |
io |
通用 IO 接口:Reader、Writer |
bufio |
带缓冲的读写:Scanner、Reader、Writer |
io/ioutil(已弃用) |
Go 1.16 后功能移入 os 包 |
filepath |
跨平台路径处理:拼接、分割、匹配 |
文件操作的基本流程
打开文件 → 读/写操作 → 关闭文件
│ │
└── defer f.Close() ────┘ // 确保函数退出时关闭
三种读取方式对比
| 方式 | 特点 | 适用场景 |
|---|---|---|
os.ReadFile |
一次性读入内存 | 小文件(< 100MB) |
bufio.Scanner |
逐行扫描,内存友好 | 大文件逐行处理 |
io.ReadAll |
读取全部到 []byte |
网络响应等流式数据 |
基本语法与用法
1. 打开与创建文件
package main
import (
"fmt"
"os"
)
func main() {
// 以只读方式打开文件(文件必须存在)
f, err := os.Open("data.txt")
if err != nil {
fmt.Println("打开失败:", err)
return
}
defer f.Close() // 💡 永远用 defer 关闭文件
// 以读写方式打开(文件必须存在)
f2, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
if err != nil {
fmt.Println("打开失败:", err)
return
}
defer f2.Close()
// 创建新文件(存在则截断)
f3, err := os.Create("newfile.txt")
if err != nil {
fmt.Println("创建失败:", err)
return
}
defer f3.Close()
}
💡 提示:os.Open 等价于 os.OpenFile(name, os.O_RDONLY, 0),只能读不能写。
2. OpenFile 的标志位
// 常用标志位组合
os.O_RDONLY // 只读
os.O_WRONLY // 只写
os.O_RDWR // 读写
os.O_APPEND // 追加模式
os.O_CREATE // 文件不存在则创建
os.O_TRUNC // 打开时截断(清空)
// 追加写入
f, err := os.OpenFile("log.txt",
os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
💡 提示:权限 0644 表示所有者可读写、其他人只读(Unix 系统)。
3. 读取文件
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// ===== 方式一:一次性读取整个文件 =====
data, err := os.ReadFile("config.txt")
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println(string(data))
// ===== 方式二:按行读取(推荐大文件)=====
file, err := os.Open("access.log")
if err != nil {
fmt.Println("打开失败:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() { // 逐行扫描
lineNum++
fmt.Printf("第%d行: %s\n", lineNum, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println("扫描错误:", err)
}
}
💡 提示:bufio.Scanner 默认最大 token 长度为 64KB。超长行需调用 scanner.Buffer() 增大缓冲区。
4. 写入文件
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// ===== 方式一:一次性写入 =====
err := os.WriteFile("output.txt", []byte("Hello, Go!\n"), 0644)
if err != nil {
fmt.Println("写入失败:", err)
return
}
// ===== 方式二:使用 bufio.Writer(带缓冲)=====
f, err := os.Create("buffered.txt")
if err != nil {
fmt.Println("创建失败:", err)
return
}
defer f.Close()
writer := bufio.NewWriter(f)
for i := 1; i <= 5; i++ {
fmt.Fprintf(writer, "第%d行内容\n", i)
}
writer.Flush() // 💡 必须 Flush,否则数据留在缓冲区不会写入磁盘
// ===== 方式三:直接写入 =====
f2, _ := os.Create("direct.txt")
defer f2.Close()
f2.WriteString("直接写入字符串\n")
f2.Write([]byte("直接写入字节切片\n"))
}
💡 提示:bufio.Writer 能显著提升频繁小写入的性能,因为它减少了系统调用次数。但最后一定要 Flush()。
5. 删除与重命名
// 删除文件
err := os.Remove("temp.txt")
if err != nil {
fmt.Println("删除失败:", err)
}
// 删除目录及其所有内容
err = os.RemoveAll("temp_dir/")
// 重命名 / 移动文件
err = os.Rename("old.txt", "new.txt")
6. 路径处理(filepath 包)
package main
import (
"fmt"
"path/filepath"
)
func main() {
// 拼接路径(跨平台安全)
p := filepath.Join("data", "logs", "app.log")
fmt.Println(p) // data\logs\app.log (Windows) 或 data/logs/app.log (Linux)
// 分割路径
dir, file := filepath.Split("/home/user/doc.txt")
fmt.Println("目录:", dir) // /home/user/
fmt.Println("文件:", file) // doc.txt
// 获取扩展名
ext := filepath.Ext("report.pdf")
fmt.Println(ext) // .pdf
// 获取不带扩展名的文件名
name := filepath.Base("report.pdf")
fmt.Println(name) // report.pdf
// 绝对路径
abs, _ := filepath.Abs("relative/path")
fmt.Println(abs)
// 路径匹配(glob 模式)
matched, _ := filepath.Match("*.go", "main.go")
fmt.Println(matched) // true
}
💡 提示:永远用 filepath.Join 拼接路径,不要手动拼接 / 或 \,否则代码在跨平台时会出问题。
7. 目录操作与遍历
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// 创建目录
os.Mkdir("logs", 0755)
os.MkdirAll("logs/2024/01", 0755) // 递归创建
// 读取目录内容
entries, err := os.ReadDir(".")
if err != nil {
fmt.Println("读取目录失败:", err)
return
}
for _, entry := range entries {
info, _ := entry.Info()
fmt.Printf("%-10s %8d bytes %s\n",
entry.Name(), info.Size(), info.Mode())
}
// 递归遍历目录树
fmt.Println("\n=== 递归遍历 ===")
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
prefix := "📄"
if info.IsDir() {
prefix = "📁"
}
fmt.Printf("%s %s\n", prefix, path)
return nil
})
}
💡 提示:filepath.Walk 会访问每个文件,对超大目录树可用 filepath.WalkDir(Go 1.16+),性能更好,因为它减少了 stat 系统调用。
示例
示例:文件复制工具(难度⭐)
实现一个简单的文件复制功能,支持任意大小的文件。
package main
import (
"fmt"
"io"
"os"
)
// copyFile 复制源文件到目标路径
func copyFile(src, dst string) (int64, error) {
// 打开源文件
sourceFile, err := os.Open(src)
if err != nil {
return 0, fmt.Errorf("打开源文件失败: %w", err)
}
defer sourceFile.Close()
// 获取源文件信息(用于设置权限)
sourceInfo, err := sourceFile.Stat()
if err != nil {
return 0, fmt.Errorf("获取文件信息失败: %w", err)
}
// 创建目标文件(继承源文件权限)
destFile, err := os.OpenFile(dst,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
sourceInfo.Mode())
if err != nil {
return 0, fmt.Errorf("创建目标文件失败: %w", err)
}
defer destFile.Close()
// 使用 io.Copy 流式复制(自动处理缓冲区)
bytesWritten, err := io.Copy(destFile, sourceFile)
if err != nil {
return 0, fmt.Errorf("复制数据失败: %w", err)
}
return bytesWritten, nil
}
func main() {
// 创建测试文件
os.WriteFile("source.txt", []byte("这是源文件的内容。\n用于测试复制功能。\n"), 0644)
// 执行复制
n, err := copyFile("source.txt", "copy.txt")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("复制完成,共 %d 字节\n", n)
// 验证结果
data, _ := os.ReadFile("copy.txt")
fmt.Println("复制内容:", string(data))
// 清理
os.Remove("source.txt")
os.Remove("copy.txt")
}
复制完成,共 45 字节
复制内容: 这是源文件的内容。
用于测试复制功能。
示例:日志文件分析器(难度⭐⭐)
读取日志文件,按级别统计并输出摘要。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// LogStats 日志统计信息
type LogStats struct {
Total int
Error int
Warning int
Info int
Debug int
}
// analyzeLog 分析日志文件并返回统计结果
func analyzeLog(filename string) (*LogStats, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("打开日志文件失败: %w", err)
}
defer file.Close()
stats := &LogStats{}
scanner := bufio.NewScanner(file)
// 增大缓冲区以处理超长行
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
stats.Total++
switch {
case strings.Contains(line, "[ERROR]"):
stats.Error++
case strings.Contains(line, "[WARNING]"):
stats.Warning++
case strings.Contains(line, "[INFO]"):
stats.Info++
case strings.Contains(line, "[DEBUG]"):
stats.Debug++
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("读取日志出错: %w", err)
}
return stats, nil
}
func main() {
// 创建模拟日志文件
logContent := `2024-01-15 08:00:01 [INFO] 服务启动成功
2024-01-15 08:00:05 [DEBUG] 加载配置文件
2024-01-15 08:01:10 [WARNING] 磁盘空间不足 80%
2024-01-15 08:02:30 [ERROR] 数据库连接超时
2024-01-15 08:02:35 [INFO] 重试连接数据库
2024-01-15 08:03:00 [ERROR] 认证失败: 用户 admin
2024-01-15 08:03:01 [INFO] 请求处理完成
2024-01-15 08:04:00 [DEBUG] 缓存命中率 95%
2024-01-15 08:05:00 [WARNING] API 响应时间超过 2s
`
os.WriteFile("app.log", []byte(logContent), 0644)
// 分析日志
stats, err := analyzeLog("app.log")
if err != nil {
fmt.Println("错误:", err)
return
}
// 输出报告
fmt.Println("========== 日志分析报告 ==========")
fmt.Printf("总行数: %d\n", stats.Total)
fmt.Printf("ERROR: %d (%.1f%%)\n", stats.Error,
float64(stats.Error)/float64(stats.Total)*100)
fmt.Printf("WARNING: %d (%.1f%%)\n", stats.Warning,
float64(stats.Warning)/float64(stats.Total)*100)
fmt.Printf("INFO: %d (%.1f%%)\n", stats.Info,
float64(stats.Info)/float64(stats.Total)*100)
fmt.Printf("DEBUG: %d (%.1f%%)\n", stats.Debug,
float64(stats.Debug)/float64(stats.Total)*100)
fmt.Println("===================================")
// 清理
os.Remove("app.log")
}
========== 日志分析报告 ==========
总行数: 9
ERROR: 2 (22.2%)
WARNING: 2 (22.2%)
INFO: 3 (33.3%)
DEBUG: 2 (22.2%)
===================================
示例:目录同步工具(难度⭐⭐⭐)
实现一个简单的目录同步功能:扫描源目录,将新文件或修改过的文件复制到目标目录。
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
// FileInfo 缓存的文件信息
type FileInfo struct {
Path string
ModTime time.Time
Size int64
}
// scanDir 扫描目录,返回文件信息映射
func scanDir(dir string) (map[string]FileInfo, error) {
files := make(map[string]FileInfo)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// 计算相对路径作为键
relPath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
files[relPath] = FileInfo{
Path: path,
ModTime: info.ModTime(),
Size: info.Size(),
}
return nil
})
return files, err
}
// copyFileData 复制单个文件
func copyFileData(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
// 确保目标目录存在
dstDir := filepath.Dir(dst)
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}
// syncDir 同步源目录到目标目录
func syncDir(src, dst string) error {
fmt.Printf("同步: %s → %s\n\n", src, dst)
// 扫描两个目录
srcFiles, err := scanDir(src)
if err != nil {
return fmt.Errorf("扫描源目录失败: %w", err)
}
dstFiles, err := scanDir(dst)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("扫描目标目录失败: %w", err)
}
copied, updated, skipped := 0, 0, 0
// 遍历源目录文件
for relPath, srcInfo := range srcFiles {
dstPath := filepath.Join(dst, relPath)
dstInfo, exists := dstFiles[relPath]
switch {
case !exists:
// 新文件,复制
fmt.Printf(" [新增] %s\n", relPath)
if err := copyFileData(srcInfo.Path, dstPath); err != nil {
return fmt.Errorf("复制 %s 失败: %w", relPath, err)
}
copied++
case srcInfo.ModTime.After(dstInfo.ModTime) || srcInfo.Size != dstInfo.Size:
// 文件已修改,更新
fmt.Printf(" [更新] %s\n", relPath)
if err := copyFileData(srcInfo.Path, dstPath); err != nil {
return fmt.Errorf("更新 %s 失败: %w", relPath, err)
}
updated++
default:
// 文件未变化,跳过
skipped++
}
}
fmt.Printf("\n同步完成: 新增 %d, 更新 %d, 跳过 %d\n",
copied, updated, skipped)
return nil
}
func main() {
// 创建测试目录结构
os.MkdirAll("src_dir/subdir", 0755)
os.WriteFile("src_dir/main.go", []byte("package main\n"), 0644)
os.WriteFile("src_dir/readme.txt", []byte("README\n"), 0644)
os.WriteFile("src_dir/subdir/util.go", []byte("package util\n"), 0644)
// 创建目标目录(部分文件)
os.MkdirAll("dst_dir", 0755)
os.WriteFile("dst_dir/readme.txt", []byte("旧的README\n"), 0644)
os.WriteFile("dst_dir/old.txt", []byte("这个文件不在源目录中\n"), 0644)
// 执行同步
err := syncDir("src_dir", "dst_dir")
if err != nil {
fmt.Println("同步错误:", err)
return
}
// 验证结果
fmt.Println("\n=== 目标目录内容 ===")
filepath.Walk("dst_dir", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, _ := filepath.Rel("dst_dir", path)
prefix := "📁"
if !info.IsDir() {
prefix = "📄"
}
fmt.Printf(" %s %s\n", prefix, relPath)
return nil
})
// 清理
os.RemoveAll("src_dir")
os.RemoveAll("dst_dir")
}
同步: src_dir → dst_dir
[新增] main.go
[更新] readme.txt
[新增] subdir\util.go
同步完成: 新增 2, 更新 1, 跳过 0
=== 目标目录内容 ===
📁 .
📁 subdir
📄 subdir\util.go
📄 main.go
📄 old.txt
📄 readme.txt
实际应用场景
场景1:配置文件热加载
监控配置文件变化,自动重新加载。
package main
import (
"encoding/json"
"fmt"
"os"
"time"
)
// Config 应用配置
type Config struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
}
type ServerConfig struct {
Port int `json:"port"`
Host string `json:"host"`
}
type DatabaseConfig struct {
DSN string `json:"dsn"`
MaxOpenConn int `json:"max_open_conn"`
}
// loadConfig 从 JSON 文件加载配置
func loadConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
return &config, nil
}
// watchConfig 监控配置文件变化并重新加载
func watchConfig(filename string, interval time.Duration, callback func(*Config)) {
var lastModTime time.Time
// 初始加载
info, err := os.Stat(filename)
if err == nil {
lastModTime = info.ModTime()
if config, err := loadConfig(filename); err == nil {
callback(config)
}
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
info, err := os.Stat(filename)
if err != nil {
fmt.Printf("检查配置文件失败: %v\n", err)
continue
}
if info.ModTime().After(lastModTime) {
fmt.Printf("[%s] 检测到配置文件变化,重新加载...\n",
time.Now().Format("15:04:05"))
config, err := loadConfig(filename)
if err != nil {
fmt.Printf("重新加载失败: %v\n", err)
continue
}
lastModTime = info.ModTime()
callback(config)
}
}
}
func main() {
// 创建初始配置
configJSON := `{
"server": {
"port": 8080,
"host": "localhost"
},
"database": {
"dsn": "user:pass@tcp(localhost:3306)/mydb",
"max_open_conn": 25
}
}`
os.WriteFile("config.json", []byte(configJSON), 0644)
defer os.Remove("config.json")
// 启动配置监控
go watchConfig("config.json", 2*time.Second, func(config *Config) {
fmt.Printf(" 服务器: %s:%d\n", config.Server.Host, config.Server.Port)
fmt.Printf(" 数据库: %s (最大连接: %d)\n",
config.Database.DSN, config.Database.MaxOpenConn)
})
// 模拟运行
time.Sleep(3 * time.Second)
// 模拟配置更新
fmt.Println("\n>> 更新配置文件...")
updatedJSON := `{
"server": {
"port": 9090,
"host": "0.0.0.0"
},
"database": {
"dsn": "user:pass@tcp(db-host:3306)/mydb",
"max_open_conn": 50
}
}`
os.WriteFile("config.json", []byte(updatedJSON), 0644)
// 等待检测到变化
time.Sleep(5 * time.Second)
}
服务器: localhost:8080
数据库: user:pass@tcp(localhost:3306)/mydb (最大连接: 25)
>> 更新配置文件...
[14:30:02] 检测到配置文件变化,重新加载...
服务器: 0.0.0.0:9090
数据库: user:pass@tcp(db-host:3306)/mydb (最大连接: 50)
场景2:批量文件重命名工具
按规则批量重命名目录中的文件。
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// RenameRule 重命名规则
type RenameRule struct {
Find string // 查找字符串
Replace string // 替换字符串
}
// batchRename 批量重命名文件
func batchRename(dir string, rule RenameRule) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("读取目录失败: %w", err)
}
var renamed []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
oldName := entry.Name()
newName := strings.ReplaceAll(oldName, rule.Find, rule.Replace)
if oldName == newName {
continue // 无需重命名
}
oldPath := filepath.Join(dir, oldName)
newPath := filepath.Join(dir, newName)
// 检查目标文件是否已存在
if _, err := os.Stat(newPath); err == nil {
fmt.Printf(" [跳过] %s → %s (目标已存在)\n", oldName, newName)
continue
}
if err := os.Rename(oldPath, newPath); err != nil {
fmt.Printf(" [错误] %s: %v\n", oldName, err)
continue
}
fmt.Printf(" [重命名] %s → %s\n", oldName, newName)
renamed = append(renamed, newName)
}
return renamed, nil
}
func main() {
// 创建测试文件
os.MkdirAll("photos", 0755)
testFiles := []string{
"IMG_20240115_001.jpg",
"IMG_20240115_002.jpg",
"IMG_20240115_003.jpg",
"IMG_20240116_001.jpg",
"IMG_20240116_002.jpg",
"notes.txt",
}
for _, name := range testFiles {
os.WriteFile(filepath.Join("photos", name), []byte(""), 0644)
}
// 规则1:替换前缀
fmt.Println("=== 规则1: IMG → Photo ===")
rule1 := RenameRule{Find: "IMG_", Replace: "Photo_"}
renamed, _ := batchRename("photos", rule1)
fmt.Printf("共重命名 %d 个文件\n\n", len(renamed))
// 规则2:添加日期前缀
fmt.Println("=== 规则2: Photo_ → 2024_Vacation_ ===")
rule2 := RenameRule{Find: "Photo_", Replace: "2024_Vacation_"}
renamed, _ = batchRename("photos", rule2)
fmt.Printf("共重命名 %d 个文件\n", len(renamed))
// 显示最终结果
fmt.Println("\n=== 最终文件列表 ===")
entries, _ := os.ReadDir("photos")
for _, entry := range entries {
fmt.Printf(" %s\n", entry.Name())
}
// 清理
os.RemoveAll("photos")
}
=== 规则1: IMG → Photo ===
[重命名] IMG_20240115_001.jpg → Photo_20240115_001.jpg
[重命名] IMG_20240115_002.jpg → Photo_20240115_002.jpg
[重命名] IMG_20240115_003.jpg → Photo_20240115_003.jpg
[重命名] IMG_20240116_001.jpg → Photo_20240116_001.jpg
[重命名] IMG_20240116_002.jpg → Photo_20240116_002.jpg
共重命名 5 个文件
=== 规则2: Photo_ → 2024_Vacation_ ===
[重命名] Photo_20240115_001.jpg → 2024_Vacation_20240115_001.jpg
[重命名] Photo_20240115_002.jpg → 2024_Vacation_20240115_002.jpg
[重命名] Photo_20240115_003.jpg → 2024_Vacation_20240115_003.jpg
[重命名] Photo_20240116_001.jpg → 2024_Vacation_20240116_001.jpg
[重命名] Photo_20240116_002.jpg → 2024_Vacation_20240116_002.jpg
共重命名 5 个文件
=== 最终文件列表 ===
2024_Vacation_20240115_001.jpg
2024_Vacation_20240115_002.jpg
2024_Vacation_20240115_003.jpg
2024_Vacation_20240116_001.jpg
2024_Vacation_20240116_002.jpg
notes.txt
❓ 常见问题
Q1:为什么读取大文件时程序内存飙升?
// ❌ 错误:整个文件加载到内存
data, _ := os.ReadFile("huge.log") // 文件 10GB → 内存爆炸
// ✅ 正确:逐行读取
file, _ := os.Open("huge.log")
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// 每次只处理一行,内存占用极小
processLine(line)
}
关键点:os.ReadFile 适合小文件(< 100MB),大文件一定要用 bufio.Scanner 逐行/分块处理。
Q2:defer f.Close() 放在 err != nil 检查之前会怎样?
// ❌ 可能导致 nil 指针 panic
f, err := os.Open("file.txt")
defer f.Close() // 如果 Open 失败,f 是 nil,Close 会 panic
if err != nil {
return err
}
// ✅ 正确做法:先检查错误
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // 确保 f 不为 nil
Q3:如何处理 Windows 和 Linux 的路径差异?
// ❌ 硬编码路径分隔符
path := "data" + "/" + "file.txt" // Linux 正常,Windows 有风险
path := "data" + "\\" + "file.txt" // Windows 正常,Linux 失败
// ✅ 使用 filepath.Join
path := filepath.Join("data", "file.txt")
// ✅ 使用原始字符串(Windows 路径)
path := `C:\Users\admin\file.txt`
// ✅ 从绝对路径获取相对路径
rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
// rel = "docs/file.txt"
Q4:写入文件时数据丢失了怎么办?
// 原因:使用了 bufio.Writer 但忘记 Flush
writer := bufio.NewWriter(f)
writer.WriteString("重要数据")
// 程序崩溃或退出 → 数据还在缓冲区,未写入磁盘
// ✅ 解决方案1:始终 defer Flush
defer writer.Flush()
// ✅ 解决方案2:关键数据直接写入
f.WriteString("重要数据") // 直接写入,不经过缓冲
// ✅ 解决方案3:写入后立即同步到磁盘
f.Sync() // 调用 fsync 系统调用
📖 小节
本课学习了 Go 语言文件 IO 的核心知识:
| 知识点 | 要点 |
|---|---|
os.Open/Create/Remove |
底层文件操作,需要手动 defer Close() |
os.ReadFile/WriteFile |
Go 1.16+ 推荐的一次性读写方式 |
bufio.Scanner |
逐行读取大文件的最佳选择 |
bufio.Writer |
频繁小写入的性能优化方案,别忘了 Flush() |
filepath.Join |
跨平台路径拼接的正确方式 |
filepath.Walk/WalkDir |
递归遍历目录树 |
io.Copy |
流式复制,自动管理缓冲区 |
核心原则:
- 永远
defer f.Close()— 在错误检查之后 - 小文件用
os.ReadFile,大文件用bufio.Scanner - 路径拼接用
filepath.Join— 不要手动拼接分隔符 bufio.Writer必须Flush()— 否则数据可能丢失- 错误处理不能省 — 文件操作的错误尤其重要
📝 作业
练习1:单词计数器
编写程序读取一个文本文件,统计其中的单词数量、行数和字符数,并输出结果。
// 提示:
// - 使用 bufio.Scanner 逐行读取
// - 使用 strings.Fields() 将每行分割为单词
// - 统计三种数据并格式化输出
练习2:CSV 转 JSON
编写程序读取一个 CSV 文件(第一行为表头),将其转换为 JSON 数组格式并写入新文件。
// 提示:
// - 使用 bufio.Scanner 逐行读取 CSV
// - 第一行作为 JSON 对象的键
// - 后续行作为值,用 strings.Split 分割
// - 使用 encoding/json 序列化输出
练习3:目录大小计算器
编写程序递归计算指定目录的总大小,并按文件类型(扩展名)分组统计。
// 提示:
// - 使用 filepath.WalkDir 遍历目录
// - 用 map[string]int64 按扩展名累计大小
// - 使用 humanize 格式化输出(如 1.5MB、230KB)
// - 处理无扩展名的文件(归类为 "无扩展名")
下一课
下一课我们将学习 JSON 处理 —— 如何在 Go 中解析和生成 JSON 数据,这是构建 API 和处理配置文件的基础技能。



