404 Not Found

404 Not Found


nginx

文件IO

第20课:文件IO

生活类比

想象你是一个图书管理员:

就像图书馆有严格的借阅规则一样,操作系统对文件也有权限控制。你需要"借书证"(权限)才能读写文件,用完后要及时"归还"(关闭),否则其他读者就无法使用。


核心概念

Go 语言通过标准库提供了丰富的文件 IO 能力,主要涉及以下包:

用途
os 底层文件操作:打开、创建、删除、重命名
io 通用 IO 接口:ReaderWriter
bufio 带缓冲的读写:ScannerReaderWriter
io/ioutil(已弃用) Go 1.16 后功能移入 os
filepath 跨平台路径处理:拼接、分割、匹配

文件操作的基本流程

TEXT
打开文件 → 读/写操作 → 关闭文件
   │                       │
   └── defer f.Close() ────┘   // 确保函数退出时关闭

三种读取方式对比

方式 特点 适用场景
os.ReadFile 一次性读入内存 小文件(< 100MB)
bufio.Scanner 逐行扫描,内存友好 大文件逐行处理
io.ReadAll 读取全部到 []byte 网络响应等流式数据

基本语法与用法

1. 打开与创建文件

GO
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 的标志位

GO
// 常用标志位组合
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. 读取文件

GO
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. 写入文件

GO
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. 删除与重命名

GO
// 删除文件
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 包)

GO
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. 目录操作与遍历

GO
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 系统调用。


示例

示例:文件复制工具(难度⭐)

实现一个简单的文件复制功能,支持任意大小的文件。

GO
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")
}
▶ 试一试
TEXT
复制完成,共 45 字节
复制内容: 这是源文件的内容。
用于测试复制功能。

示例:日志文件分析器(难度⭐⭐)

读取日志文件,按级别统计并输出摘要。

GO
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")
}
▶ 试一试
TEXT
========== 日志分析报告 ==========
总行数:  9
ERROR:   2 (22.2%)
WARNING: 2 (22.2%)
INFO:    3 (33.3%)
DEBUG:   2 (22.2%)
===================================

示例:目录同步工具(难度⭐⭐⭐)

实现一个简单的目录同步功能:扫描源目录,将新文件或修改过的文件复制到目标目录。

GO
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")
}
▶ 试一试
TEXT
同步: 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:配置文件热加载

监控配置文件变化,自动重新加载。

GO
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)
}
TEXT
  服务器: 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:批量文件重命名工具

按规则批量重命名目录中的文件。

GO
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")
}
TEXT
=== 规则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:为什么读取大文件时程序内存飙升?

GO
// ❌ 错误:整个文件加载到内存
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 检查之前会怎样?

GO
// ❌ 可能导致 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 的路径差异?

GO
// ❌ 硬编码路径分隔符
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:写入文件时数据丢失了怎么办?

GO
// 原因:使用了 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 流式复制,自动管理缓冲区

核心原则

  1. 永远 defer f.Close() — 在错误检查之后
  2. 小文件用 os.ReadFile,大文件用 bufio.Scanner
  3. 路径拼接用 filepath.Join — 不要手动拼接分隔符
  4. bufio.Writer 必须 Flush() — 否则数据可能丢失
  5. 错误处理不能省 — 文件操作的错误尤其重要

📝 作业

练习1:单词计数器

编写程序读取一个文本文件,统计其中的单词数量、行数和字符数,并输出结果。

GO
// 提示:
// - 使用 bufio.Scanner 逐行读取
// - 使用 strings.Fields() 将每行分割为单词
// - 统计三种数据并格式化输出

练习2:CSV 转 JSON

编写程序读取一个 CSV 文件(第一行为表头),将其转换为 JSON 数组格式并写入新文件。

GO
// 提示:
// - 使用 bufio.Scanner 逐行读取 CSV
// - 第一行作为 JSON 对象的键
// - 后续行作为值,用 strings.Split 分割
// - 使用 encoding/json 序列化输出

练习3:目录大小计算器

编写程序递归计算指定目录的总大小,并按文件类型(扩展名)分组统计。

GO
// 提示:
// - 使用 filepath.WalkDir 遍历目录
// - 用 map[string]int64 按扩展名累计大小
// - 使用 humanize 格式化输出(如 1.5MB、230KB)
// - 处理无扩展名的文件(归类为 "无扩展名")

下一课

下一课我们将学习 JSON 处理 —— 如何在 Go 中解析和生成 JSON 数据,这是构建 API 和处理配置文件的基础技能。

👉 第21课:JSON处理

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏