文章

IO 与文件系统

IO 与文件系统

包总览

核心接口/函数用途
ioReader, Writer, Copy, ReadAllIO 基础抽象
io/fsFS, File, DirEntry文件系统抽象
osOpen, Create, Mkdir, Exit操作系统交互
path/filepathJoin, Clean, Walk, Glob跨平台路径处理
bufioScanner, Reader, Writer缓冲 IO
archive/tar读写 tar 归档tar 文件处理
archive/zip读写 zip 归档zip 文件处理
compress/gzipgzip 压缩/解压gzip 流处理

io 包

核心接口

// Reader:读取数据
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer:写入数据
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Closer:关闭资源
type Closer interface {
    Close() error
}

// Seeker:随机定位
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// ReadWriter = Reader + Writer
// ReadCloser = Reader + Closer
// ReadWriteCloser = Reader + Writer + Closer
// ReadWriteSeeker = 全部

io 常用函数

// io.Copy:从 Reader 复制到 Writer(高效,零拷贝优化)
n, err := io.Copy(dst, src)

// io.CopyN:复制 N 字节
n, err := io.CopyN(dst, src, 1024)

// io.ReadAll:读取全部内容
data, err := io.ReadAll(reader)

// io.LimitReader:限制读取长度
limited := io.LimitReader(reader, 1024)  // 最多读 1024 字节

// io.MultiReader:串联多个 Reader
combined := io.MultiReader(r1, r2, r3)  // 依次读 r1→r2→r3

// io.MultiWriter:同时写入多个 Writer
tee := io.MultiWriter(w1, w2)  // 写入同时复制到 w1 和 w2

// io.TeeReader:读取的同时写入(类似 tee 命令)
tee := io.TeeReader(src, dst)  // 从 src 读,同时写一份到 dst

// io.Pipe:创建同步管道
r, w := io.Pipe()  // 写入 w 的数据可以从 r 读出

io 实用实现

// strings.Reader:把 string 当 Reader
r := strings.NewReader("hello world")

// bytes.Buffer:可读可写的缓冲区
buf := bytes.NewBuffer(nil)
buf.WriteString("hello")
buf.Write([]byte(" world"))
data := buf.Bytes()  // hello world

// bytes.Reader:把 []byte 当 Reader
r := bytes.NewReader([]byte{0x01, 0x02, 0x03})

// io.Discard:丢弃所有写入(类似 /dev/null)
io.Copy(io.Discard, reader)  // 读取并丢弃

os 包

文件操作

// 打开文件(只读)
f, err := os.Open("config.yaml")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

// 打开文件(指定权限和标志)
f, err := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
// O_RDONLY  只读
// O_WRONLY  只写
// O_RDWR    读写
// O_CREATE  不存在则创建
// O_TRUNC   打开时截断
// O_APPEND  追加写入
// O_EXCL    与 O_CREATE 一起使用,文件必须不存在
if err != nil {
    log.Fatal(err)
}
defer f.Close()

// 创建文件(等价于 OpenFile with O_CREATE|O_WRONLY|O_TRUNC, 0666)
f, err := os.Create("output.txt")

// 读取
data, err := os.ReadFile("config.yaml")  // Go 1.16+,一次性读取

// 写入
err := os.WriteFile("output.txt", []byte("hello"), 0644)  // Go 1.16+

// 获取文件信息
info, err := os.Stat("config.yaml")
fmt.Println(info.Name())       // config.yaml
fmt.Println(info.Size())       // 文件大小(字节)
fmt.Println(info.Mode())       // 文件权限
fmt.Println(info.ModTime())    // 修改时间
fmt.Println(info.IsDir())      // 是否目录

// 判断文件是否存在
if _, err := os.Stat("file.txt"); err != nil {
    if os.IsNotExist(err) {
        // 文件不存在
    }
}
// Go 1.13+ 更简洁:
if errors.Is(err, fs.ErrNotExist) {
    // 文件不存在
}

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

// 重命名/移动
err := os.Rename("old.txt", "new.txt")

目录操作

// 创建目录
os.Mkdir("dir", 0755)              // 创建单层
os.MkdirAll("a/b/c", 0755)         // 递归创建多层

// 读取目录
entries, err := os.ReadDir(".")     // Go 1.16+,返回 []fs.DirEntry
for _, entry := range entries {
    fmt.Println(entry.Name())
    info, _ := entry.Info()
    fmt.Println(info.Size())
    if entry.IsDir() {
        fmt.Println("directory")
    }
}

// 临时文件
tmpFile, err := os.CreateTemp("", "prefix-*.txt")
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()

// 临时目录
tmpDir, err := os.MkdirTemp("", "myapp-*")
defer os.RemoveAll(tmpDir)

环境变量

// 读取
val := os.Getenv("HOME")                    // 不存在返回 ""
val, exists := os.LookupEnv("DATABASE_URL") // 检查是否存在

// 设置
os.Setenv("DEBUG", "true")

// 取消
os.Unsetenv("DEBUG")

// 列出所有
for _, env := range os.Environ() {
    fmt.Println(env)  // "KEY=VALUE" 格式
}

// 展开
expanded := os.ExpandEnv("$HOME/.config/$APP")

进程与信号

// 获取进程信息
fmt.Println(os.Getpid())     // 当前进程 PID
fmt.Println(os.Getppid())    // 父进程 PID
fmt.Println(os.Getwd())      // 当前工作目录

// 退出
os.Exit(1)                   // 立即退出(不执行 defer)

// 优雅退出
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
    sig := <-sigCh
    log.Printf("received signal: %v", sig)
    // 清理逻辑
    os.Exit(0)
}()

bufio 包

Scanner(逐行/逐词读取)

// 逐行读取
f, _ := os.Open("large.log")
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

// 增大 buffer(默认 64KB,大行需要调大)
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) // 1MB buffer

// 逐词读取
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
    word := scanner.Text()
}

// 按字节扫描
scanner.Split(bufio.ScanBytes)

// 逐行读取带行号
scanner := bufio.NewScanner(os.Stdin)
lineNum := 0
for scanner.Scan() {
    lineNum++
    fmt.Printf("%d: %s\n", lineNum, scanner.Text())
}

Reader / Writer

// bufio.Reader:带缓冲读取
r := bufio.NewReader(f)
line, err := r.ReadString('\n')  // 读到换行符
line = strings.TrimRight(line, "\n")

byte, err := r.ReadByte()       // 读一个字节
r.UnreadByte()                   // 回退一个字节

// Peek:预读但不消费
peeked, err := r.Peek(4)  // 预读 4 字节,不移动读指针
if string(peeked) == "\x89PNG" {
    // 是 PNG 文件
}

// bufio.Writer:带缓冲写入
w := bufio.NewWriter(f)
w.WriteString("hello")
w.WriteByte('\n')
w.Flush()  // 必须刷新!否则数据在缓冲区中没写入
defer w.Flush()  // 或在 defer 中刷新

path/filepath 包

import "path/filepath"

// 路径拼接(跨平台安全)
p := filepath.Join("home", "user", "config.yaml")
// Linux:   "home/user/config.yaml"
// Windows: "home\\user\\config.yaml"

// 路径清理
filepath.Clean("./a/b/../c/./d")  // "a/c/d"

// 获取目录和文件名
filepath.Dir("/home/user/file.txt")    // "/home/user"
filepath.Base("/home/user/file.txt")   // "file.txt"
filepath.Ext("/home/user/file.txt")    // ".txt"

// 分割路径和扩展名
dir, file := filepath.Split("/home/user/file.txt")
// dir = "/home/user/", file = "file.txt"
ext := filepath.Ext("file.tar.gz")  // ".gz"

// 绝对路径
abs, _ := filepath.Abs("config.yaml")
abs, _ := filepath.Rel("/home/user", "/home/user/sub/file")
// abs = "sub/file"

// 通配符匹配
matches, _ := filepath.Glob("/data/*.log")
matches, _ := filepath.Glob("/data/**/*.yaml")  // 不支持 **

// 遍历目录
filepath.WalkDir("/data", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err
    }
    if d.IsDir() {
        return nil  // 跳过目录
    }
    if filepath.Ext(path) == ".yaml" {
        fmt.Println(path)
    }
    return nil
})

// 文件路径匹配(shell 模式)
matched, _ := filepath.Match("*.go", "main.go")  // true

常用文件操作模式

安全写入(先写临时文件再 rename)

func SafeWriteFile(path string, data []byte, perm os.FileMode) error {
    dir := filepath.Dir(path)

    // 创建临时文件
    tmp, err := os.CreateTemp(dir, ".tmp-*")
    if err != nil {
        return err
    }
    tmpPath := tmp.Name()

    // 写入失败时清理临时文件
    defer func() {
        if err != nil {
            os.Remove(tmpPath)
        }
    }()

    // 写入数据
    if _, err = tmp.Write(data); err != nil {
        tmp.Close()
        return err
    }

    // 确保数据刷盘
    if err = tmp.Sync(); err != nil {
        tmp.Close()
        return err
    }
    tmp.Close()

    // 原子性 rename
    return os.Rename(tmpPath, path)
}

文件监听(简单轮询)

func WatchFile(path string, interval time.Duration, onChange func()) {
    var lastMod time.Time
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for range ticker.C {
        info, err := os.Stat(path)
        if err != nil {
            continue
        }
        if info.ModTime().After(lastMod) {
            lastMod = info.ModTime()
            onChange()
        }
    }
}

行尾处理平台差异

// Windows: \r\n, Unix: \n, Old Mac: \r
// bufio.Scanner 自动处理换行符差异
// 手动读取时需要处理:
func normalizeLineEndings(data []byte) []byte {
    // 统一替换为 \n
    data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
    data = bytes.ReplaceAll(data, []byte("\r"), []byte("\n"))
    return data
}

文件权限速查

权限数值说明
rwxrwxrwx0777所有用户读写执行
rw-rw-rw-0666所有用户读写
rwxr-xr-x0755所有者读写执行,其他读执行
rw-r--r--0644所有者读写,其他只读
rwx------0700仅所有者读写执行
rw-------0600仅所有者读写
首位  = 文件类型(- 普通文件, d 目录, l 符号链接)
后9位 = 3组 rwx(所有者/组/其他)

r = 4, w = 2, x = 1
0755 = rwx(7) r-x(5) r-x(5)
0644 = rw-(6) r--(4) r--(4)