第 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 输出列:
| path | count | avg_ms | max_ms | last_status |
|---|---|---|---|---|
| /api/v1/orders | 1 | 3200 | 3200 | 500 |
商品价格在其他模块仍用 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() / BufferedReader | bufio.Scanner |
| 大文件 | Stream API | ScanFile 回调,配合 ch10 pool |
| 关闭 | try-with-resources | defer 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++
}