V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 18 章 / 共 20 章

文件与 IO

os / bufio / io 接口家族、权限、临时文件、原子写

一次性读写整文件

对几 MB 以内的小文件,直接 ReadFile / WriteFile 最省事——Go 帮你处理 Open / Read / Close 整套流程。

package main

import (
    "log"
    "os"
)

func main() {
    if err := os.WriteFile("out.txt", []byte("hello\n"), 0o644); err != nil {
        log.Fatal(err)
    }
    data, err := os.ReadFile("out.txt")
    if err != nil {
        log.Fatal(err)
    }
    os.Stdout.Write(data)
}

os.OpenFile 与权限位

需要追加、控制读写模式或自定义权限时,用 os.OpenFile。Linux 权限位最常用:0o644(owner 读写,其他只读)、0o600(仅 owner 读写)、0o755(目录或可执行)。Windows 下权限位会被忽略,写文件用 0o644 即可。

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    f, err := os.OpenFile("app.log",
        os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    fmt.Fprintln(f, "boot ok")

    // 常用 flag 组合:
    //   O_RDONLY | O_RDWR | O_WRONLY  访问模式
    //   O_CREATE 不存在则创建
    //   O_TRUNC  存在则清空
    //   O_APPEND 写入时追加
    //   O_EXCL   配合 O_CREATE,存在则报错(防止覆盖)
}

按行读取大文件

bufio.Scanner 一次只持有一行,适合扫几百 MB 的日志。默认行长上限 64KB,超长行需要用 Buffer 扩。

package main

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

func main() {
    f, err := os.Open("big.log")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    sc := bufio.NewScanner(f)
    sc.Buffer(make([]byte, 1024*1024), 16*1024*1024) // 16MB 单行上限
    for sc.Scan() {
        line := sc.Text()
        fmt.Println(line)
    }
    if err := sc.Err(); err != nil {
        log.Fatal(err)
    }
}

io.Reader / io.Writer 家族

Go 用接口抽象 IO:任何能读的实现 io.Reader,能写的实现 io.Writer。标准库的核心 IO 函数都接这两个接口,所以同一段代码能处理文件、网络、内存、压缩流。

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "strings"
)

func main() {
    // 准备一个源文件
    _ = os.WriteFile("a.bin", []byte("hello world"), 0o644)

    src, err := os.Open("a.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer src.Close()
    dst, err := os.Create("b.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer dst.Close()

    n, _ := io.Copy(dst, src)
    fmt.Println("copied", n)

    // 内存里造一个 Reader
    r := strings.NewReader("hello")
    buf, _ := io.ReadAll(r)
    fmt.Println(string(buf))

    // 同时写文件 + 终端
    mw := io.MultiWriter(dst, os.Stdout)
    fmt.Fprintln(mw, "broadcast")

    // 限流读取
    lr := io.LimitReader(strings.NewReader(strings.Repeat("x", 100)), 10)
    out, _ := io.ReadAll(lr)
    fmt.Println(len(out)) // 10
}

缓冲与 Flush

频繁的小写入要包一层 bufio.Writer,能把上百次系统调用合并成几次。注意写完一定要 Flush——否则缓冲里的数据可能没落盘。

package main

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

func main() {
    f, err := os.Create("out.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    bw := bufio.NewWriter(f)
    defer bw.Flush() // 重要:不 Flush 数据可能没落盘

    for i := 0; i < 10000; i++ {
        fmt.Fprintln(bw, "line", i)
    }
}

临时文件与临时目录

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    // 临时文件,文件名带随机后缀
    f, err := os.CreateTemp("", "upload-*.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer os.Remove(f.Name())
    defer f.Close()

    // 临时目录
    dir, err := os.MkdirTemp("", "build-")
    if err != nil {
        log.Fatal(err)
    }
    defer os.RemoveAll(dir)

    fmt.Println(f.Name(), dir)
}

原子写:写临时再 rename

直接覆盖目标文件,进程中途崩溃会留下半截文件。标准做法是写到同目录的临时文件,flush 后 rename——同分区 rename 在大多数文件系统上是原子操作。

package main

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

func atomicWrite(path string, data []byte) error {
    dir := filepath.Dir(path)
    f, err := os.CreateTemp(dir, ".tmp-*")
    if err != nil {
        return err
    }
    tmp := f.Name()
    defer os.Remove(tmp) // 失败时清理

    if _, err := f.Write(data); err != nil {
        f.Close()
        return err
    }
    if err := f.Sync(); err != nil { // 落盘
        f.Close()
        return err
    }
    if err := f.Close(); err != nil {
        return err
    }
    return os.Rename(tmp, path)
}

func main() {
    if err := atomicWrite("config.json", []byte(`{"v":1}`)); err != nil {
        fmt.Println("err:", err)
        return
    }
    fmt.Println("written safely")
}

path/filepath 与目录遍历

filepath 自动适配 Windows / POSIX 的路径分隔符。WalkDir(Go 1.16+)比早期 Walk 更快,因为它复用目录条目的元数据,不会为每个文件再 Stat 一次。

package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
)

func main() {
    p := filepath.Join("data", "users", "list.json")
    fmt.Println(filepath.Dir(p), filepath.Base(p), filepath.Ext(p))

    abs, _ := filepath.Abs(p)
    fmt.Println(abs)
    fmt.Println(filepath.Clean("a/./b/../c")) // a/c

    matches, _ := filepath.Glob("*.go")
    fmt.Println(matches)

    err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() && d.Name() == ".git" {
            return filepath.SkipDir
        }
        if !d.IsDir() {
            fmt.Println(path)
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

目录管理与判断存在

package main

import (
    "errors"
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    if err := os.MkdirAll("data/cache", 0o755); err != nil {
        log.Fatal(err)
    }

    path := "data/cache"
    if info, err := os.Stat(path); err == nil {
        fmt.Println("size", info.Size(), "mode", info.Mode())
    } else if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("missing")
    }

    // 重命名 / 移动 / 删除
    _ = os.WriteFile("a.txt", []byte("x"), 0o644)
    _ = os.Rename("a.txt", "b.txt")
    _ = os.Remove("b.txt")
    _ = os.RemoveAll("data/cache") // 递归删除整目录
}

embed:把文件编进二进制

Go 1.16+ 通过 //go:embed 指令,把静态资源一起打进可执行文件。部署单二进制时非常实用。

package main

import (
    "embed"
    "io/fs"
    "log"
    "net/http"
)

//go:embed assets/* templates/*.html
var staticFS embed.FS

func main() {
    sub, _ := fs.Sub(staticFS, "assets")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
    log.Fatal(http.ListenAndServe(":8080", nil))
}