下载工作台
Go 编程实战

文件 IO 与 JSON

试读上半部分 · 解锁后可读全文

第 11 章 · 文件 IO、bufio 与 JSON 日志

本章目标:掌握 os 文件操作、bufio 按行扫描、encoding/json 序列化与反序列化;读写 JSON Lines 格式的 access.log;输出 CSV 运营日报;在 toolkit-go 落地完整 读日志 → 过滤 → 写报表 流水线;对照 java-dev ch07 NIO 与 Jackson。

学时建议:3~4 小时(含 2 小时跟练)

前置:完成 go-dev ch10(context 与 Worker Pool)。


11.1 场景说明:access 日志 → CSV 日报

toolkit-go 是虚构运维团队 toolkit-team 的 CLI 工具。输入来自网关/nginx 的 JSON Lines 日志;输出 慢请求 Top N 的 CSV,供运营导入 Excel。

toolkit-go/
├── data/
│   ├── access.log.jsonl    # 输入:一行一条 JSON
│   └── report-20260819.csv # 输出:慢路径汇总
├── internal/
│   ├── model/
│   ├── logparser/
│   └── report/
└── cmd/toolkit/

单行 access 日志(字段虚构,与 gin-web 中间件对齐):

{"time":"2026-08-19T10:00:01Z","method":"GET","path":"/api/v1/products/go-handbook","status":200,"duration_ms":820,"request_id":"req-demo-101","user_slug":"user-demo"}
{"time":"2026-08-19T10:00:02Z","method":"POST","path":"/api/v1/orders","status":500,"duration_ms":3200,"request_id":"req-demo-102","user_slug":"user-demo"}

CSV 输出列

pathcountavg_msmax_mslast_status
/api/v1/orders132003200500

商品价格在其他模块仍用 int64 分6800 = 68.00 元);本章聚焦 日志 IO


11.2 os 包:打开、创建、目录

import (
    "os"
    "path/filepath"
)

func ensureDir(dir string) error {
    return os.MkdirAll(dir, 0o755)
}

func openLog(path string) (*os.File, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open %s: %w", path, err)
    }
    return f, nil
}

func createReport(path string) (*os.File, error) {
    if err := ensureDir(filepath.Dir(path)); err != nil {
        return nil, err
    }
    return os.Create(path)
}
API用途
os.Open只读打开
os.Create创建或截断写
os.OpenFile自定义 flag(O_APPEND 等)
os.ReadFile小文件一次性读入 []byte
os.WriteFile小文件一次性写入
os.Stat文件信息、是否存在
os.MkdirAll递归建目录

defer Close(ch07 复习):

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

11.3 bufio.Scanner:按行读 JSONL

// internal/logparser/reader.go
package logparser

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

func ReadLines(ctx context.Context, path string) ([]string, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open: %w", err)
    }
    defer f.Close()

    var lines []string
    sc := bufio.NewScanner(f)
    // 默认 max token 64KB;超长 JSON 行需扩大
    buf := make([]byte, 0, 64*1024)
    sc.Buffer(buf, 1024*1024) // max 1MB per line

    for sc.Scan() {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
        line := sc.Text()
        if line == "" {
            continue
        }
        lines = append(lines, line)
    }
    if err := sc.Err(); err != nil {
        return nil, fmt.Errorf("scan: %w", err)
    }
    if len(lines) == 0 {
        return nil, ErrEmptyLog
    }
    return lines, nil
}

大文件流式(不一次性 ReadLines):

func ScanFile(path string, fn func(line string) error) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    sc := bufio.NewScanner(f)
    for sc.Scan() {
        if err := fn(sc.Text()); err != nil {
            return err
        }
    }
    return sc.Err()
}
对比Java(java-dev ch07)Go
按行读Files.lines() / BufferedReaderbufio.Scanner
大文件Stream APIScanFile 回调,配合 ch10 pool
关闭try-with-resourcesdefer f.Close()

11.4 encoding/json:LogEntry 与 Product

// internal/model/entry.go
package model

import "time"

type LogEntry struct {
    Time       time.Time `json:"time"`
    Method     string    `json:"method"`
    Path       string    `json:"path"`
    Status     int       `json:"status"`
    DurationMs int       `json:"duration_ms"`
    RequestID  string    `json:"request_id"`
    UserSlug   string    `json:"user_slug"`
}

解析单行

// internal/logparser/parser.go
package logparser

import (
    "encoding/json"
    "errors"
    "fmt"

    "example.com/toolkit-go/internal/model"
)

var ErrEmptyLog = errors.New("empty log file")

type ParseStats struct {
    Parsed  int
    Skipped int
}

func ParseLine(line string) (model.LogEntry, error) {
    var e model.LogEntry
    if err := json.Unmarshal([]byte(line), &e); err != nil {
        return e, fmt.Errorf("%w: %v", ErrBadJSON, err)
    }
    return e, nil
}

var ErrBadJSON = errors.New("invalid json line")

func ParseAll(lines []string) ([]model.LogEntry, ParseStats) {
    var out []model.LogEntry
    var st ParseStats
    for _, ln := range lines {
        e, err := ParseLine(ln)
        if err != nil {
            st.Skipped++
            continue
        }
        out = append(out, e)
        st.Parsed++
    }

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。