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

项目介绍

今天我们用 OpenCode 写一个命令行并发 HTTP 负载测试工具——指定目标 URL、请求总数和并发数,工具会模拟并发请求并输出延迟统计和状态码分布。类似 ab(ApacheBench)的轻量替代,但代码只有 80 行左右。

  • 语言:Go
  • 核心功能:并发 HTTP 请求、延迟统计(min/max/avg/P50/P90/P99)、状态码分布
  • 技术点:goroutine 并发、channel 信号量控制并发度、sync.WaitGroup

准备工作

  • 安装 Go 1.18+:https://go.dev/dl/
  • 安装 OpenCode: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.Stringflag.Int 这类常规操作一步到位。描述清楚"几个参数、分别叫什么、默认值是什么"即可。

第二步:实现基础 HTTP 请求

我对 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:5 分钟完成,时间花在"想清楚要什么"而不是"怎么写",效率提升 3-4 倍

OpenCode 的强项在于对标准库和语言惯用法的深刻理解。你不需要说"用 channel 做信号量",它自己就知道。你不需要说"记得 Close Body",它自动补上。这种"懂行"的代码生成,才是 AI 编程助手的真正价值。