Skip to content

Go 文件处理教程

📖 关于本教程本教程覆盖 Go 文件 I/O 的方方面面:读写文件、缓冲 IO、目录遍历、压缩解压、JSON 序列化、日志系统、系统命令、正则表达式等,配合大量实战代码和注释讲解。


1. 写文件及相对路径问题

1.1 写文件基础

go
package main

import (
    "fmt"
    "os"
)

func main() {
    // ==================== 方式1:os.WriteFile(最简单)====================
    // 一次性写入整个文件,适合小文件
    data := []byte("Hello, Go!\n第二行内容\n")
    err := os.WriteFile("output.txt", data, 0644)
    if err != nil {
        fmt.Println("写入失败:", err)
        return
    }

    // ==================== 方式2:os.Create + Write ====================
    // 更灵活,可以分多次写入
    file, err := os.Create("output2.txt") // 创建或截断文件
    if err != nil {
        fmt.Println("创建失败:", err)
        return
    }
    defer file.Close() // 务必关闭文件

    file.WriteString("第一行\n")
    file.WriteString("第二行\n")
    file.Write([]byte("第三行(字节方式)\n"))

    // ==================== 方式3:os.OpenFile(最灵活)====================
    // 可以控制打开模式:追加、只写、创建等
    f, err := os.OpenFile("output3.txt",
        os.O_WRONLY|os.O_CREATE|os.O_APPEND, // 只写 | 不存在则创建 | 追加模式
        0644,
    )
    if err != nil {
        fmt.Println("打开失败:", err)
        return
    }
    defer f.Close()

    fmt.Fprintln(f, "追加的内容") // 用 fmt 写入文件
    fmt.Fprintf(f, "格式化写入: %d\n", 42)
}

常用文件打开标志:

text
标志             含义
──────────────────────────────
os.O_RDONLY     只读
os.O_WRONLY     只写
os.O_RDWR      读写
os.O_CREATE     不存在则创建
os.O_APPEND     追加模式
os.O_TRUNC      打开时清空文件
os.O_EXCL       和 O_CREATE 一起用,文件已存在则报错

文件权限说明:

text
0644 = rw-r--r--
  - 所有者:读+写
  - 同组:读
  - 其他:读

0755 = rwxr-xr-x(可执行文件/目录用)
0600 = rw-------(私密文件,如密钥)

1.2 相对路径问题

❌ Go 的相对路径是相对于「运行命令时所在的目录」,不是源码文件所在目录!

text
项目结构:
myproject/
├── cmd/
│   └── server/
│       └── main.go
├── configs/
│   └── config.yaml
└── data/
    └── output.txt
go
// cmd/server/main.go
package main

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

func main() {
    // ==================== 相对路径的坑 ====================

    // 这个路径相对于你执行命令的目录,不是 main.go 所在目录!
    // 如果在项目根目录运行:go run ./cmd/server
    // 则 "configs/config.yaml" 相对于项目根目录 ✅
    //
    // 如果 cd 到 cmd/server/ 运行:go run .
    // 则 "configs/config.yaml" 相对于 cmd/server/ ❌ 找不到!

    // 查看当前工作目录
    wd, _ := os.Getwd()
    fmt.Println("当前工作目录:", wd)

    // ==================== 解决方案1:基于可执行文件位置 ====================
    execPath, _ := os.Executable()
    execDir := filepath.Dir(execPath)
    configPath := filepath.Join(execDir, "configs", "config.yaml")
    fmt.Println("基于可执行文件:", configPath)

    // ==================== 解决方案2:基于源码文件位置(开发期)====================
    _, filename, _, _ := runtime.Caller(0)
    sourceDir := filepath.Dir(filename)
    configPath2 := filepath.Join(sourceDir, "..", "..", "configs", "config.yaml")
    fmt.Println("基于源码文件:", configPath2)

    // ==================== 解决方案3:环境变量或命令行参数(推荐)====================
    configDir := os.Getenv("CONFIG_DIR")
    if configDir == "" {
        configDir = "configs" // 默认值
    }
    configPath3 := filepath.Join(configDir, "config.yaml")
    fmt.Println("基于环境变量:", configPath3)

    // ==================== 解决方案4:嵌入文件(Go 1.16+)====================
    // 编译时直接把文件打包进二进制,彻底避免路径问题
    // 见下方 embed 示例
}
go
// ==================== embed:嵌入文件(终极方案)====================
package main

import (
    "embed"
    "fmt"
)

//go:embed configs/config.yaml
var configData []byte

//go:embed templates/*
var templateFS embed.FS

func main() {
    // 配置文件已经编译进二进制,不存在路径问题
    fmt.Println(string(configData))

    // 读取嵌入的目录
    data, _ := templateFS.ReadFile("templates/index.html")
    fmt.Println(string(data))
}

2. 读文件

go
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // ==================== 方式1:os.ReadFile(最简单)====================
    // 一次性读取整个文件到内存,适合小文件
    data, err := os.ReadFile("example.txt")
    if err != nil {
        fmt.Println("读取失败:", err)
        return
    }
    fmt.Println(string(data))

    // ==================== 方式2:os.Open + Read ====================
    // 手动控制读取过程
    file, err := os.Open("example.txt") // 只读打开
    if err != nil {
        fmt.Println("打开失败:", err)
        return
    }
    defer file.Close()

    // 固定缓冲区读取
    buf := make([]byte, 1024)
    for {
        n, err := file.Read(buf)
        if n > 0 {
            fmt.Print(string(buf[:n]))
        }
        if err == io.EOF {
            break // 读完了
        }
        if err != nil {
            fmt.Println("读取错误:", err)
            break
        }
    }

    // ==================== 方式3:io.ReadAll ====================
    // 从任意 Reader 读取全部内容
    file2, _ := os.Open("example.txt")
    defer file2.Close()

    all, err := io.ReadAll(file2)
    if err != nil {
        fmt.Println("ReadAll 失败:", err)
        return
    }
    fmt.Println(string(all))

    // ==================== 读取文件信息 ====================
    info, err := os.Stat("example.txt")
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("文件不存在")
        }
        return
    }
    fmt.Printf("文件名: %s\n", info.Name())
    fmt.Printf("大小: %d 字节\n", info.Size())
    fmt.Printf("权限: %s\n", info.Mode())
    fmt.Printf("修改时间: %s\n", info.ModTime())
    fmt.Printf("是否是目录: %v\n", info.IsDir())
}

3. 用 bufio 读写文件

3.1 bufio 读文件

go
package main

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

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    // ==================== 按行读取(最常用)====================
    scanner := bufio.NewScanner(file)
    lineNum := 0
    for scanner.Scan() {
        lineNum++
        line := scanner.Text() // 获取当前行(不含换行符)
        fmt.Printf("%3d: %s\n", lineNum, line)
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("扫描错误:", err)
    }

    // ⚠️ Scanner 默认最大行长度 64KB,超长行会报错
    // 解决:设置更大的缓冲区
    file.Seek(0, 0) // 重置文件指针到开头
    scanner2 := bufio.NewScanner(file)
    scanner2.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB 缓冲区
    for scanner2.Scan() {
        _ = scanner2.Text()
    }

    // ==================== 按单词读取 ====================
    file.Seek(0, 0)
    scanner3 := bufio.NewScanner(file)
    scanner3.Split(bufio.ScanWords) // 按单词分割(空格/换行分隔)
    for scanner3.Scan() {
        fmt.Printf("单词: %q\n", scanner3.Text())
    }

    // ==================== 按字节读取 ====================
    file.Seek(0, 0)
    scanner4 := bufio.NewScanner(file)
    scanner4.Split(bufio.ScanBytes)
    for scanner4.Scan() {
        fmt.Printf("%x ", scanner4.Bytes()[0])
    }
    fmt.Println()

    // ==================== 按 rune 读取(支持中文)====================
    file.Seek(0, 0)
    scanner5 := bufio.NewScanner(file)
    scanner5.Split(bufio.ScanRunes)
    for scanner5.Scan() {
        fmt.Printf("%s", scanner5.Text())
    }

    // ==================== 自定义分隔符 ====================
    // 按 "---" 分割
    input := "part1---part2---part3"
    scanner6 := bufio.NewScanner(strings.NewReader(input))
    scanner6.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        if atEOF && len(data) == 0 {
            return 0, nil, nil
        }
        if i := strings.Index(string(data), "---"); i >= 0 {
            return i + 3, data[:i], nil
        }
        if atEOF {
            return len(data), data, nil
        }
        return 0, nil, nil // 需要更多数据
    })
    for scanner6.Scan() {
        fmt.Printf("段落: %q\n", scanner6.Text())
    }
    // 段落: "part1"
    // 段落: "part2"
    // 段落: "part3"

    // ==================== bufio.Reader 逐行读取(替代方案)====================
    file.Seek(0, 0)
    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n') // 读到换行符为止
        if len(line) > 0 {
            line = strings.TrimRight(line, "\r\n")
            fmt.Println("行:", line)
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("错误:", err)
            break
        }
    }
}

3.2 bufio 写文件

go
package main

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

func main() {
    file, err := os.Create("buffered_output.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    // 创建带缓冲的 Writer
    // 默认缓冲区 4096 字节,数据先写入缓冲区,满了才刷到磁盘
    writer := bufio.NewWriter(file)

    // 也可以指定缓冲区大小
    // writer := bufio.NewWriterSize(file, 8192) // 8KB 缓冲

    // 写入数据(先到缓冲区,还没落盘)
    writer.WriteString("第一行\n")
    writer.WriteString("第二行\n")

    fmt.Println("缓冲区中未刷新的字节:", writer.Buffered())   // > 0
    fmt.Println("缓冲区剩余可用空间:", writer.Available())

    // 更多写入
    for i := 0; i < 100; i++ {
        fmt.Fprintf(writer, "行 %d: 这是一些数据内容\n", i)
    }

    // ⚠️ 必须调用 Flush,否则缓冲区中的数据会丢失!
    err = writer.Flush()
    if err != nil {
        fmt.Println("Flush 失败:", err)
    }

    fmt.Println("写入完成")
}

⚠️ bufio.Writer 的关键注意事项 Flush() 必须调用!如果忘记调用,缓冲区中的数据不会写入文件。最佳实践是用 defer:先 defer file.Close(),再 defer writer.Flush()(defer 是后进先出,Flush 会在 Close 之前执行)。


4. 练习:自行实现带缓冲的 FileWriter

💡 要求实现一个 BufferedFileWriter,内部维护一个字节缓冲区,写入时先存缓冲区,缓冲区满了自动刷新到文件。支持 WriteWriteStringFlushClose 方法。

go
package main

import (
    "fmt"
    "os"
)

// BufferedFileWriter 带缓冲的文件写入器
type BufferedFileWriter struct {
    file    *os.File
    buf     []byte // 缓冲区
    size    int    // 缓冲区容量
    offset  int    // 当前已写入缓冲区的字节数
}

// NewBufferedFileWriter 创建带缓冲的文件写入器
// path: 文件路径
// bufSize: 缓冲区大小(字节)
func NewBufferedFileWriter(path string, bufSize int) (*BufferedFileWriter, error) {
    if bufSize <= 0 {
        bufSize = 4096 // 默认 4KB
    }

    file, err := os.Create(path)
    if err != nil {
        return nil, fmt.Errorf("create file: %w", err)
    }

    return &BufferedFileWriter{
        file:   file,
        buf:    make([]byte, bufSize),
        size:   bufSize,
        offset: 0,
    }, nil
}

// Write 实现 io.Writer 接口
func (w *BufferedFileWriter) Write(p []byte) (int, error) {
    totalWritten := 0
    remaining := p

    for len(remaining) > 0 {
        // 计算缓冲区剩余空间
        space := w.size - w.offset

        if len(remaining) < space {
            // 剩余数据能全部放进缓冲区
            copy(w.buf[w.offset:], remaining)
            w.offset += len(remaining)
            totalWritten += len(remaining)
            remaining = nil
        } else {
            // 先填满缓冲区
            copy(w.buf[w.offset:], remaining[:space])
            w.offset = w.size
            totalWritten += space
            remaining = remaining[space:]

            // 缓冲区满了,刷新到文件
            if err := w.Flush(); err != nil {
                return totalWritten, err
            }
        }
    }

    return totalWritten, nil
}

// WriteString 写入字符串
func (w *BufferedFileWriter) WriteString(s string) (int, error) {
    return w.Write([]byte(s))
}

// Flush 将缓冲区内容刷新到文件
func (w *BufferedFileWriter) Flush() error {
    if w.offset == 0 {
        return nil // 缓冲区为空,无需刷新
    }

    // 将缓冲区数据写入文件
    _, err := w.file.Write(w.buf[:w.offset])
    if err != nil {
        return fmt.Errorf("flush to file: %w", err)
    }

    w.offset = 0 // 重置偏移量
    return nil
}

// Buffered 返回缓冲区中未刷新的字节数
func (w *BufferedFileWriter) Buffered() int {
    return w.offset
}

// Available 返回缓冲区剩余可用空间
func (w *BufferedFileWriter) Available() int {
    return w.size - w.offset
}

// Close 刷新缓冲区并关闭文件
func (w *BufferedFileWriter) Close() error {
    // 先刷新剩余数据
    if err := w.Flush(); err != nil {
        w.file.Close() // 即使 Flush 失败也要关闭文件
        return err
    }
    return w.file.Close()
}

func main() {
    // 创建带缓冲的写入器(缓冲区 64 字节,方便观察效果)
    writer, err := NewBufferedFileWriter("custom_buffered.txt", 64)
    if err != nil {
        fmt.Println("创建失败:", err)
        return
    }
    defer writer.Close()

    // 多次小写入(都在缓冲区中)
    for i := 0; i < 10; i++ {
        n, err := writer.WriteString(fmt.Sprintf("行 %02d: Hello, Buffered Writer!\n", i))
        if err != nil {
            fmt.Println("写入错误:", err)
            return
        }
        fmt.Printf("写入 %d 字节, 缓冲: %d, 可用: %d\n",
            n, writer.Buffered(), writer.Available())
    }

    // Close 会自动 Flush
    fmt.Println("写入完成")

    // 验证:读回来看看
    data, _ := os.ReadFile("custom_buffered.txt")
    fmt.Printf("\n文件内容(%d 字节):\n%s", len(data), string(data))
}

5. 文件管理和目录遍历

5.1 文件和目录操作

go
package main

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

func main() {
    // ==================== 创建目录 ====================
    os.Mkdir("testdir", 0755)              // 创建单层目录
    os.MkdirAll("a/b/c/d", 0755)          // 递归创建多层目录

    // ==================== 创建临时文件/目录 ====================
    tmpFile, _ := os.CreateTemp("", "prefix-*.txt") // 系统临时目录
    fmt.Println("临时文件:", tmpFile.Name())
    tmpFile.Close()
    defer os.Remove(tmpFile.Name())

    tmpDir, _ := os.MkdirTemp("", "myapp-")
    fmt.Println("临时目录:", tmpDir)
    defer os.RemoveAll(tmpDir)

    // ==================== 重命名 / 移动 ====================
    os.WriteFile("old.txt", []byte("hello"), 0644)
    os.Rename("old.txt", "new.txt")          // 重命名
    os.Rename("new.txt", "testdir/new.txt")  // 移动

    // ==================== 复制文件(Go 没有内置 Copy)====================
    copyFile("testdir/new.txt", "testdir/copy.txt")

    // ==================== 删除 ====================
    os.Remove("testdir/copy.txt")     // 删除单个文件
    os.RemoveAll("a")                  // 递归删除目录及内容

    // ==================== 路径操作 ====================
    path := "/home/user/docs/report.tar.gz"
    fmt.Println("Dir:", filepath.Dir(path))       // /home/user/docs
    fmt.Println("Base:", filepath.Base(path))     // report.tar.gz
    fmt.Println("Ext:", filepath.Ext(path))       // .gz
    fmt.Println("Clean:", filepath.Clean("a/../b/./c")) // b/c

    // 拼接路径(自动处理分隔符)
    joined := filepath.Join("home", "user", "docs", "file.txt")
    fmt.Println("Join:", joined) // home/user/docs/file.txt

    // 绝对路径
    abs, _ := filepath.Abs("testdir")
    fmt.Println("Abs:", abs)

    // 相对路径
    rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
    fmt.Println("Rel:", rel) // docs/file.txt

    // 匹配模式
    match, _ := filepath.Match("*.go", "main.go")
    fmt.Println("Match:", match) // true
}

// 复制文件的实现
func copyFile(src, dst string) error {
    data, err := os.ReadFile(src)
    if err != nil {
        return err
    }
    return os.WriteFile(dst, data, 0644)
}

5.2 目录遍历

go
package main

import (
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

func main() {
    root := "."

    // ==================== 方式1:filepath.Walk(经典方式)====================
    fmt.Println("=== filepath.Walk ===")
    filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // 跳过隐藏目录
        if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != "." {
            return filepath.SkipDir
        }

        indent := strings.Repeat("  ", strings.Count(path, string(os.PathSeparator)))
        if info.IsDir() {
            fmt.Printf("%s📁 %s/\n", indent, info.Name())
        } else {
            fmt.Printf("%s📄 %s (%d bytes)\n", indent, info.Name(), info.Size())
        }
        return nil
    })

    // ==================== 方式2:filepath.WalkDir(Go 1.16+,更快)====================
    // WalkDir 比 Walk 快,因为它不需要在每个文件上调用 Stat
    fmt.Println("\n=== filepath.WalkDir ===")
    filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() {
            fmt.Printf("[DIR]  %s\n", path)
        } else {
            fmt.Printf("[FILE] %s\n", path)
        }
        return nil
    })

    // ==================== 方式3:os.ReadDir(只读一层)====================
    fmt.Println("\n=== os.ReadDir ===")
    entries, _ := os.ReadDir(root)
    for _, entry := range entries {
        info, _ := entry.Info()
        if entry.IsDir() {
            fmt.Printf("📁 %-20s\n", entry.Name())
        } else {
            fmt.Printf("📄 %-20s %d bytes\n", entry.Name(), info.Size())
        }
    }

    // ==================== 方式4:filepath.Glob(模式匹配)====================
    fmt.Println("\n=== Glob ===")
    goFiles, _ := filepath.Glob("*.go")
    fmt.Println("Go 文件:", goFiles)

    allGoFiles, _ := filepath.Glob("**/*.go") // ⚠️ ** 在 Glob 中不递归!
    fmt.Println("Go 文件:", allGoFiles)

    // ==================== 实用:递归查找特定文件 ====================
    fmt.Println("\n=== 查找所有 .go 文件 ===")
    var goFileList []string
    filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        // 跳过 vendor 和 .git
        if d.IsDir() && (d.Name() == "vendor" || d.Name() == ".git") {
            return filepath.SkipDir
        }
        if !d.IsDir() && filepath.Ext(path) == ".go" {
            goFileList = append(goFileList, path)
        }
        return nil
    })
    for _, f := range goFileList {
        fmt.Println(" ", f)
    }
}

6. 文件练习:图像的分割与合并

💡 题目将一个大文件(如图片)按指定大小分割成多个小文件,然后再将这些小文件合并还原成原始文件。这是断点续传、分片上传等功能的基础。

go
package main

import (
    "crypto/md5"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "sort"
    "strings"
)

// SplitFile 将文件分割成多个小块
// chunkSize 单位为字节
func SplitFile(srcPath string, chunkSize int64) ([]string, error) {
    // 打开源文件
    src, err := os.Open(srcPath)
    if err != nil {
        return nil, fmt.Errorf("open source: %w", err)
    }
    defer src.Close()

    // 获取文件信息
    info, err := src.Stat()
    if err != nil {
        return nil, fmt.Errorf("stat: %w", err)
    }

    // 创建输出目录
    outDir := srcPath + "_parts"
    os.MkdirAll(outDir, 0755)

    // 计算分块数
    totalChunks := (info.Size() + chunkSize - 1) / chunkSize
    fmt.Printf("文件大小: %d 字节, 块大小: %d, 总块数: %d\n",
        info.Size(), chunkSize, totalChunks)

    var parts []string
    buf := make([]byte, chunkSize)

    for i := int64(0); i < totalChunks; i++ {
        // 读取一块
        n, err := src.Read(buf)
        if err != nil && err != io.EOF {
            return nil, fmt.Errorf("read chunk %d: %w", i, err)
        }
        if n == 0 {
            break
        }

        // 写入分块文件
        partName := filepath.Join(outDir, fmt.Sprintf("part_%04d", i))
        if err := os.WriteFile(partName, buf[:n], 0644); err != nil {
            return nil, fmt.Errorf("write chunk %d: %w", i, err)
        }

        parts = append(parts, partName)
        fmt.Printf("  写入分块 %d: %d 字节\n", i, n)
    }

    return parts, nil
}

// MergeFiles 将多个分块文件合并为一个文件
func MergeFiles(parts []string, dstPath string) error {
    // 按文件名排序,确保顺序正确
    sort.Strings(parts)

    // 创建目标文件
    dst, err := os.Create(dstPath)
    if err != nil {
        return fmt.Errorf("create dest: %w", err)
    }
    defer dst.Close()

    // 逐块写入
    for i, partPath := range parts {
        part, err := os.Open(partPath)
        if err != nil {
            return fmt.Errorf("open part %d: %w", i, err)
        }

        n, err := io.Copy(dst, part) // 高效拷贝,不会全部读入内存
        part.Close()
        if err != nil {
            return fmt.Errorf("copy part %d: %w", i, err)
        }
        fmt.Printf("  合并分块 %d: %d 字节\n", i, n)
    }

    return nil
}

// fileMD5 计算文件的 MD5 哈希(用于验证完整性)
func fileMD5(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer f.Close()

    h := md5.New()
    if _, err := io.Copy(h, f); err != nil {
        return "", err
    }
    return fmt.Sprintf("%x", h.Sum(nil)), nil
}

func main() {
    // 创建一个测试文件(模拟图片)
    testFile := "test_image.bin"
    createTestFile(testFile, 1024*1024) // 1MB 测试文件

    // 计算原始文件 MD5
    origMD5, _ := fileMD5(testFile)
    fmt.Printf("原始文件 MD5: %s\n\n", origMD5)

    // 分割:每块 256KB
    fmt.Println("=== 分割文件 ===")
    parts, err := SplitFile(testFile, 256*1024)
    if err != nil {
        fmt.Println("分割失败:", err)
        return
    }
    fmt.Printf("分割完成,共 %d 个分块\n\n", len(parts))

    // 合并
    fmt.Println("=== 合并文件 ===")
    mergedFile := "merged_image.bin"
    if err := MergeFiles(parts, mergedFile); err != nil {
        fmt.Println("合并失败:", err)
        return
    }

    // 验证
    mergedMD5, _ := fileMD5(mergedFile)
    fmt.Printf("\n合并文件 MD5: %s\n", mergedMD5)
    if origMD5 == mergedMD5 {
        fmt.Println("✅ 校验通过:文件完全一致")
    } else {
        fmt.Println("❌ 校验失败:文件不一致")
    }

    // 清理
    os.Remove(testFile)
    os.Remove(mergedFile)
    os.RemoveAll(testFile + "_parts")
}

// 创建测试文件
func createTestFile(path string, size int) {
    f, _ := os.Create(path)
    defer f.Close()
    // 写入可辨识的模式数据
    for i := 0; i < size; i++ {
        f.Write([]byte{byte(i % 256)})
    }
}

// 实用函数:在目录中查找所有分块文件
func findParts(dir string) ([]string, error) {
    entries, err := os.ReadDir(dir)
    if err != nil {
        return nil, err
    }
    var parts []string
    for _, e := range entries {
        if !e.IsDir() && strings.HasPrefix(e.Name(), "part_") {
            parts = append(parts, filepath.Join(dir, e.Name()))
        }
    }
    sort.Strings(parts)
    return parts, nil
}

7. 高级 IO 功能

go
package main

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

func main() {
    // ==================== io.Copy:高效拷贝 ====================
    // 内部使用 32KB 缓冲区,不会把整个文件读入内存
    src, _ := os.Open("source.txt")
    dst, _ := os.Create("dest.txt")
    n, err := io.Copy(dst, src) // dst 是 Writer,src 是 Reader
    fmt.Printf("拷贝了 %d 字节, err: %v\n", n, err)
    src.Close()
    dst.Close()

    // ==================== io.CopyN:只拷贝 N 个字节 ====================
    src2, _ := os.Open("source.txt")
    dst2, _ := os.Create("first_100.txt")
    io.CopyN(dst2, src2, 100) // 只拷贝前 100 字节
    src2.Close()
    dst2.Close()

    // ==================== io.TeeReader:读取时同时写到另一个地方 ====================
    // 类似 Linux 的 tee 命令
    original := strings.NewReader("Hello, TeeReader!")
    var buf bytes.Buffer
    tee := io.TeeReader(original, &buf) // 读 tee 时,数据同时写入 buf

    data, _ := io.ReadAll(tee)
    fmt.Println("读到:", string(data))       // Hello, TeeReader!
    fmt.Println("缓冲:", buf.String())       // Hello, TeeReader!(同时有一份)

    // ==================== io.MultiReader:串联多个 Reader ====================
    r1 := strings.NewReader("Part 1 ")
    r2 := strings.NewReader("Part 2 ")
    r3 := strings.NewReader("Part 3")
    multi := io.MultiReader(r1, r2, r3) // 依次读取三个 Reader

    all, _ := io.ReadAll(multi)
    fmt.Println("Multi:", string(all)) // Part 1 Part 2 Part 3

    // ==================== io.MultiWriter:同时写到多个地方 ====================
    var buf1, buf2 bytes.Buffer
    multiW := io.MultiWriter(&buf1, &buf2, os.Stdout)
    fmt.Fprintln(multiW, "写到三个地方") // stdout + buf1 + buf2 都有

    // ==================== io.LimitReader:限制读取量 ====================
    bigReader := strings.NewReader("abcdefghijklmnopqrstuvwxyz")
    limited := io.LimitReader(bigReader, 5)
    result, _ := io.ReadAll(limited)
    fmt.Println("Limited:", string(result)) // abcde

    // ==================== io.SectionReader:读取文件的某一段 ====================
    file, _ := os.Open("source.txt")
    defer file.Close()
    section := io.NewSectionReader(file, 10, 20) // 从偏移 10 开始,读 20 字节
    sectionData, _ := io.ReadAll(section)
    fmt.Println("Section:", string(sectionData))

    // ==================== io.Pipe:内存管道 ====================
    // 一个 goroutine 写,另一个 goroutine 读,同步通信
    pr, pw := io.Pipe()

    go func() {
        defer pw.Close()
        pw.Write([]byte("通过管道传输的数据"))
    }()

    pipeData, _ := io.ReadAll(pr)
    fmt.Println("Pipe:", string(pipeData))

    // ==================== bytes.Buffer:内存中的 Reader/Writer ====================
    var b bytes.Buffer
    b.WriteString("hello ")
    b.WriteString("world")
    fmt.Println("Buffer:", b.String())   // hello world
    fmt.Println("Len:", b.Len())          // 11

    // Buffer 同时实现了 io.Reader 和 io.Writer
    readBack := make([]byte, 5)
    b.Read(readBack)
    fmt.Println("Read:", string(readBack)) // hello
}

8. 文件的压缩和解压,Reader 和 Writer 接口

8.1 Reader 和 Writer 接口

go
// Go 的 I/O 设计哲学:一切皆是 Reader/Writer 流
//
// type Reader interface {
//     Read(p []byte) (n int, err error)
// }
//
// type Writer interface {
//     Write(p []byte) (n int, err error)
// }
//
// 实现了这两个接口的类型可以无缝组合:
// 文件、网络连接、压缩流、加密流、缓冲流...
// 它们像乐高积木一样拼接在一起
//
// 例如写压缩文件的流水线:
//   数据 → gzip.Writer → bufio.Writer → os.File
//
// 读压缩文件的流水线:
//   os.File → gzip.Reader → bufio.Reader → 数据

8.2 gzip 压缩和解压

go
package main

import (
    "compress/gzip"
    "fmt"
    "io"
    "os"
    "strings"
)

// 压缩文件
func gzipCompress(srcPath, dstPath string) error {
    // 打开源文件
    src, err := os.Open(srcPath)
    if err != nil {
        return err
    }
    defer src.Close()

    // 创建目标文件
    dst, err := os.Create(dstPath)
    if err != nil {
        return err
    }
    defer dst.Close()

    // 创建 gzip Writer(写入 dst 的数据会被自动压缩)
    gw, err := gzip.NewWriterLevel(dst, gzip.BestCompression)
    if err != nil {
        return err
    }
    defer gw.Close()

    // 设置 gzip header(可选)
    gw.Name = srcPath
    gw.Comment = "compressed by Go"

    // 将源文件内容写入 gzip Writer
    // io.Copy 内部循环 Read→Write,不会一次性读入内存
    _, err = io.Copy(gw, src)
    return err
}

// 解压文件
func gzipDecompress(srcPath, dstPath string) error {
    // 打开压缩文件
    src, err := os.Open(srcPath)
    if err != nil {
        return err
    }
    defer src.Close()

    // 创建 gzip Reader(从 src 读出的数据会被自动解压)
    gr, err := gzip.NewReader(src)
    if err != nil {
        return err
    }
    defer gr.Close()

    fmt.Printf("原始文件名: %s\n", gr.Name)
    fmt.Printf("注释: %s\n", gr.Comment)

    // 创建目标文件
    dst, err := os.Create(dstPath)
    if err != nil {
        return err
    }
    defer dst.Close()

    // 将解压后的数据写入目标文件
    _, err = io.Copy(dst, gr)
    return err
}

func main() {
    // 创建测试文件
    testData := []byte("Hello, Gzip! " + strings.Repeat("Go is awesome! ", 1000))
    os.WriteFile("test.txt", testData, 0644)

    // 压缩
    err := gzipCompress("test.txt", "test.txt.gz")
    if err != nil {
        fmt.Println("压缩失败:", err)
        return
    }

    // 比较大小
    origInfo, _ := os.Stat("test.txt")
    compInfo, _ := os.Stat("test.txt.gz")
    fmt.Printf("原始: %d 字节\n", origInfo.Size())
    fmt.Printf("压缩: %d 字节\n", compInfo.Size())
    fmt.Printf("压缩率: %.1f%%\n",
        (1-float64(compInfo.Size())/float64(origInfo.Size()))*100)

    // 解压
    err = gzipDecompress("test.txt.gz", "test_restored.txt")
    if err != nil {
        fmt.Println("解压失败:", err)
        return
    }

    fmt.Println("✅ 压缩解压完成")
}

8.3 tar.gz 打包和解包

go
package main

import (
    "archive/tar"
    "compress/gzip"
    "fmt"
    "io"
    "os"
    "path/filepath"
)

// 创建 tar.gz 压缩包
func createTarGz(srcDir, dstPath string) error {
    dst, err := os.Create(dstPath)
    if err != nil {
        return err
    }
    defer dst.Close()

    // 流水线:tar.Writer → gzip.Writer → os.File
    gw := gzip.NewWriter(dst)
    defer gw.Close()
    tw := tar.NewWriter(gw)
    defer tw.Close()

    // 遍历目录,逐个添加文件
    return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // 创建 tar header
        header, err := tar.FileInfoHeader(info, "")
        if err != nil {
            return err
        }
        // 使用相对路径
        header.Name, _ = filepath.Rel(srcDir, path)

        // 写入 header
        if err := tw.WriteHeader(header); err != nil {
            return err
        }

        // 如果是文件(非目录),写入内容
        if !info.IsDir() {
            file, err := os.Open(path)
            if err != nil {
                return err
            }
            defer file.Close()
            _, err = io.Copy(tw, file)
            return err
        }

        return nil
    })
}

// 解压 tar.gz
func extractTarGz(srcPath, dstDir string) error {
    src, err := os.Open(srcPath)
    if err != nil {
        return err
    }
    defer src.Close()

    // 流水线:os.File → gzip.Reader → tar.Reader
    gr, err := gzip.NewReader(src)
    if err != nil {
        return err
    }
    defer gr.Close()
    tr := tar.NewReader(gr)

    // 逐个读取 tar 中的文件
    for {
        header, err := tr.Next()
        if err == io.EOF {
            break // 读完了
        }
        if err != nil {
            return err
        }

        targetPath := filepath.Join(dstDir, header.Name)
        fmt.Printf("  解压: %s\n", targetPath)

        switch header.Typeflag {
        case tar.TypeDir:
            os.MkdirAll(targetPath, 0755)
        case tar.TypeReg:
            os.MkdirAll(filepath.Dir(targetPath), 0755)
            outFile, err := os.Create(targetPath)
            if err != nil {
                return err
            }
            io.Copy(outFile, tr)
            outFile.Close()
        }
    }

    return nil
}

func main() {
    // 创建 tar.gz
    err := createTarGz("./testdir", "archive.tar.gz")
    fmt.Println("打包:", err)

    // 解压 tar.gz
    err = extractTarGz("archive.tar.gz", "./extracted")
    fmt.Println("解压:", err)
}

9. JSON 序列化的若干问题

9.1 基本序列化/反序列化

go
package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type User struct {
    Name      string    `json:"name"`
    Age       int       `json:"age"`
    Email     string    `json:"email,omitempty"` // 空值时省略
    Password  string    `json:"-"`               // 永远不序列化
    CreatedAt time.Time `json:"created_at"`
}

func main() {
    // ==================== 序列化(struct → JSON)====================
    user := User{
        Name:      "Alice",
        Age:       25,
        Email:     "alice@example.com",
        Password:  "secret123",
        CreatedAt: time.Now(),
    }

    // 紧凑 JSON
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
    // {"name":"Alice","age":25,"email":"alice@example.com","created_at":"2024-01-01T12:00:00Z"}
    // 注意:Password 没有出现(因为 json:"-")

    // 格式化 JSON
    pretty, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println(string(pretty))

    // ==================== 反序列化(JSON → struct)====================
    jsonStr := `{"name":"Bob","age":30,"email":"bob@test.com"}`
    var user2 User
    err := json.Unmarshal([]byte(jsonStr), &user2)
    if err != nil {
        fmt.Println("解析失败:", err)
        return
    }
    fmt.Printf("%+v\n", user2)
}

9.2 常见问题

go
package main

import (
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

func main() {
    // ==================== 问题1:数字精度丢失 ====================
    // JSON 中数字默认解析为 float64,大整数会丢失精度
    jsonStr := `{"id": 1234567890123456789}`

    // ❌ 精度丢失
    var m map[string]interface{}
    json.Unmarshal([]byte(jsonStr), &m)
    fmt.Printf("float64: %.0f\n", m["id"].(float64))
    // 1234567890123456768(最后几位不对!)

    // ✅ 用 json.Number 保持精度
    decoder := json.NewDecoder(strings.NewReader(jsonStr))
    decoder.UseNumber()
    var m2 map[string]interface{}
    decoder.Decode(&m2)
    num := m2["id"].(json.Number)
    fmt.Println("json.Number:", num.String())    // 精确值
    intVal, _ := num.Int64()
    fmt.Println("Int64:", intVal)

    // ✅ 或者直接定义为 string tag
    type Order struct {
        ID int64 `json:"id,string"` // 前端传字符串 "123",自动转 int64
    }

    // ==================== 问题2:嵌套结构体的 omitempty ====================
    type Address struct {
        City string `json:"city"`
    }
    type Person struct {
        Name    string   `json:"name"`
        Address *Address `json:"address,omitempty"` // 指针类型才能被 omitempty 省略
    }

    p1 := Person{Name: "Alice"}
    data1, _ := json.Marshal(p1)
    fmt.Println(string(data1)) // {"name":"Alice"}(address 被省略)

    p2 := Person{Name: "Bob", Address: &Address{City: "Beijing"}}
    data2, _ := json.Marshal(p2)
    fmt.Println(string(data2)) // {"name":"Bob","address":{"city":"Beijing"}}

    // ⚠️ 注意:非指针结构体的 omitempty 不会省略空结构体
    type Person2 struct {
        Name    string  `json:"name"`
        Address Address `json:"address,omitempty"` // 值类型
    }
    p3 := Person2{Name: "Charlie"}
    data3, _ := json.Marshal(p3)
    fmt.Println(string(data3)) // {"name":"Charlie","address":{"city":""}}
    // Address 没被省略!因为空结构体不是零值 nil

    // 问题3 见下方独立代码块

    // ==================== 问题4:未知字段处理 ====================
    type Config struct {
        Name string `json:"name"`
        // 其他未知字段会被忽略(默认行为)
    }

    // 如果要禁止未知字段:
    decoder2 := json.NewDecoder(strings.NewReader(`{"name":"app","unknown":123}`))
    decoder2.DisallowUnknownFields()
    var cfg Config
    err := decoder2.Decode(&cfg)
    fmt.Println("未知字段:", err) // json: unknown field "unknown"

    // ==================== 问题5:解析到 map(动态结构)====================
    dynamicJSON := `{"name":"Alice","scores":[90,85,92],"meta":{"role":"admin"}}`
    var result map[string]interface{}
    json.Unmarshal([]byte(dynamicJSON), &result)

    // 类型断言访问嵌套数据
    name := result["name"].(string)
    scores := result["scores"].([]interface{})
    meta := result["meta"].(map[string]interface{})
    fmt.Println(name, scores, meta["role"])
}

9.3 自定义 JSON 序列化(问题3)

go
package main

import (
    "encoding/json"
    "fmt"
    "time"
)

// 自定义时间戳类型
type Timestamp struct {
    time.Time
}

// 实现 json.Marshaler 接口:序列化为 Unix 时间戳
func (t Timestamp) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.Unix())
}

// 实现 json.Unmarshaler 接口:从 Unix 时间戳反序列化
func (t *Timestamp) UnmarshalJSON(data []byte) error {
    var stamp int64
    if err := json.Unmarshal(data, &stamp); err != nil {
        return err
    }
    t.Time = time.Unix(stamp, 0)
    return nil
}

type Event struct {
    Name string    `json:"name"`
    At   Timestamp `json:"at"`
}

func main() {
    event := Event{
        Name: "Go 发布会",
        At:   Timestamp{time.Now()},
    }

    data, _ := json.Marshal(event)
    fmt.Println(string(data))
    // {"name":"Go 发布会","at":1704067200}

    var event2 Event
    json.Unmarshal(data, &event2)
    fmt.Println(event2.At.Format("2006-01-02 15:04:05"))
}

9.4 流式 JSON(Encoder/Decoder)

go
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

type LogEntry struct {
    Level   string `json:"level"`
    Message string `json:"message"`
}

func main() {
    // ==================== 写 JSON 流(每行一个 JSON 对象)====================
    file, _ := os.Create("logs.jsonl")
    encoder := json.NewEncoder(file)

    logs := []LogEntry{
        {"INFO", "server started"},
        {"WARN", "high memory usage"},
        {"ERROR", "connection timeout"},
    }

    for _, log := range logs {
        encoder.Encode(log) // 每次 Encode 自动追加换行符
    }
    file.Close()

    // ==================== 读 JSON 流 ====================
    file2, _ := os.Open("logs.jsonl")
    defer file2.Close()
    decoder := json.NewDecoder(file2)

    for decoder.More() { // 还有数据吗?
        var entry LogEntry
        if err := decoder.Decode(&entry); err != nil {
            break
        }
        fmt.Printf("[%s] %s\n", entry.Level, entry.Message)
    }

    // ==================== 从字符串读写 ====================
    var buf strings.Builder
    enc := json.NewEncoder(&buf)
    enc.SetIndent("", "  ") // 格式化输出
    enc.Encode(map[string]int{"a": 1, "b": 2})
    fmt.Println(buf.String())
}

10. log 和 slog

10.1 标准 log 包

go
package main

import (
    "log"
    "os"
)

func main() {
    // ==================== 基本用法 ====================
    log.Println("普通日志")             // 2024/01/01 12:00:00 普通日志
    log.Printf("格式化: %s=%d\n", "x", 42)

    // log.Fatal:打印日志后调用 os.Exit(1)
    // log.Fatal("致命错误") // 会退出程序!

    // log.Panic:打印日志后调用 panic
    // log.Panic("崩溃") // 会 panic!

    // ==================== 配置日志格式 ====================
    // 设置前缀
    log.SetPrefix("[MyApp] ")

    // 设置输出格式
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
    // 常用 flag:
    // log.Ldate      日期: 2024/01/01
    // log.Ltime      时间: 12:00:00
    // log.Lmicroseconds  微秒: 12:00:00.123456
    // log.Llongfile   完整路径: /home/user/main.go:10
    // log.Lshortfile  短路径: main.go:10
    // log.LUTC       使用 UTC 时间
    // log.Lmsgprefix  前缀放在消息前而非时间前

    log.Println("带格式的日志")
    // [MyApp] 2024/01/01 12:00:00 main.go:25: 带格式的日志

    // ==================== 输出到文件 ====================
    file, err := os.OpenFile("app.log",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    log.SetOutput(file) // 日志写入文件而非 stderr
    log.Println("这条日志写入文件")

    // ==================== 自定义 Logger ====================
    // 创建独立的 Logger 实例(不影响全局 log)
    errorLog := log.New(os.Stderr, "[ERROR] ",
        log.Ldate|log.Ltime|log.Lshortfile)
    infoLog := log.New(os.Stdout, "[INFO] ",
        log.Ldate|log.Ltime)

    errorLog.Println("这是错误日志")
    infoLog.Println("这是信息日志")

    // 同时输出到文件和终端
    // multiWriter := io.MultiWriter(os.Stdout, file)
    // log.SetOutput(multiWriter)
}

10.2 slog 结构化日志(Go 1.21+)

go
package main

import (
    "context"
    "log/slog"
    "os"
)

func main() {
    // ==================== 基本用法 ====================
    slog.Info("服务启动",
        "port", 8080,
        "env", "production",
    )
    // 输出: 2024/01/01 12:00:00 INFO 服务启动 port=8080 env=production

    slog.Warn("内存使用率高", "usage", 85.5)
    slog.Error("连接失败", "err", "timeout", "host", "db.example.com")

    // ==================== JSON 格式输出 ====================
    jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelDebug, // 设置最低日志级别
    })
    jsonLogger := slog.New(jsonHandler)

    jsonLogger.Info("用户登录",
        "user_id", 12345,
        "ip", "192.168.1.1",
    )
    // {"time":"2024-01-01T12:00:00Z","level":"INFO","msg":"用户登录","user_id":12345,"ip":"192.168.1.1"}

    // ==================== 文本格式输出 ====================
    textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })
    textLogger := slog.New(textHandler)
    textLogger.Info("请求处理", "method", "GET", "path", "/api/users", "latency", "23ms")

    // ==================== 设置为默认 Logger ====================
    slog.SetDefault(jsonLogger)
    slog.Info("这条用 JSON 格式") // 全局生效

    // ==================== 结构化字段(类型安全)====================
    slog.Info("订单创建",
        slog.String("order_id", "ORD-001"),
        slog.Int("amount", 9900),
        slog.Bool("paid", false),
        slog.Group("customer",
            slog.String("name", "Alice"),
            slog.String("email", "alice@test.com"),
        ),
    )

    // ==================== 带固定字段的子 Logger ====================
    // 适用于请求级别的日志:每条日志自动带上 request_id
    reqLogger := jsonLogger.With(
        "request_id", "req-abc-123",
        "user_id", 42,
    )
    reqLogger.Info("处理开始")
    reqLogger.Info("查询数据库")
    reqLogger.Error("处理失败", "err", "not found")
    // 每条日志都自动带上 request_id 和 user_id

    // ==================== 带 Context ====================
    ctx := context.Background()
    slog.InfoContext(ctx, "带上下文的日志", "key", "value")

    // ==================== 输出到文件 ====================
    file, _ := os.OpenFile("structured.log",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    defer file.Close()

    fileLogger := slog.New(slog.NewJSONHandler(file, nil))
    fileLogger.Info("写入文件", "action", "save")
}

11. 标准输入和标准输出

go
package main

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

func main() {
    // ==================== fmt.Scan 系列(简单输入)====================
    fmt.Print("请输入姓名: ")
    var name string
    fmt.Scan(&name) // 读到空格或换行停止
    fmt.Println("你好,", name)

    fmt.Print("请输入年龄和城市(空格分隔): ")
    var age int
    var city string
    fmt.Scan(&age, &city) // 空格分隔的多个值
    fmt.Printf("年龄: %d, 城市: %s\n", age, city)

    // Scanf:格式化读取
    fmt.Print("请输入日期 (yyyy-mm-dd): ")
    var year, month, day int
    fmt.Scanf("%d-%d-%d", &year, &month, &day)
    fmt.Printf("日期: %d%d%d\n", year, month, day)

    // ==================== bufio.Scanner(读取整行,推荐)====================
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Print("请输入一整行内容: ")
    if scanner.Scan() {
        line := scanner.Text()
        fmt.Println("你输入了:", line)
        fmt.Println("长度:", len(line))
    }

    // ==================== 持续读取(交互式程序)====================
    fmt.Println("\n交互模式(输入 quit 退出):")
    scanner2 := bufio.NewScanner(os.Stdin)
    for {
        fmt.Print("> ")
        if !scanner2.Scan() {
            break
        }
        input := strings.TrimSpace(scanner2.Text())

        if input == "quit" || input == "exit" {
            fmt.Println("再见!")
            break
        }

        if input == "" {
            continue
        }

        // 处理命令
        parts := strings.Fields(input)
        switch parts[0] {
        case "hello":
            fmt.Println("Hello, World!")
        case "echo":
            fmt.Println(strings.Join(parts[1:], " "))
        case "help":
            fmt.Println("命令: hello, echo <text>, quit")
        default:
            fmt.Printf("未知命令: %s(输入 help 查看帮助)\n", parts[0])
        }
    }

    // ==================== 标准输出的几种方式 ====================
    fmt.Println("Println:自动换行")
    fmt.Printf("Printf:格式化 %s %d\n", "hello", 42)
    fmt.Print("Print:不换行")
    fmt.Fprintln(os.Stdout, "\nFprintln:写入指定 Writer")
    fmt.Fprintln(os.Stderr, "这条写到标准错误")

    // 高效输出:bufio.Writer
    writer := bufio.NewWriter(os.Stdout)
    for i := 0; i < 100; i++ {
        fmt.Fprintf(writer, "行 %d\n", i)
    }
    writer.Flush() // 别忘了
}

12. 用 Go 执行系统命令

go
package main

import (
    "bytes"
    "context"
    "fmt"
    "os"
    "os/exec"
    "strings"
    "time"
)

func main() {
    // ==================== 最简单:exec.Command + Output ====================
    out, err := exec.Command("echo", "Hello from Go").Output()
    if err != nil {
        fmt.Println("执行失败:", err)
        return
    }
    fmt.Println("输出:", string(out)) // Hello from Go

    // ==================== 分别获取 stdout 和 stderr ====================
    cmd := exec.Command("ls", "-la", "/tmp")
    var stdout, stderr bytes.Buffer
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err = cmd.Run() // 阻塞直到命令执行完
    if err != nil {
        fmt.Println("错误:", stderr.String())
        return
    }
    fmt.Println(stdout.String())

    // ==================== CombinedOutput:stdout + stderr 混合 ====================
    combined, _ := exec.Command("go", "version").CombinedOutput()
    fmt.Println(string(combined))

    // ==================== 执行 Shell 命令(含管道、重定向等)====================
    // exec.Command 不经过 shell,不支持管道
    // 需要通过 sh -c 来执行
    shellCmd := exec.Command("sh", "-c", "ps aux | grep go | head -5")
    shellOut, _ := shellCmd.Output()
    fmt.Println(string(shellOut))

    // ==================== 管道传递数据给 stdin ====================
    cmd2 := exec.Command("grep", "hello")
    cmd2.Stdin = strings.NewReader("hello world\nfoo bar\nhello go\n")
    var result bytes.Buffer
    cmd2.Stdout = &result
    cmd2.Run()
    fmt.Println("grep 结果:", result.String())
    // hello world
    // hello go

    // ==================== 设置环境变量和工作目录 ====================
    cmd3 := exec.Command("sh", "-c", "echo $MY_VAR && pwd")
    cmd3.Env = append(os.Environ(), "MY_VAR=custom_value")
    cmd3.Dir = "/tmp" // 设置工作目录
    out3, _ := cmd3.Output()
    fmt.Println(string(out3))

    // ==================== 超时控制 ====================
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    cmd4 := exec.CommandContext(ctx, "sleep", "10") // 10 秒的命令
    err = cmd4.Run()
    if ctx.Err() == context.DeadlineExceeded {
        fmt.Println("命令超时被终止")
    }

    // ==================== 实时流式输出 ====================
    cmd5 := exec.Command("sh", "-c", "for i in 1 2 3 4 5; do echo $i; sleep 0.5; done")
    cmd5.Stdout = os.Stdout // 直接输出到终端(实时)
    cmd5.Stderr = os.Stderr
    cmd5.Run()

    // ==================== Start + Wait(非阻塞启动)====================
    cmd6 := exec.Command("sleep", "2")
    cmd6.Start() // 启动命令但不等待
    fmt.Println("命令已启动,PID:", cmd6.Process.Pid)
    fmt.Println("做其他事情...")
    cmd6.Wait() // 等待命令完成
    fmt.Println("命令执行完毕")

    // ==================== 检查命令是否存在 ====================
    path, err := exec.LookPath("git")
    if err != nil {
        fmt.Println("git 未安装")
    } else {
        fmt.Println("git 路径:", path)
    }
}

⚠️ 安全注意事项永远不要将用户输入直接拼接到 sh -c 的命令中,这会导致命令注入漏洞。应该使用 exec.Command("程序", "参数1", "参数2") 的方式,每个参数单独传递。


13. 正则表达式

go
package main

import (
    "fmt"
    "regexp"
    "strings"
)

func main() {
    // ==================== 基本匹配 ====================
    // 推荐用 MustCompile(编译失败直接 panic,适用于写死的正则)
    re := regexp.MustCompile(`\d+`) // 匹配数字

    fmt.Println(re.MatchString("abc123"))    // true
    fmt.Println(re.MatchString("no digits")) // false

    // Compile 返回 error(适用于动态正则)
    re2, err := regexp.Compile(`[invalid`)
    fmt.Println(re2, err) // nil, error parsing regexp: missing closing ]: ...

    // ==================== 查找匹配 ====================
    text := "价格:$29.99 和 $45.50 以及 $100"

    priceRe := regexp.MustCompile(`\$\d+(\.\d{2})?`)

    // 查找第一个匹配
    first := priceRe.FindString(text)
    fmt.Println("第一个:", first) // $29.99

    // 查找所有匹配
    all := priceRe.FindAllString(text, -1) // -1 表示全部
    fmt.Println("所有:", all) // [$29.99 $45.50 $100]

    // 限制数量
    two := priceRe.FindAllString(text, 2)
    fmt.Println("前两个:", two) // [$29.99 $45.50]

    // ==================== 捕获组 ====================
    emailRe := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
    email := "alice@example.com"

    // FindStringSubmatch 返回完整匹配 + 各捕获组
    matches := emailRe.FindStringSubmatch(email)
    fmt.Println("完整匹配:", matches[0]) // alice@example.com
    fmt.Println("用户名:", matches[1])   // alice
    fmt.Println("域名:", matches[2])     // example
    fmt.Println("后缀:", matches[3])     // com

    // 所有匹配的捕获组
    text2 := "联系 alice@foo.com 或 bob@bar.org"
    allMatches := emailRe.FindAllStringSubmatch(text2, -1)
    for _, m := range allMatches {
        fmt.Printf("邮箱: %s, 用户: %s\n", m[0], m[1])
    }

    // ==================== 命名捕获组 ====================
    namedRe := regexp.MustCompile(
        `(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
    date := "今天是 2024-01-15 星期一"

    match := namedRe.FindStringSubmatch(date)
    names := namedRe.SubexpNames()

    for i, name := range names {
        if i == 0 || name == "" {
            continue
        }
        fmt.Printf("%s = %s\n", name, match[i])
    }
    // year = 2024
    // month = 01
    // day = 15

    // ==================== 替换 ====================
    re3 := regexp.MustCompile(`\b\w`)
    // 每个单词首字母大写
    result := re3.ReplaceAllStringFunc("hello world go", strings.ToUpper)
    fmt.Println(result) // Hello World Go

    // 带引用的替换
    re4 := regexp.MustCompile(`(\w+)@(\w+)`)
    result2 := re4.ReplaceAllString("alice@example", "$1 at $2")
    fmt.Println(result2) // alice at example

    // ==================== 分割 ====================
    re5 := regexp.MustCompile(`[,;\s]+`) // 按逗号、分号、空白分割
    parts := re5.Split("a, b; c  d,,e", -1)
    fmt.Println(parts) // [a b c d e]

    // ==================== 实用示例 ====================

    // 验证手机号
    phoneRe := regexp.MustCompile(`^1[3-9]\d{9}$`)
    fmt.Println("手机号:", phoneRe.MatchString("13812345678")) // true
    fmt.Println("手机号:", phoneRe.MatchString("12345678"))     // false

    // 验证邮箱(简化版)
    emailValidRe := regexp.MustCompile(
        `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    fmt.Println("邮箱:", emailValidRe.MatchString("test@example.com"))  // true
    fmt.Println("邮箱:", emailValidRe.MatchString("invalid@"))           // false

    // 提取 URL
    urlRe := regexp.MustCompile(`https?://[^\s<>"]+`)
    html := `访问 https://go.dev 或 http://example.com/path?q=1 了解详情`
    urls := urlRe.FindAllString(html, -1)
    fmt.Println("URLs:", urls)

    // 提取 HTML 标签内容
    tagRe := regexp.MustCompile(`<h1>(.*?)</h1>`)
    htmlStr := "<h1>Go 教程</h1><p>内容</p><h1>第二章</h1>"
    titles := tagRe.FindAllStringSubmatch(htmlStr, -1)
    for _, t := range titles {
        fmt.Println("标题:", t[1])
    }

    // 清理多余空白
    spaceRe := regexp.MustCompile(`\s+`)
    cleaned := strings.TrimSpace(spaceRe.ReplaceAllString("  hello   world  go  ", " "))
    fmt.Println("清理后:", cleaned) // hello world go
}

💡 Go 正则的特殊之处 Go 的 regexp 使用 RE2 语法,保证线性时间复杂度,不支持回溯、前瞻/后顾断言((?=...) (?<=...))和反向引用。这是为了安全和性能。如果需要这些功能,可以使用第三方库 github.com/dlclark/regexp2


附录:常用 IO 模式速查

text
场景                          推荐方式
──────────────────────────────────────────────────
小文件一次性读取               os.ReadFile
小文件一次性写入               os.WriteFile
大文件逐行读取                 bufio.Scanner
大文件流式写入                 bufio.Writer
文件拷贝                      io.Copy
多 Reader 串联                io.MultiReader
同时写多个目标                 io.MultiWriter
压缩/解压                     compress/gzip + io.Copy
JSON 序列化                   json.Marshal / json.NewEncoder
结构化日志                     slog(Go 1.21+)
执行系统命令                   exec.Command
正则匹配                      regexp.MustCompile
路径拼接                      filepath.Join
目录遍历                      filepath.WalkDir