今天我们用 OpenCode 写一个命令行并发 HTTP 负载测试工具——指定目标 URL、请求总数和并发数,工具会模拟并发请求并输出延迟统计和状态码分布。类似 ab(ApacheBench)的轻量替代,但代码只有 80 行左右。
npm install -g @anthropic/opencode(或参考 opencode.ai)mkdir go-loadtester cd go-loadtester opencode
启动 OpenCode 后,我们直接开始对话式开发。
我对 OpenCode 说:
> 创建一个 Go 项目,包名为 main,接受三个命令行参数:-url(目标地址)、-n(请求总数,默认 100)、-c(并发数,默认 10)。用一个 main.go 文件就行。
OpenCode 直接生成了完整的 main.go,包括 flag 包解析参数和一个入口函数。我检查了一下变量命名和默认值,完全符合预期,无需修改。
要点:OpenCode 对 Go 标准库非常熟悉,flag.String、flag.Int 这类常规操作一步到位。描述清楚"几个参数、分别叫什么、默认值是什么"即可。
我对 OpenCode 说:
> 在 main 函数里实现逻辑:循环 *n 次,每次发一个 GET 请求到 *url,记录每次请求的耗时,统计成功/失败数量。用 sync.Mutex 保护共享变量。
OpenCode 理解了需求,在 main 中添加了循环逻辑,time.Since(reqStart) 记录延迟,用 sync.Mutex 保护 latencies 切片和 errors 计数器。还自动记得 resp.Body.Close() 避免资源泄漏——Go 程序员的标准操作。
mu.Lock()
latencies = append(latencies, latency)
if err != nil {
errors++
} else {
resp.Body.Close()
}
mu.Unlock()
要点:OpenCode 不仅实现了功能,还遵循了 Go 的最佳实践(关闭 Body、Mutex 保护的临界区尽量小)。不需要我们额外提醒。
我对 OpenCode 说:
> 现在加并发:用 goroutine 并发执行请求,但用一个 channel 当信号量控制并发数不超过 *c。用 sync.WaitGroup 等待所有 goroutine 完成。记录总耗时。
OpenCode 生成了 goroutine + channel 信号量 + WaitGroup 的经典模式:
sem := make(chan struct{}, *c)
// ...
for i := 0; i < *n; i++ {
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
// HTTP request logic
}()
}
wg.Wait()
这个实现用带缓冲的 channel 做信号量非常地道——Go 社区的常见写法,OpenCode 直接采用了最佳方案。
要点:当你提到"信号量控制并发",OpenCode 不会真的去找 sync.Semaphore(Go 没有内置的信号量),而是自动用 channel 实现,说明它对 Go 的设计哲学有很好的理解。
我对 OpenCode 说:
> 请求全部完成后,输出统计信息:总请求数、并发数、总耗时、每秒请求数(RPS)、错误数。然后对 latencies 切片排序,输出 min、max、avg、P50、P90、P99 延迟。最后输出各 HTTP 状态码的分布。
OpenCode 使用 sort.Slice 排序后计算百分位:
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
p50 := latencies[len(latencies)*50/100]
p90 := latencies[len(latencies)*90/100]
p99 := latencies[len(latencies)*99/100]
还自动处理了 latencies 为空时的边界情况,避免除零和索引越界。
要点:OpenCode 会考虑边界情况(空切片),这是它优于简单代码生成的关键。虽然不会主动写出单元测试,但实现函数时通常会加上防护逻辑。
我对 OpenCode 说:
> 加上 URL 参数为空的提示,用 fmt.Printf 美化输出格式,延迟显示到微秒级别(如果太小就显示纳秒)。确保在零延迟或空数据情况下不会 panic。
OpenCode 添加了参数校验提示和格式化的输出表格,用 Round(time.Microsecond) 控制显示精度。
最终运行效果:
$ go run main.go -url https://www.baidu.com -n 100 -c 10 === Load Test Results === URL: https://www.baidu.com Requests: 100 Concurrency: 10 Duration: 2.153s RPS: 46.45 Errors: 0 Latency Stats: Min: 26.123ms Max: 2.053s Avg: 197.456ms P50: 156.789ms P90: 345.234ms P99: 1.234s Status Codes: 200: 100
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: go run main.go -url <URL> [-n requests] [-c concurrency]")
fmt.Println("Example: go run main.go -url https://example.com -n 100 -c 10")
return
}
var wg sync.WaitGroup
sem := make(chan struct{}, *c)
var mu sync.Mutex
var latencies []time.Duration
statusCodes := make(map[int]int)
var errors int
start := time.Now()
for i := 0; i < *n; i++ {
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
reqStart := time.Now()
resp, err := http.Get(*url)
latency := time.Since(reqStart)
mu.Lock()
latencies = append(latencies, latency)
if err != nil {
errors++
} else {
statusCodes[resp.StatusCode]++
resp.Body.Close()
}
mu.Unlock()
}()
}
wg.Wait()
elapsed := time.Since(start)
if len(latencies) == 0 {
fmt.Println("No requests completed.")
return
}
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
var total time.Duration
for _, l := range latencies {
total += l
}
avg := total / time.Duration(len(latencies))
p50 := latencies[len(latencies)*50/100]
p90 := latencies[len(latencies)*90/100]
p99 := latencies[len(latencies)*99/100]
fmt.Printf("\n=== Load Test Results ===\n")
fmt.Printf("URL: %s\n", *url)
fmt.Printf("Requests: %d\n", *n)
fmt.Printf("Concurrency: %d\n", *c)
fmt.Printf("Duration: %v\n", elapsed.Round(time.Millisecond))
fmt.Printf("RPS: %.2f\n", float64(*n)/elapsed.Seconds())
fmt.Printf("Errors: %d\n\n", errors)
fmt.Println("Latency Stats:")
fmt.Printf(" Min: %v\n", latencies[0].Round(time.Microsecond))
fmt.Printf(" Max: %v\n", latencies[len(latencies)-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\n", p99.Round(time.Microsecond))
fmt.Println("Status Codes:")
for code, count := range statusCodes {
fmt.Printf(" %d: %d\n", code, count)
}
}
这个 80 行的并发负载测试工具,从零开始到完整可运行,一共只对 OpenCode 说了 5 句话。每句话都是自然语言描述需求,不需要关心具体 API 或语法细节。
对比传统开发方式:
flag 文档、写并发逻辑、处理边界情况、调百分比计算,熟练工也要 15-20 分钟OpenCode 的强项在于对标准库和语言惯用法的深刻理解。你不需要说"用 channel 做信号量",它自己就知道。你不需要说"记得 Close Body",它自动补上。这种"懂行"的代码生成,才是 AI 编程助手的真正价值。