Go 文件与 I/O 操作完全指南

引言

文件操作是任何编程语言都必须掌握的基础技能,Go 语言在这方面的设计简洁而强大。Go 的 I/O 操作主要围绕 ioosioutilbufiofmt 这几个核心包展开。标准库的设计遵循 Unix 哲学:一个工具做好一件事,通过组合实现复杂功能。

本文将系统性地介绍 Go 中的各类文件与 I/O 操作,从基础的读写文件,到高级的缓冲 I/O 和目录操作,再到实际生产环境中的日志分析工具实现。

一、os 包基础文件操作

1.1 文件创建与打开

Go 的 os 包提供了最底层的文件操作函数:

复制代码
package main
​
import (
    "os"
    "fmt"
)
​
func main() {
    // Create 创建文件(如果存在则截断)
    file, err := os.Create("test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    fmt.Println("文件创建成功")
​
    // Open 以只读方式打开文件
    file, err = os.Open("test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
​
    // OpenFile 更通用的打开方式
    // 第二个参数是模式:O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, O_CREATE, O_EXCL, O_TRUNC
    file, err = os.OpenFile("test.txt", os.O_RDWR|os.O_APPEND, 0644)
    if err != nil {
        panic(err)
    }
    defer file.Close()
}

1.2 文件读写

使用 os.File 的 Read 和 Write 方法:

复制代码
func fileReadWrite() error {
    // 写入数据
    file, err := os.Create("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()
​
    data := []byte("Hello, Go文件操作!\n第二行数据")
    n, err := file.Write(data)
    if err != nil {
        return err
    }
    fmt.Printf("成功写入 %d 字节\n", n)
​
    // 使用 WriteString 写入字符串
    n, err = file.WriteString("第三行数据\n")
    if err != nil {
        return err
    }
    fmt.Printf("成功写入字符串 %d 字节\n", n)
​
    // 读取数据
    file.Seek(0, 0) // 将指针移到文件开头
    content := make([]byte, 1024)
    n, err = file.Read(content)
    if err != nil && err != io.EOF {
        return err
    }
    fmt.Printf("读取到: %s\n", content[:n])
​
    return nil
}

1.3 文件指针操作

os.File 内部维护一个文件指针,表示当前读写位置:

复制代码
func fileSeeking() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()
​
    // 获取当前指针位置
    offset, err := file.Seek(0, io.SeekCurrent)
    if err != nil {
        return err
    }
    fmt.Printf("当前偏移: %d\n", offset)
​
    // Seek 设置指针位置
    // 第二个参数:0=起始位置, 1=当前位置, 2=结束位置
    _, err = file.Seek(10, io.SeekStart)
    if err != nil {
        return err
    }
​
    // 获取新的偏移量
    offset, err = file.Seek(0, io.SeekCurrent)
    if err != nil {
        return err
    }
    fmt.Printf("移动后偏移: %d\n", offset)
​
    return nil
}

二、ioutil 与便捷读写函数

2.1 完整读取文件

ioutil 包提供了更便捷的高级函数:

复制代码
import (
    "io/ioutil"
    "fmt"
)
​
func readAllFile() error {
    // ReadFile 读取整个文件(适合小文件)
    content, err := ioutil.ReadFile("data.txt")
    if err != nil {
        return err
    }
    fmt.Printf("文件内容:\n%s\n", string(content))
​
    return nil
}

ReadFile 底层原理

复制代码
// ioutil.ReadFile 的简化实现
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
​
    // 尝试获取文件大小
    stat, err := f.Stat()
    if err != nil {
        return nil, err
    }
​
    // 如果文件大小已知,预分配内存
    size := stat.Size()
    if size == 0 {
        return []byte{}, nil
    }
​
    data := make([]byte, size)
    for {
        n, err := f.Read(data)
        if n > 0 {
            data = append(data, data[:n]...)
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
    }
​
    return data, nil
}

2.2 完整写入文件

复制代码
func writeFile() error {
    data := []byte("这是要写入的内容\n第二行")
​
    // WriteFile 写入整个文件(如果文件存在则覆盖)
    err := ioutil.WriteFile("output.txt", data, 0644)
    if err != nil {
        return err
    }
​
    fmt.Println("文件写入成功")
    return nil
}

2.3 临时文件与目录

复制代码
func tempFileDemo() error {
    // TempDir 创建临时目录
    dir, err := ioutil.TempDir("", "myapp-*")
    if err != nil {
        return err
    }
    defer os.RemoveAll(dir) // 使用完清理
    fmt.Printf("创建临时目录: %s\n", dir)
​
    // TempFile 创建临时文件
    file, err := ioutil.TempFile(dir, "data-*.txt")
    if err != nil {
        return err
    }
    defer os.Remove(file.Name()) // 清理文件
​
    // 写入临时文件
    file.WriteString("临时数据")
    file.Close()
​
    fmt.Printf("创建临时文件: %s\n", file.Name())
    return nil
}

三、缓冲 I/O(bufio)

3.1 为什么需要缓冲 I/O

直接的文件读写每次都会触发系统调用,对于大量小数据量的操作性能很差。bufio 通过在内存中维护缓冲区来减少系统调用次数:

复制代码
无缓冲 I/O:
应用 -> [read() syscall] -> 内核 -> 磁盘 (每次读取一个字节)
应用 -> [read() syscall] -> 内核 -> 磁盘 (每次读取一个字节)
...
​
有缓冲 I/O:
应用 -> 读取缓冲区 -> [read() syscall] -> 内核 -> 磁盘 (一次性读取大量数据)
应用 -> 读取缓冲区 (内存操作,极快)
应用 -> 读取缓冲区 (内存操作,极快)
...

3.2 bufio.Reader

复制代码
func bufferedRead() error {
    file, err := os.Open("largefile.txt")
    if err != nil {
        return err
    }
    defer file.Close()
​
    // 创建缓冲读取器,8KB 缓冲区
    reader := bufio.NewReaderSize(file, 8*1024)
​
    // 按行读取
    for {
        line, err := reader.ReadString('\n')
        if err != nil && err != io.EOF {
            return err
        }
        fmt.Print(line)
        if err == io.EOF {
            break
        }
    }
​
    return nil
}

bufio.Reader 的主要方法:

复制代码
// Read 从缓冲区读取数据
func (b *Reader) Read(p []byte) (n int, err error)
​
// ReadByte 读取单个字节
func (b *Reader) ReadByte() (byte, error)
​
// ReadBytes 读取直到指定分隔符
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
​
// ReadString 读取直到指定分隔符,返回字符串
func (b *Reader) ReadString(delim byte) (string, error)
​
// ReadLine 读取一行(不建议使用,更推荐 ReadBytes)
func (b *Reader) ReadLine() ([]byte, bool, error)
​
// ReadSlice 读取直到分隔符
func (b *Reader) ReadSlice(delim byte) ([]byte, error)
​
// Peek 返回缓冲区中的前 n 个字节,不移动指针
func (b *Reader) Peek(n int) ([]byte, error)
​
// Discard 跳过前 n 个字节
func (b *Reader) Discard(n int) (discarded int, err error)

3.3 bufio.Writer

复制代码
func bufferedWrite() error {
    file, err := os.Create("output.txt")
    if err != nil {
        return err
    }
    defer file.Close()
​
    // 创建缓冲写入器
    writer := bufio.NewWriterSize(file, 8*1024)
​
    // 写入数据
    for i := 0; i < 1000; i++ {
        _, err := writer.WriteString(fmt.Sprintf("第 %d 行数据\n", i))
        if err != nil {
            return err
        }
    }
​
    // 重要:刷新缓冲区,确保所有数据写入文件
    err = writer.Flush()
    if err != nil {
        return err
    }
​
    return nil
}

3.4 Scanner 逐行处理

对于按行分割的数据,bufio.Scanner 是最简洁的 API:

复制代码
func lineCounter(filename string) (int, error) {
    file, err := os.Open(filename)
    if err != nil {
        return 0, err
    }
    defer file.Close()
​
    scanner := bufio.NewScanner(file)
​
    // 可选:设置缓冲区大小(默认 64KB)
    const maxCapacity = 1024 * 1024 // 1MB
    buf := make([]byte, maxCapacity)
    scanner.Buffer(buf, maxCapacity)
​
    // 可选:设置分割函数(默认按行分割)
    // scanner.Split(bufio.ScanLines)
​
    count := 0
    for scanner.Scan() {
        count++
    }
​
    if err := scanner.Err(); err != nil {
        return 0, err
    }
​
    return count, nil
}

Scanner 工作原理

复制代码
数据流向:
磁盘 -> 内核缓冲区 -> 用户缓冲区(64KB) -> Scanner缓冲区(<=64KB) -> 应用程序
​
Scanner 内部维护两个缓冲区:
1. 用户缓冲区(Read Bytes):从内核读取的大块数据
2. Token 缓冲区:当前正在处理的行
​
当 token 超过 64KB 时,需要使用 Buffer() 扩展

四、格式化 I/O

4.1 fmt 包的格式化输出

复制代码
func fmtDemo() {
    // 基础格式化
    fmt.Printf("字符串: %s, 整数: %d, 浮点: %.2f\n", "hello", 42, 3.14159)
​
    // 常用动词
    // %v  默认格式
    // %+v 结构体时显示字段名
    // %#v Go 语法表示
    // %T  类型
    // %%  转义百分号
​
    type User struct {
        Name string
        Age  int
    }
    user := User{"Alice", 30}
​
    fmt.Printf("%v\n", user)        // {Alice 30}
    fmt.Printf("%+v\n", user)      // {Name:Alice Age:30}
    fmt.Printf("%#v\n", user)      // main.User{Name:"Alice", Age:30}
    fmt.Printf("%T\n", user)       // main.User
​
    // 宽度和对齐
    fmt.Printf("|%6s|%6d|%6.2f|\n", "Hello", 42, 3.14)  // | Hello|    42|  3.14|
    fmt.Printf("|%-6s|%-6d|%-6.2f|\n", "Hello", 42, 3.14) // |Hello |42    |3.14  |
​
    // 进制转换
    fmt.Printf("十进制: %d\n", 255)
    fmt.Printf("二进制: %b\n", 255)
    fmt.Printf("十六进制: %x\n", 255)
    fmt.Printf("八进制: %o\n", 255)
}

4.2 fmt 包的格式化输入

复制代码
func fmtScanDemo() {
    // 从标准输入扫描
    var name string
    var age int
    var salary float64
​
    fmt.Print("请输入姓名、年龄、薪资: ")
​
    // Scan 从空白分隔的输入中读取
    fmt.Scan(&name, &age, &salary)
    fmt.Printf("姓名: %s, 年龄: %d, 薪资: %.2f\n", name, age, salary)
​
    // Scanf 按格式解析
    fmt.Print("请按格式输入(姓名,年龄,薪资): ")
    fmt.Scanf("%s,%d,%f", &name, &age, &salary)
    fmt.Printf("姓名: %s, 年龄: %d, 薪资: %.2f\n", name, age, salary)
​
    // Scanln 读取一行(知道行尾)
    fmt.Println("请输入姓名和年龄:")
    fmt.Scanln(&name, &age)
    fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
}

4.3 bufio + fmt 扫描文件

复制代码
func scanFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
​
    scanner := bufio.NewScanner(file)
​
    var name string
    var age int
    var score float64
    var totalScore float64
    var count int
​
    for scanner.Scan() {
        line := scanner.Text()
        // 跳过空行和注释
        if len(line) == 0 || line[0] == '#' {
            continue
        }
​
        // 解析数据
        _, err := fmt.Sscanf(line, "%s %d %f", &name, &age, &score)
        if err != nil {
            continue
        }
​
        totalScore += score
        count++
        fmt.Printf("学生: %s, 年龄: %d, 成绩: %.2f\n", name, age, score)
    }
​
    if count > 0 {
        fmt.Printf("平均成绩: %.2f\n", totalScore/float64(count))
    }
​
    return scanner.Err()
}

五、目录操作与文件遍历

5.1 目录基本操作

复制代码
import (
    "os"
    "path/filepath"
)
​
func dirOperations() error {
    // 创建目录
    err := os.Mkdir("testdir", 0755)
    if err != nil && !os.IsExist(err) {
        return err
    }
​
    // 创建多层目录
    err = os.MkdirAll("a/b/c/d", 0755)
    if err != nil {
        return err
    }
​
    // 读取目录内容
    entries, err := os.ReadDir(".")
    if err != nil {
        return err
    }
​
    for _, entry := range entries {
        fmt.Printf("%s\t", entry.Name())
        if entry.IsDir() {
            fmt.Printf("[DIR]")
        } else {
            info, _ := entry.Info()
            fmt.Printf("[FILE] %d bytes", info.Size())
        }
        fmt.Println()
    }
​
    // 删除目录
    err = os.Remove("testdir")
    if err != nil && !os.IsNotExist(err) {
        return err
    }
​
    // 删除目录树
    err = os.RemoveAll("a")
    if err != nil && !os.IsNotExist(err) {
        return err
    }
​
    return nil
}

5.2 递归遍历目录

使用 filepath.Walkfilepath.WalkDir

复制代码
// Walk 遍历目录树
func walkDemo() error {
    return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
​
        // 获取相对路径
        relPath, _ := filepath.Rel(".", path)
​
        // 跳过隐藏文件和目录
        if strings.HasPrefix(filepath.Base(path), ".") {
            if info.IsDir() {
                return filepath.SkipDir
            }
            return nil
        }
​
        // 打印结构
        indent := strings.Count(relPath, string(filepath.Separator))
        for i := 0; i < indent; i++ {
            fmt.Print("  ")
        }
​
        if info.IsDir() {
            fmt.Printf("📁 %s/\n", info.Name())
        } else {
            fmt.Printf("📄 %s (%d bytes)\n", info.Name(), info.Size())
        }
​
        return nil
    })
}
​
// WalkDir 更高效(不调用 Stat 对于目录)
func walkDirDemo() error {
    return filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
​
        info, err := d.Info()
        if err != nil {
            return err
        }
​
        fmt.Printf("%s: %d bytes\n", path, info.Size())
        return nil
    })
}

5.3 查找特定文件

复制代码
func findFiles(root, pattern string) ([]string, error) {
    var matches []string
​
    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
​
        // 匹配文件名模式
        matched, err := filepath.Match(pattern, info.Name())
        if err != nil {
            return err
        }
​
        if matched {
            absPath, _ := filepath.Abs(path)
            matches = append(matches, absPath)
        }
​
        return nil
    })
​
    return matches, err
}

六、文件权限与属性

6.1 文件权限

Go 使用 Unix 风格的权限模型:

复制代码
func permissionDemo() error {
    // 创建文件并设置权限
    file, err := os.OpenFile("secure.txt", os.O_CREATE|os.O_WRONLY, 0600)
    if err != nil {
        return err
    }
    file.Close()
​
    // 修改文件权限
    err = os.Chmod("secure.txt", 0644)
    if err != nil {
        return err
    }
​
    // 修改目录权限
    err = os.Mkdir("rwx", 0755)
    if err != nil {
        return err
    }
    err = os.Chmod("rwx", 0700)
​
    return nil
}

6.2 文件属性获取

复制代码
func fileInfoDemo(filename string) error {
    info, err := os.Stat(filename)
    if err != nil {
        return err
    }
​
    fmt.Printf("文件名: %s\n", info.Name())
    fmt.Printf("大小: %d bytes\n", info.Size())
    fmt.Printf("权限: %o\n", info.Mode().Perm())
    fmt.Printf("是否目录: %t\n", info.IsDir())
    fmt.Printf("修改时间: %s\n", info.ModTime().Format("2006-01-02 15:04:05"))
​
    // 获取更详细的权限信息
    mode := info.Mode()
    fmt.Printf("是常规文件: %t\n", mode.IsRegular())
    fmt.Printf("是目录: %t\n", mode.IsDir())
    fmt.Printf("是符号链接: %t\n", mode&os.ModeSymlink != 0)
​
    return nil
}

6.3 文件时间戳

复制代码
func fileTimeDemo() error {
    // 获取文件访问和修改时间
    info, err := os.Stat("data.txt")
    if err != nil {
        return err
    }
​
    modTime := info.ModTime()
    accessTime := statAtime(info.Sys().(*syscall.Stat_t))
​
    fmt.Printf("修改时间: %s\n", modTime)
    fmt.Printf("访问时间: %s\n", accessTime)
​
    // 设置文件时间戳(仅 Unix)
    // os.Chtimes("data.txt", time.Now(), time.Now())
​
    return nil
}

七、实战案例:日志分析工具

7.1 需求分析

我们需要实现一个日志分析工具,具备以下功能:

  1. 多格式支持:支持 Nginx、Apache JSON 格式日志

  2. 实时统计:访问量、状态码分布、Top N IP

  3. 模式匹配:支持正则表达式过滤

  4. 性能优化:使用 goroutine 并行处理

7.2 完整实现

复制代码
package main
​
import (
    "bufio"
    "bytes"
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "log"
    "mime"
    "mime/quotedprintable"
    "net"
    "os"
    "path/filepath"
    "regexp"
    "runtime"
    "sort"
    "strings"
    "sync"
    "sync/atomic"
    "time"
)
​
// 日志条目
type LogEntry struct {
    Timestamp  time.Time
    Method     string
    Path       string
    Status     int
    Size       int64
    ClientIP   string
    UserAgent  string
    Referer    string
    Protocol   string
    ResponseTime float64
}
​
// 统计信息
type Stats struct {
    TotalRequests    int64
    TotalBytes       int64
    StatusCounts     map[int]int64
    MethodCounts     map[string]int64
    TopIPs           map[string]int64
    TopPaths         map[string]int64
   mu sync.RWMutex
}
​
func NewStats() *Stats {
    return &Stats{
        StatusCounts: make(map[int]int64),
        MethodCounts: make(map[string]int64),
        TopIPs:       make(map[string]int64),
        TopPaths:     make(map[string]int64),
    }
}
​
func (s *Stats) Increment(status int, method, ip, path string, size int64) {
    atomic.AddInt64(&s.TotalRequests, 1)
    atomic.AddInt64(&s.TotalBytes, size)
​
    s.mu.Lock()
    s.StatusCounts[status]++
    s.MethodCounts[method]++
    s.TopIPs[ip]++
    s.TopPaths[path]++
    s.mu.Unlock()
}
​
type LogParser struct {
    stats       *Stats
    pattern     *regexp.Regexp
    workers     int
    entryChan   chan *LogEntry
    resultChan  chan *Stats
    ctx         context.Context
    cancel      context.CancelFunc
}
​
func NewLogParser(workers int) *LogParser {
    ctx, cancel := context.WithCancel(context.Background())
    return &LogParser{
        stats:      NewStats(),
        workers:    workers,
        entryChan:  make(chan *LogEntry, 10000),
        resultChan: make(chan *Stats, workers),
        ctx:        ctx,
        cancel:     cancel,
    }
}
​
// Nginx 日志格式解析
// 192.168.1.1 - - [10/Oct/2026:13:55:36 +0000] "GET /api/users HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0"
​
var nginxPattern = regexp.MustCompile(`(?P<ip>[\d\.]+) - \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"`)
​
// Apache 日志格式解析
// 127.0.0.1 - frank [10/Oct/2026:13:55:36 +0000] "GET /apache_pb.gif HTTP/1.0" 200 2326
​
var apachePattern = regexp.MustCompile(`(?P<ip>[\d\.]+) \S+ \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+)`)
​
// JSON 日志格式解析
func parseJSONLog(line []byte) (*LogEntry, error) {
    var entry LogEntry
    if err := json.Unmarshal(line, &entry); err != nil {
        return nil, err
    }
    return &entry, nil
}
​
func (p *LogParser) parseNginxLog(line string) (*LogEntry, error) {
    matches := nginxPattern.FindStringSubmatch(line)
    if matches == nil {
        return nil, fmt.Errorf("无法解析日志行")
    }
​
    timestamp, err := time.Parse("02/Jan/2006:15:04:05 -0700", nginxPattern.SubexpNames()[1])
    if err != nil {
        // 尝试 RFC3339 格式
        timestamp = time.Now()
    }
​
    entry := &LogEntry{
        Timestamp: timestamp,
        Method:    nginxPattern.SubexpNames()[2],
        Path:      nginxPattern.SubexpNames()[3],
        Protocol:  nginxPattern.SubexpNames()[4],
    }
​
    fmt.Sscanf(nginxPattern.SubexpNames()[5], "%d", &entry.Status)
    fmt.Sscanf(nginxPattern.SubexpNames()[6], "%d", &entry.Size)
    entry.ClientIP = nginxPattern.SubexpNames()[0]
    entry.Referer = nginxPattern.SubexpNames()[7]
    entry.UserAgent = nginxPattern.SubexpNames()[8]
​
    return entry, nil
}
​
func (p *LogParser) parseLine(line string) (*LogEntry, error) {
    // JSON 格式检测
    if len(line) > 0 && line[0] == '{' {
        return parseJSONLog([]byte(line))
    }
​
    // 尝试 Nginx 格式
    if strings.Contains(line, "[") && strings.Contains(line, "\"GET") {
        return p.parseNginxLog(line)
    }
​
    return nil, fmt.Errorf("未知格式")
}
​
func (p *LogParser) processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("打开文件失败 %s: %w", filename, err)
    }
    defer file.Close()
​
    // 使用 Scanner 按行处理
    scanner := bufio.NewScanner(file)
    buf := make([]byte, 0, 64*1024)
    scanner.Buffer(buf, 1024*1024)
​
    lineNum := 0
    for scanner.Scan() {
        lineNum++
        line := scanner.Text()
​
        // 跳过空行
        if len(strings.TrimSpace(line)) == 0 {
            continue
        }
​
        // 正则过滤
        if p.pattern != nil && !p.pattern.MatchString(line) {
            continue
        }
​
        entry, err := p.parseLine(line)
        if err != nil {
            log.Printf("解析错误 [文件: %s, 行: %d]: %v", filename, lineNum, err)
            continue
        }
​
        p.stats.Increment(entry.Status, entry.Method, entry.ClientIP, entry.Path, entry.Size)
    }
​
    return scanner.Err()
}
​
func (p *LogParser) processFileWorker(files <-chan string) {
    localStats := NewStats()
​
    for file := range files {
        err := p.parseFileStats(file, localStats)
        if err != nil {
            log.Printf("处理文件失败: %s, 错误: %v", file, err)
        }
    }
​
    p.resultChan <- localStats
}
​
func (p *LogParser) parseFileStats(filename string, stats *Stats) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
​
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if len(strings.TrimSpace(line)) == 0 {
            continue
        }
​
        if p.pattern != nil && !p.pattern.MatchString(line) {
            continue
        }
​
        entry, err := p.parseLine(line)
        if err != nil {
            continue
        }
​
        stats.Increment(entry.Status, entry.Method, entry.ClientIP, entry.Path, entry.Size)
    }
​
    return scanner.Err()
}
​
func (p *LogParser) mergeStats(workerStats []*Stats) {
    for _, ws := range workerStats {
        p.stats.mu.Lock()
        for k, v := range ws.StatusCounts {
            p.stats.StatusCounts[k] += v
        }
        for k, v := range ws.MethodCounts {
            p.stats.MethodCounts[k] += v
        }
        for k, v := range ws.TopIPs {
            p.stats.TopIPs[k] += v
        }
        for k, v := range ws.TopPaths {
            p.stats.TopPaths[k] += v
        }
        p.stats.mu.Unlock()
    }
}
​
func (p *LogParser) Parse(pattern string, paths []string) error {
    // 编译过滤模式
    if pattern != "" {
        p.pattern = regexp.MustCompile(pattern)
    }
​
    // 收集所有日志文件
    var files []string
    for _, path := range paths {
        info, err := os.Stat(path)
        if err != nil {
            return err
        }
​
        if info.IsDir() {
            dirFiles, err := collectLogFiles(path)
            if err != nil {
                return err
            }
            files = append(files, dirFiles...)
        } else {
            files = append(files, path)
        }
    }
​
    if len(files) == 0 {
        return fmt.Errorf("没有找到日志文件")
    }
​
    log.Printf("找到 %d 个日志文件,使用 %d 个工作协程", len(files), p.workers)
​
    // 创建文件通道
    fileChan := make(chan string, len(files))
    for _, f := range files {
        fileChan <- f
    }
    close(fileChan)
​
    // 启动工作协程
    var wg sync.WaitGroup
    for i := 0; i < p.workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            p.processFileWorker(fileChan)
        }()
    }
​
    // 等待所有工作完成并收集结果
    go func() {
        wg.Wait()
        close(p.resultChan)
    }()
​
    var workerStats []*Stats
    for stats := range p.resultChan {
        workerStats = append(workerStats, stats)
    }
​
    p.mergeStats(workerStats)
    return nil
}
​
func collectLogFiles(dir string) ([]string, error) {
    var files []string
    pattern := filepath.Join(dir, "*.log")
​
    matches, err := filepath.Glob(pattern)
    if err != nil {
        return nil, err
    }
    files = append(files, matches...)
​
    // 递归处理子目录
    entries, err := os.ReadDir(dir)
    if err != nil {
        return nil, err
    }
​
    for _, entry := range entries {
        if entry.IsDir() {
            subFiles, err := collectLogFiles(filepath.Join(dir, entry.Name()))
            if err != nil {
                continue
            }
            files = append(files, subFiles...)
        }
    }
​
    return files, nil
}
​
func (s *Stats) Print() {
    s.mu.Lock()
    defer s.mu.Unlock()
​
    fmt.Println("\n" + strings.Repeat("=", 60))
    fmt.Println("                    日志分析报告")
    fmt.Println(strings.Repeat("=", 60))
​
    fmt.Printf("\n总请求数: %d\n", atomic.LoadInt64(&s.TotalRequests))
    fmt.Printf("总流量: %s\n", formatBytes(atomic.LoadInt64(&s.TotalBytes)))
​
    fmt.Println("\n--- 状态码分布 ---")
    var statusCodes []int
    for code := range s.StatusCounts {
        statusCodes = append(statusCodes, code)
    }
    sort.Ints(statusCodes)
    for _, code := range statusCodes {
        count := s.StatusCounts[code]
        pct := float64(count) / float64(atomic.LoadInt64(&s.TotalRequests)) * 100
        fmt.Printf("  %d: %d (%.1f%%) %s\n", code, count, pct, statusCodeDesc(code))
    }
​
    fmt.Println("\n--- 请求方法分布 ---")
    for method, count := range s.MethodCounts {
        pct := float64(count) / float64(atomic.LoadInt64(&s.TotalRequests)) * 100
        fmt.Printf("  %s: %d (%.1f%%)\n", method, count, pct)
    }
​
    fmt.Println("\n--- Top 10 IP 地址 ---")
    printTopN(s.TopIPs, 10)
​
    fmt.Println("\n--- Top 10 请求路径 ---")
    printTopN(s.TopPaths, 10)
​
    fmt.Println(strings.Repeat("=", 60))
}
​
func printTopN(m map[string]int64, n int) {
    type kv struct {
        Key   string
        Value int64
    }
​
    var ss []kv
    for k, v := range m {
        ss = append(ss, kv{k, v})
    }
​
    sort.Slice(ss, func(i, j int) bool {
        return ss[i].Value > ss[j].Value
    })
​
    for i := 0; i < n && i < len(ss); i++ {
        fmt.Printf("  %s: %d\n", ss[i].Key, ss[i].Value)
    }
}
​
func statusCodeDesc(code int) string {
    switch {
    case code >= 200 && code < 300:
        return "成功"
    case code >= 300 && code < 400:
        return "重定向"
    case code >= 400 && code < 500:
        return "客户端错误"
    case code >= 500:
        return "服务端错误"
    default:
        return "未知"
    }
}
​
func formatBytes(bytes int64) string {
    const unit = 1024
    if bytes < unit {
        return fmt.Sprintf("%d B", bytes)
    }
    div, exp := int64(unit), 0
    for n := bytes / unit; n >= unit; n /= unit {
        div *= unit
        exp++
    }
    return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
​
func main() {
    workers := flag.Int("w", runtime.NumCPU(), "工作协程数量")
    pattern := flag.String("filter", "", "正则表达式过滤")
    flag.Parse()
​
    paths := flag.Args()
    if len(paths) == 0 {
        fmt.Println("用法: loganalyzer [选项] <日志文件或目录>")
        flag.PrintDefaults()
        os.Exit(1)
    }
​
    parser := NewLogParser(*workers)
​
    start := time.Now()
    if err := parser.Parse(*pattern, paths); err != nil {
        log.Fatalf("分析失败: %v", err)
    }
​
    parser.stats.Print()
    fmt.Printf("\n分析耗时: %v\n", time.Since(start))
}

7.3 使用示例与测试

创建测试日志文件:

复制代码
# 创建测试数据
mkdir -p logs
cat > logs/access.log << 'EOF'
192.168.1.1 - - [10/Oct/2026:13:55:36 +0000] "GET /api/users HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0"
192.168.1.2 - - [10/Oct/2026:13:55:37 +0000] "POST /api/login HTTP/1.1" 200 512 "http://example.com" "Mozilla/5.0"
192.168.1.1 - - [10/Oct/2026:13:55:38 +0000] "GET /api/users/123 HTTP/1.1" 404 256 "http://example.com" "Mozilla/5.0"
192.168.1.3 - - [10/Oct/2026:13:55:39 +0000] "GET /static/app.js HTTP/1.1" 200 10240 "http://example.com" "Mozilla/5.0"
192.168.1.1 - - [10/Oct/2026:13:55:40 +0000] "GET /api/products HTTP/1.1" 500 128 "http://example.com" "Mozilla/5.0"
EOF

运行分析工具:

复制代码
go run main.go logs/
​
# 带过滤条件的分析
go run main.go -filter "/api/" logs/
​
# 指定工作协程数
go run main.go -w 8 logs/

7.4 性能优化要点

1. 缓冲区大小选择

复制代码
// 小文件:默认缓冲区足够
scanner := bufio.NewScanner(file)
​
// 大文件:增加缓冲区避免内存压力
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024) // 64KB
scanner.Buffer(buf, 1024*1024)    // 最大 token 1MB

2. 并行处理策略

复制代码
文件级并行:
  File1 -> Worker1 -> Stats1
  File2 -> Worker2 -> Stats2
  File3 -> Worker3 -> Stats3
            ↓
        Merge Results
​
行级并行(需要更复杂的实现):
  Scanner -> Channel -> Workers -> Reduce

3. 内存优化

复制代码
// 避免在循环中分配大对象
for scanner.Scan() {
    // 复用buffer
    line := scanner.Bytes()
    process(line) // 使用字节切片而非字符串拷贝
}

八、实战案例:配置文件的读写

8.1 INI 配置文件

复制代码
package config
​
import (
    "bufio"
    "fmt"
    "os"
    "regexp"
    "strings"
)
​
type Config struct {
    sections map[string]map[string]string
}
​
func NewConfig() *Config {
    return &Config{
        sections: make(map[string]map[string]string),
    }
}
​
func (c *Config) Load(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
​
    var currentSection string
    sectionRE := regexp.MustCompile(`^\[(.+)\]$`)
    kvRE := regexp.MustCompile(`^([^=]+)=(.*)$`)
​
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
​
        // 跳过空行和注释
        if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
            continue
        }
​
        // 解析节
        if matches := sectionRE.FindStringSubmatch(line); matches != nil {
            currentSection = matches[1]
            if _, ok := c.sections[currentSection]; !ok {
                c.sections[currentSection] = make(map[string]string)
            }
            continue
        }
​
        // 解析键值对
        if matches := kvRE.FindStringSubmatch(line); matches != nil {
            key := strings.TrimSpace(matches[1])
            value := strings.TrimSpace(matches[2])
            value = strings.Trim(value, "\"")
​
            if currentSection == "" {
                currentSection = "default"
                c.sections[currentSection] = make(map[string]string)
            }
​
            c.sections[currentSection][key] = value
        }
    }
​
    return scanner.Err()
}
​
func (c *Config) Save(filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
​
    writer := bufio.NewWriter(file)
    for section, kv := range c.sections {
        fmt.Fprintf(writer, "[%s]\n", section)
        for k, v := range kv {
            fmt.Fprintf(writer, "%s = %q\n", k, v)
        }
        fmt.Fprintln(writer)
    }
​
    return writer.Flush()
}
​
func (c *Config) Get(section, key string) string {
    if s, ok := c.sections[section]; ok {
        if v, ok := s[key]; ok {
            return v
        }
    }
    return ""
}
​
func (c *Config) Set(section, key, value string) {
    if _, ok := c.sections[section]; !ok {
        c.sections[section] = make(map[string]string)
    }
    c.sections[section][key] = value
}

总结

Go 的 I/O 系统设计得非常优雅,通过组合不同的包和接口,我们可以构建出高效、灵活的文件处理方案:

  1. 基础 I/O :使用 os 包进行底层的文件操作

  2. 便捷函数ioutil 包提供常用的高层操作

  3. 缓冲 I/Obufio 包优化大量小读写的性能

  4. 格式化 I/Ofmt 包处理格式化的输入输出

  5. 目录操作ospath/filepath 包处理目录和路径

在实际项目中,选择合适的 I/O 方式需要考虑:

  • 数据量大小:大文件使用流式处理,避免一次性加载

  • 性能要求 :频繁小读写使用 bufio,偶尔一次用 ioutil

  • 并发安全 :文件操作本身非线程安全,需要加锁或使用 sync.Mutex

  • 错误处理:始终检查返回值,特别是 I/O 操作

相关推荐
szial2 小时前
uv 实战指南:用一个工具重塑 Python 开发工作流
开发语言·python·uv
wjs20242 小时前
HTML 段落
开发语言
CSCN新手听安2 小时前
【Qt】Qt窗口(五)QDialog对话框的使用,点击按钮弹出新的对话框,自定义对话框界面,模态对话框model
开发语言·c++·qt
枫叶丹42 小时前
【HarmonyOS 6.0】CANN Kit 新增支持获取 AI 模型 Dump 维测数据功能详解
开发语言·人工智能·华为·信息可视化·harmonyos
沐知全栈开发2 小时前
JavaScript Array(数组)
开发语言
geovindu7 小时前
go: Mediator Pattern
设计模式·golang·中介者模式
MATLAB代码顾问10 小时前
5大智能算法优化标准测试函数对比(Python实现)
开发语言·python
万粉变现经纪人11 小时前
如何解决 pip install llama-cpp-python 报错 未安装 CMake/Ninja 或 CPU 不支持 AVX 问题
开发语言·python·开源·aigc·pip·ai写作·llama
清风明月一壶酒12 小时前
OpenClaw自动处理Word文档全流程
开发语言·c#·word