今天我们用一个实际的例子,展示如何用 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
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 即开即用,非常适合日常做性能对比和接口压测。