OpenCode 实践课:4 分钟用 Go 写出一个命令行 HTTP 负载测试工具

项目介绍

今天我们用一个实际的例子,展示如何用 OpenCode 在 4 分钟内从零写出一个命令行 HTTP 负载测试工具 gobench。它可以指定目标 URL、并发数和请求总数,测试完成后输出 QPS 和延迟分布(Min/Max/Avg/P50/P90/P99),效果类似 mini 版 wrk。

技术栈: Go 标准库(net/http、sync、sort、flag),零外部依赖。

最终效果:

$ go run gobench.go -url http://localhost:8080 -n 200 -c 20
Benchmarking http://localhost:8080 with 200 requests, concurrency 20...

Results:
  Total:      200
  Success:    200
  Fail:       0
  Duration:   1.234s
  QPS:        162.07 req/s

Latency:
  Min:  1.2ms
  Max:  45.6ms
  Avg:  12.3ms
  P50:  10.1ms
  P90:  22.8ms
  P99:  40.2ms

准备工作

  • Go 1.18+ 已安装
  • 已安装 OpenCode(npm install -g @anthropic-ai/opencode
  • 在终端中打开项目目录,运行 opencode 启动交互式会话

实践过程

第一步:生成项目骨架

我先向 OpenCode 描述了项目的基本需求:

> 我对 OpenCode 说:
> "帮我用 Go 标准库写一个 HTTP 负载测试工具,叫 gobench。接收 -url、-n(请求数)、-c(并发数)三个参数,用 goroutine 并发发送 GET 请求,统计成功/失败数量和总耗时。"

OpenCode 立刻理解了需求,生成了完整的 main.go,包括 flag 解析、HTTP 客户端、goroutine 并发控制和 WaitGroup 同步。它甚至自动加入了信号量 channel 来限制并发数——sem := make(chan struct{}, *c),这正是 Go 并发编程中控制协程数量的经典写法。

关键要点: 你不需要精确描述用 channel 做信号量,只需要说"限制并发数为 c",OpenCode 就能自动生成符合 Go 习惯的代码。

第二步:添加延迟统计

光有成功/失败计数不够,我需要看到延迟分布:

> 我对 OpenCode 说:
> "加上延迟统计,记录每个请求的耗时,最后输出 min/max/avg/p50/p90/p99。"

OpenCode 在代码中加入了 latencies 切片记录每次请求耗时,用 sort.Slice 排序后计算分位数。它自动判断失败的请求不应计入延迟统计,只在请求成功时才记录延迟数据。所有共享变量的读写都用了 mu.Lock() 加锁保护。

第三步:优化输出格式

输出太松散,我让 OpenCode 美化一下:

> 我对 OpenCode 说:
> "把输出格式对齐,加上 QPS 计算,延迟单位用人类可读的 ms/µs 格式。"

OpenCode 将输出改为对齐的两列样式,添加了 float64(*n)/elapsed.Seconds() 计算 QPS,延迟用 Round(time.Microsecond) 展示,Go 的 time.Duration.String() 会自动根据值大小选择 ms 或 µs 单位,不需要额外处理。

第四步:最终审查

> 我对 OpenCode 说:
> "检查一下代码有没有竞态条件或其他问题。"

OpenCode 审查后确认:latencies 切片用索引写入(每个 goroutine 写不同位置),mutex 保护了 success/fail 计数和延迟写入排序区段,整体没有竞态问题。还建议给 HTTP Client 加上超时避免请求卡死。

完整代码

package main

import (
	"flag"
	"fmt"
	"net/http"
	"sort"
	"sync"
	"time"
)

func main() {
	url := flag.String("url", "", "Target URL (required)")
	n := flag.Int("n", 100, "Number of requests")
	c := flag.Int("c", 10, "Concurrency level")
	flag.Parse()

	if *url == "" {
		fmt.Println("Usage: gobench -url <url> [-n requests] [-c concurrency]")
		return
	}

	client := &http.Client{Timeout: 10 * time.Second}
	var wg sync.WaitGroup
	sem := make(chan struct{}, *c)
	latencies := make([]time.Duration, *n)
	var success, fail int
	var mu sync.Mutex

	start := time.Now()
	for i := 0; i < *n; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			reqStart := time.Now()
			resp, err := client.Get(*url)

			mu.Lock()
			if err != nil || (resp != nil && resp.StatusCode >= 400) {
				fail++
			} else {
				success++
				latencies[idx] = time.Since(reqStart)
			}
			mu.Unlock()

			if resp != nil {
				resp.Body.Close()
			}
		}(i)
	}
	wg.Wait()
	elapsed := time.Since(start)

	var valid []time.Duration
	for _, l := range latencies {
		if l > 0 {
			valid = append(valid, l)
		}
	}
	sort.Slice(valid, func(i, j int) bool { return valid[i] < valid[j] })

	fmt.Printf("\nResults:\n")
	fmt.Printf("  Total:      %d\n", *n)
	fmt.Printf("  Success:    %d\n", success)
	fmt.Printf("  Fail:       %d\n", fail)
	fmt.Printf("  Duration:   %v\n", elapsed.Round(time.Millisecond))
	fmt.Printf("  QPS:        %.2f req/s\n", float64(*n)/elapsed.Seconds())

	if len(valid) > 0 {
		var sum time.Duration
		for _, l := range valid {
			sum += l
		}
		avg := sum / time.Duration(len(valid))
		p50 := valid[len(valid)*50/100]
		p90 := valid[len(valid)*90/100]
		p99 := valid[len(valid)*99/100]

		fmt.Println()
		fmt.Printf("Latency:\n")
		fmt.Printf("  Min:  %v\n", valid[0].Round(time.Microsecond))
		fmt.Printf("  Max:  %v\n", valid[len(valid)-1].Round(time.Microsecond))
		fmt.Printf("  Avg:  %v\n", avg.Round(time.Microsecond))
		fmt.Printf("  P50:  %v\n", p50.Round(time.Microsecond))
		fmt.Printf("  P90:  %v\n", p90.Round(time.Microsecond))
		fmt.Printf("  P99:  %v\n", p99.Round(time.Microsecond))
	}
}

小结

从写下第一个提示词到拿到完整可运行的负载测试工具,整个过程不到 4 分钟。如果手工编写,光是查 sync.WaitGroup 用法和信号量 channel 的写法就要花不少时间。

OpenCode 的核心价值在于三点:

即时翻译需求 — "并发控制" 直接翻译成 channel 信号量并自动加锁

自动考虑边界 — 失败请求不参与延迟统计,超时不会卡死

迭代式改进 — 先用一句话描述需求,再逐步调优细节

整个项目 88 行代码,零外部依赖,编译后只生成一个二进制文件,go run gobench.go 即开即用,非常适合日常做性能对比和接口压测。