下载工作台
Go 编程实战

毕业项目 toolkit-go

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

第 15 章 · 毕业项目 toolkit-go 完整 Walkthrough

本章目标:综合运用 ch01~ch14,完成 toolkit-go 命令行工具:读取 JSON Lines access 日志 → 按 path 聚合慢请求 → Top N 排序 → 输出 CSV 运营日报;编写 ≥3 单元测试go build 产出单二进制;按 100 分验收表自评;逐文件说明工程结构,衔接 gin-web ch01

学时建议:6~8 小时(含算法复习 1 小时 + 项目实现 5 小时)

前置:完成 ch01~ch14;目录 ~/learn-go/toolkit-gogo mod init example.com/toolkit-go


15.1 项目背景与交付物

toolkit-go 是虚构运维团队 toolkit-team 的 CLI,服务 贤紫优选 商城网关日志分析。运营同学每日需要:

  1. 读取 access.log.jsonl(JSON Lines,与 gin-web 访问日志字段兼容);
  2. 找出 慢于阈值 的请求,按 path 聚合 count/avg/max;
  3. 输出 Top N 路径的 CSV 日报,导入 Excel;
  4. 可选启动 HTTP /health 探针(ch12 选修)。

不连接真实生产;数据均为本地样本;用户标识仅用 user-demo 等虚构 slug。

序号交付物说明
1cmd/toolkit/main.go主入口,flags
2data/access.log.jsonl样本日志 ≥20 行
3report-YYYYMMDD.csv运行后生成的日报
4go.mod / go.sum模块与依赖校验
5README.md用法、flags、示例
6internal/**/_test.go≥3 单元测试
7toolkittoolkit.exego build 单二进制

对标 java-dev ch17 toolkit-demo-graduationpython-dev ch16~ch17 日报项目;Go 版强调 单二进制部署并发解析(ch09/10)。


15.2 推荐目录结构

~/learn-go/toolkit-go/
├── cmd/
│   ├── toolkit/
│   │   └── main.go              # CLI 入口
│   └── probe/                   # 选修 HTTP 探针 ch12
│       └── main.go
├── internal/
│   ├── model/
│   │   └── entry.go             # LogEntry、Product
│   ├── logparser/
│   │   ├── reader.go            # ReadLines、ScanFile
│   │   ├── parser.go            # ParseLine、ParseAll
│   │   ├── concurrent.go        # ch09 并发解析
│   │   └── pool.go              # ch10 Worker Pool
│   ├── report/
│   │   ├── aggregate.go         # AggregateSlow
│   │   ├── csv.go               # CSVReporter
│   │   └── slice.go             # MapToSlice
│   ├── sortalgo/
│   │   ├── topn.go              # TopNSlowRows ch14
│   │   └── bubble.go            # 教学选修
│   └── app/
│       └── runner.go            # RunReport 总控
├── pkg/
│   └── id/
│       └── id.go                # request_id ch08
├── data/
│   └── access.log.jsonl
├── go.mod
├── go.sum
└── README.md

模块路径example.com/toolkit-go(教学用,勿与真实域名混淆)。


15.3 日志格式与 CSV 输出

输入 — 每行一个 JSON(字段虚构):

{"time":"2026-08-19T10:00:01Z","method":"GET","path":"/api/v1/products/go-handbook","status":200,"duration_ms":45,"request_id":"req-demo-001","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-002","user_slug":"user-demo"}
{"time":"2026-08-19T10:00:03Z","method":"GET","path":"/api/v1/cart","status":200,"duration_ms":890,"request_id":"req-demo-003","user_slug":"user-demo"}

输出 CSVreport-20260819.csv

path,count,avg_ms,max_ms,last_status
/api/v1/orders,1,3200,3200,500
/api/v1/cart,1,890,890,200
含义
path请求路径
count慢请求次数(≥ min-ms)
avg_ms平均耗时
max_ms最大耗时
last_status最后一条的状态码

15.4 逐文件说明

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"`
}

type Product struct {
    Slug        string `json:"slug"`
    Name        string `json:"name"`
    Price       int64  `json:"price"` // 分
    IsPublished bool   `json:"is_published"`
}

其他文件(复用前几章)

文件来源职责
logparser/reader.goch11ReadLines + context
logparser/parser.goch11ParseLine、ParseAll
report/aggregate.goch11AggregateSlow
report/csv.goch11CSVReporter.Write
sortalgo/topn.goch14TopNSlowRows

internal/app/runner.go

package app

import (
    "context"
    "fmt"
    "log"
    "time"

    "example.com/toolkit-go/internal/logparser"
    "example.com/toolkit-go/internal/report"
    "example.com/toolkit-go/internal/sortalgo"
)

type Config struct {
    Input     string
    Output    string
    MinMs     int
    TopN      int
    Workers   int
    Timeout   time.Duration
    Verbose   bool
}

type Result struct {
    Parsed  int
    Skipped int
    Rows    int
    OutPath string
}

func RunReport(ctx context.Context, cfg Config) (Result, error) {
    lines, err := logparser.ReadLines(ctx, cfg.Input)
    if err != nil {
        return Result{}, fmt.Errorf("read: %w", err)
    }
    entries, stats := logparser.ParseAll(lines)
    if cfg.Verbose {
        log.Printf("parsed=%d skipped=%d", stats.Parsed, stats.Skipped)
    }

    agg := report.AggregateSlow(entries, cfg.MinMs)
    rows := report.MapToSlice(agg)
    top := sortalgo.TopNSlowRows(rows, cfg.TopN)

    out := cfg.Output
    if out == "" {
        out = fmt.Sprintf("report-%s.csv", time.Now().Format("20060102"))
    }
    if err := report.CSVReporter{OutPath: out}.Write(top); err != nil {
        return Result{}, fmt.Errorf("write csv: %w", err)
    }

    return Result{
        Parsed:  stats.Parsed,
        Skipped: stats.Skipped,
        Rows:    len(top),
        OutPath: out,
    }, nil
}

15.5 cmd/toolkit/main.go 完整入口

package main

import (
    "context"
    "errors"
    "flag"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

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

func main() {
    input := flag.String("input", "data/access.log.jsonl", "JSON Lines log file")
    output := flag.String("output", "", "csv output (default report-YYYYMMDD.csv)")
    minMs := flag.Int("min-ms", 500, "slow request threshold in ms")
    top := flag.Int("top", 10, "top N slow paths")
    workers := flag.Int("workers", 4, "concurrent workers (future use)")
    timeout := flag.Duration("timeout", 60*time.Second, "max run duration")
    verbose := flag.Bool("v", false, "verbose logging")
    flag.Parse()

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    ctx, cancel := context.WithTimeout(ctx, *timeout)
    defer cancel()

    _ = workers // ch10 并发版可在此启用 RunParsePool

    res, err := app.RunReport(ctx, app.Config{
        Input:   *input,
        Output:  *output,
        MinMs:   *minMs,
        TopN:    *top,

以下内容需解锁后阅读

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

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