文件操作是编程中最基础也最常用的需求。Go 语言的 io 包设计优雅,通过 io.Readerio.Writer 接口实现了高度的抽象和复用。

1. 文件读取

1.1 一次性读取整个文件

package main

import (
    "fmt"
    "os"
)

func main() {
    // 方法一:os.ReadFile (推荐,Go 1.16+)
    data, err := os.ReadFile("test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))

    // 方法二:ioutil.ReadFile (已废弃,但仍可用)
    // data, err := ioutil.ReadFile("test.txt")
}

1.2 逐行读取(大文件)

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

func main() {
    file, err := os.Open("large.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()  // 确保文件关闭

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(line)
    }

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

1.3 按块读取

func main() {
    file, _ := os.Open("data.bin")
    defer file.Close()

    buffer := make([]byte, 1024)  // 每次读 1KB
    for {
        n, err := file.Read(buffer)
        if err == io.EOF {
            break  // 文件读完
        }
        if err != nil {
            panic(err)
        }
        fmt.Printf("Read %d bytes\n", n)
        // 处理 buffer[:n]
    }
}

2. 文件写入

2.1 一次性写入

func main() {
    data := []byte("Hello, World!\n")

    // 写入文件(会覆盖原文件)
    // 0644 是文件权限:rw-r--r--
    err := os.WriteFile("output.txt", data, 0644)
    if err != nil {
        panic(err)
    }
}

2.2 追加写入

func main() {
    file, err := os.OpenFile("log.txt",
        os.O_APPEND|os.O_CREATE|os.O_WRONLY,  // 追加模式
        0644)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    file.WriteString("New log entry\n")
}

2.3 使用 bufio 缓冲写入

func main() {
    file, _ := os.Create("output.txt")
    defer file.Close()

    writer := bufio.NewWriter(file)

    for i := 0; i < 1000; i++ {
        writer.WriteString(fmt.Sprintf("Line %d\n", i))
    }

    writer.Flush()  // 重要:将缓冲区内容写入文件
}

3. 文件与目录操作

3.1 检查文件是否存在

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

3.2 创建目录

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

// 创建多层目录(类似 mkdir -p)
os.MkdirAll("path/to/mydir", 0755)

3.3 遍历目录

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

func main() {
    // 方法一:filepath.Walk
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.IsDir() {
            fmt.Println("File:", path)
        }
        return nil
    })

    // 方法二:os.ReadDir (Go 1.16+)
    entries, _ := os.ReadDir(".")
    for _, entry := range entries {
        fmt.Println(entry.Name(), entry.IsDir())
    }
}

3.4 删除文件/目录

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

// 删除目录及其所有内容(类似 rm -rf)
os.RemoveAll("mydir")

4. io.Copy 文件拷贝

4.1 复制文件

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

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

    // io.Copy 高效复制
    _, err = io.Copy(destination, source)
    return err
}

4.2 显示进度的复制

type ProgressReader struct {
    reader io.Reader
    total  int64
    read   int64
}

func (pr *ProgressReader) Read(p []byte) (int, error) {
    n, err := pr.reader.Read(p)
    pr.read += int64(n)

    // 打印进度
    fmt.Printf("\rProgress: %.2f%%", float64(pr.read)/float64(pr.total)*100)

    return n, err
}

5. 网络文件下载

import (
    "io"
    "net/http"
    "os"
)

func downloadFile(url, filepath string) error {
    // 发起 HTTP GET 请求
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // 创建文件
    out, err := os.Create(filepath)
    if err != nil {
        return err
    }
    defer out.Close()

    // 将响应体复制到文件
    _, err = io.Copy(out, resp.Body)
    return err
}

func main() {
    downloadFile("https://example.com/file.zip", "file.zip")
}

6. io.Reader 和 io.Writer 接口

6.1 理解接口

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

实现了这两个接口的类型:

  • os.File
  • bytes.Buffer
  • strings.Reader
  • net.Conn
  • http.Response.Body

6.2 组合使用

import (
    "bytes"
    "io"
    "strings"
)

func main() {
    // strings.Reader -> bytes.Buffer
    reader := strings.NewReader("Hello, World!")
    var buffer bytes.Buffer

    io.Copy(&buffer, reader)

    fmt.Println(buffer.String())  // Hello, World!
}

7. 临时文件与目录

import (
    "os"
)

func main() {
    // 创建临时文件
    tmpFile, err := os.CreateTemp("", "example-*.txt")
    if err != nil {
        panic(err)
    }
    defer os.Remove(tmpFile.Name())  // 清理

    tmpFile.WriteString("Temporary data")
    tmpFile.Close()

    // 创建临时目录
    tmpDir, _ := os.MkdirTemp("", "example-")
    defer os.RemoveAll(tmpDir)
}

8. 总结

  • 读取:小文件用 os.ReadFile,大文件用 bufio.Scanner
  • 写入:小文件用 os.WriteFile,大量写入用 bufio.Writer
  • 复制:使用 io.Copy,高效且简洁
  • 接口:理解 io.Readerio.Writer,它们是 Go IO 的核心

思考题: 为什么 bufio.Writer 需要手动调用 Flush()?如果忘记调用会发生什么?


相关阅读