第一章:Go语言人混沌工程的底层认知与使命
混沌工程不是故障注入的简单堆砌,而是面向分布式系统韧性验证的科学实验范式。对Go语言开发者而言,其使命在于利用Go原生的并发模型、静态链接能力与轻量级运行时,构建可嵌入、可观测、可编排的混沌探针,而非依赖外部重型框架。
混沌工程的本质是控制实验
真正的混沌实验必须满足四个核心原则:建立稳态假设、设计真实扰动、最小化爆炸半径、自动化验证结果。例如,在HTTP服务中模拟延迟,不能仅用time.Sleep()阻塞goroutine,而应通过拦截HTTP RoundTripper或使用net/http/httptest构造可控响应流:
// 构建带可控延迟的RoundTripper,不影响主业务goroutine
type DelayRoundTripper struct {
base http.RoundTripper
delay time.Duration
}
func (d *DelayRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// 在独立goroutine中注入延迟,避免阻塞调用方
done := make(chan struct{})
go func() {
time.Sleep(d.delay)
close(done)
}()
<-done
return d.base.RoundTrip(req)
}
Go语言赋予混沌实践的独特优势
- 并发安全:
sync/atomic与chan天然支持高并发探针调度; - 零依赖部署:
CGO_ENABLED=0 go build -a -ldflags '-s -w'生成单二进制探针,可秒级注入容器; - 运行时洞察:通过
runtime.ReadMemStats与debug.ReadGCStats实时采集内存/ GC扰动指标。
关键实践路径
- 探针需内置健康自检:启动时校验目标端口连通性与权限;
- 扰动必须声明超时与恢复机制,禁止“只发不收”;
- 所有混沌操作日志须携带traceID并输出至标准错误流,便于与OpenTelemetry链路对齐。
| 能力维度 | Go原生支持方式 | 混沌场景示例 |
|---|---|---|
| 网络扰动 | net.DialTimeout, http.Transport定制 |
DNS劫持、连接拒绝 |
| 资源扰动 | runtime.GC(), debug.SetGCPercent(-1) |
内存泄漏诱导、GC暂停放大 |
| 时序扰动 | time.Now()重定向(via interface)、clock包 |
时钟偏移、时间跳跃 |
混沌工程师的终极交付物,不是一次成功的故障注入,而是可复现、可归因、可防御的系统韧性认知。
第二章:goleak——goroutine泄漏检测的黄金标准
2.1 goleak原理剖析:从runtime.GoroutineProfile到泄漏判定逻辑
goleak 的核心在于快照比对:采集测试前后活跃 goroutine 的堆栈快照,识别未被回收的“残留协程”。
Goroutine 快照采集
var profiles []runtime.StackRecord
n := runtime.GoroutineProfile(profiles[:0])
profiles = make([]runtime.StackRecord, n)
runtime.GoroutineProfile(profiles) // 填充所有活跃 goroutine 的栈帧信息
runtime.GoroutineProfile 返回当前所有非空闲 goroutine 的运行时栈记录;需两次调用——首次获取所需容量 n,第二次填充数据。参数为预分配切片,避免 GC 干扰检测精度。
泄漏判定逻辑
- 过滤掉 runtime 系统 goroutine(如
timerproc,sysmon) - 对比 baseline(测试前)与 final(测试后)快照,保留仅在 final 中出现的 goroutine
- 按栈顶函数+完整调用链哈希去重,避免误报瞬时 goroutine
| 判定维度 | 说明 |
|---|---|
| 栈顶函数名 | 如 http.HandlerFunc.ServeHTTP |
| 调用链深度 ≥ 3 | 排除 runtime.init 类短栈 |
| 非守护型 goroutine | 不匹配 runtime.gcBgMarkWorker 等 |
graph TD
A[采集 baseline 快照] --> B[执行测试代码]
B --> C[采集 final 快照]
C --> D[过滤系统 goroutine]
D --> E[差集计算 + 栈哈希归一化]
E --> F[报告泄漏 goroutine]
2.2 实战:在HTTP服务启动/关闭生命周期中精准捕获残留goroutine
HTTP服务器优雅启停时,常因未显式等待异步任务完成而遗留 goroutine。以下为关键检测与收敛方案:
检测残留 goroutine 的标准方法
使用 runtime.NumGoroutine() 在 http.Server.Shutdown() 前后采样对比:
func monitorGoroutines() {
before := runtime.NumGoroutine()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(err)
}
after := runtime.NumGoroutine()
if diff := after - before; diff > 0 {
log.Printf("⚠️ 残留 %d 个 goroutine", diff)
debug.WriteStacks() // 输出完整栈信息
}
}
runtime.NumGoroutine()返回当前活跃 goroutine 总数;debug.WriteStacks()将所有 goroutine 栈写入标准错误,便于定位未退出的协程。
常见残留场景与修复策略
| 场景 | 原因 | 修复方式 |
|---|---|---|
后台心跳协程未监听 ctx.Done() |
go func(){ for { time.Sleep(...) } }() |
改为 for !ctx.Done() { select { case <-ctx.Done(): return } } |
| HTTP 中间件启动长轮询 | http.HandlerFunc 内启 goroutine 但未绑定请求上下文 |
使用 r.Context() 并在 defer 中清理 |
生命周期钩子注入流程
graph TD
A[Start Server] --> B[注册 OnShutdown 回调]
B --> C[启动后台 goroutine]
C --> D[Shutdown 调用]
D --> E[执行 OnShutdown 清理]
E --> F[等待所有 goroutine 退出]
2.3 进阶:自定义goleak.IgnoreTopFunction实现业务无关性白名单过滤
在大型微服务中,goleak 默认仅忽略标准库协程(如 runtime.goexit),但业务框架常启动长期运行的守护协程(如 metrics reporter、健康检查心跳),导致误报。需解耦检测逻辑与业务代码。
核心机制:IgnoreTopFunction 的语义
goleak.IgnoreTopFunction 接收协程栈顶函数名(如 "github.com/myorg/pkg/monitor.(*Reporter).Run"),返回 true 则跳过该 goroutine 检测。
白名单注册示例
func init() {
goleak.AddOptions(
goleak.IgnoreTopFunction("github.com/myorg/pkg/monitor.(*Reporter).Run"),
goleak.IgnoreTopFunction("github.com/myorg/pkg/health.(*Checker).loop"),
)
}
✅ 逻辑分析:IgnoreTopFunction 匹配栈帧最顶层的函数全限定名(含包路径+结构体+方法),不依赖调用上下文,天然隔离业务逻辑;参数为字符串字面量,零反射、零运行时解析,性能无损。
推荐实践策略
- ✅ 优先使用
IgnoreTopFunction(精准、轻量) - ❌ 避免
IgnoreCurrent(污染测试上下文) - ⚠️ 禁用正则匹配(
IgnoreGoroutine)——易误伤
| 方式 | 匹配粒度 | 业务耦合度 | 维护成本 |
|---|---|---|---|
IgnoreTopFunction |
函数级 | 低(仅函数名) | 低 |
IgnoreGoroutine |
栈内容 | 高(含业务路径) | 高 |
2.4 调试:结合pprof goroutine trace定位泄漏源头协程栈帧
当 goroutine 持续增长却未退出时,runtime/pprof 的 trace 是定位阻塞点的黄金工具。
启动 trace 采集
go tool trace -http=:8080 ./myapp.trace
该命令启动 Web UI,自动解析 trace 文件并可视化调度、GC、goroutine 生命周期事件。
关键分析路径
- 在 UI 中点击 “Goroutine analysis” → 查看长生命周期 goroutine
- 追踪其 stack trace,定位阻塞调用(如
select{}无 default、channel 写入未消费)
典型泄漏栈帧特征
| 现象 | 栈帧示例片段 |
|---|---|
| channel 写入阻塞 | runtime.chansend1 → main.processLoop |
| 定时器未 stop | time.Sleep → runtime.timerproc |
| WaitGroup 未 Done | sync.runtime_Semacquire → main.worker |
// 示例泄漏协程:未关闭的 channel 监听
go func() {
for range ch { // 若 ch 永不关闭,此 goroutine 永不退出
process()
}
}()
range ch 编译为 runtime.chanrecv2 调用,trace 中表现为持续处于 Gwaiting 状态,且 stack trace 锁定在 runtime.gopark → chanrecv → 用户函数。需结合 pprof -goroutine 确认数量趋势,再用 trace 定位具体帧。
2.5 集成:将goleak嵌入testmain并接入CI/CD流水线实现门禁式防护
为什么需要 testmain 钩子
goleak 默认仅在 go test 单个包中生效,无法跨测试文件捕获全局 goroutine 泄漏。通过自定义 TestMain,可统一初始化检测器并在所有测试执行前后介入。
嵌入 testmain 的标准模式
func TestMain(m *testing.M) {
// 启动前:记录初始 goroutine 快照
goleak.VerifyTestMain(m,
goleak.IgnoreCurrent(), // 忽略当前 goroutine(testmain 自身)
goleak.IgnoreTopFunction("runtime.goexit"), // 忽略 runtime 底层函数
)
}
逻辑分析:
goleak.VerifyTestMain是门禁核心——它包装m.Run(),在测试开始前采集 baseline,结束后比对并报告新增未终止 goroutine。IgnoreCurrent()防止误报 testmain 主协程;IgnoreTopFunction排除已知安全的系统级调用栈。
CI/CD 流水线集成要点
| 环境 | 检查策略 | 失败响应 |
|---|---|---|
| PR 触发 | 强制启用 goleak + -race |
阻断合并 |
| nightly | 全量包扫描 + 堆栈采样深度=5 | 邮件告警 + Issue |
门禁流程可视化
graph TD
A[CI 触发] --> B[go test -run=. -count=1]
B --> C{goleak.VerifyTestMain}
C --> D[采集初始 goroutine 快照]
C --> E[执行全部测试]
C --> F[比对终态 goroutine]
F -->|泄漏存在| G[返回非0码 → 流水线失败]
F -->|无泄漏| H[测试通过]
第三章:go mod tidy——依赖洁净度与隐式goroutine风险消减
3.1 tidy背后的真实代价:间接引入含init goroutine的第三方模块分析
Go 的 go mod tidy 表面静默,实则可能悄然拉入带 init() 函数启动 goroutine 的依赖——这类模块在导入即执行,无法按需延迟初始化。
潜在风险示例
// github.com/badlib/v2/client.go(简化)
func init() {
go func() {
for range time.Tick(30 * time.Second) {
reportHealth() // 后台心跳,无 context 控制
}
}()
}
该 init 在模块首次被 import 时触发,即使主程序从未调用其任何导出函数。tidy 会因 transitive dependency(如 github.com/goodlib → github.com/badlib/v2)自动引入,且不提示副作用。
常见“隐性 init goroutine”来源
- 监控埋点 SDK(如
datadog-go,newrelic-go-agent) - 配置热加载器(如
viper的某些后端驱动) - 日志异步刷盘封装(如
zerolog的FileWriter变体)
| 模块名 | 是否含 init goroutine | 启动时机 | 可取消性 |
|---|---|---|---|
github.com/segmentio/kafka-go |
否 | 按需 | ✅ |
github.com/elastic/apm-agent-go |
是(metrics reporter) | import 时 |
❌ |
go.opentelemetry.io/otel/exporters/otlp/otlptrace |
是(batch sender) | init() 中启动 |
⚠️(需手动 Shutdown) |
graph TD
A[go mod tidy] --> B[解析所有 import]
B --> C{是否含 indirect 依赖?}
C -->|是| D[递归解析 go.mod]
D --> E[执行所有 init 函数]
E --> F[启动后台 goroutine]
F --> G[进程生命周期内常驻]
3.2 实践:利用go list -deps -f ‘{{.ImportPath}}’定位潜在goroutine污染源
Go 程序中隐式启动的 goroutine(如日志轮转、HTTP keep-alive、第三方库后台心跳)常成为并发泄漏源头。go list 提供静态依赖图谱能力,是低成本初筛手段。
依赖图谱扫描命令
go list -deps -f '{{.ImportPath}}' ./cmd/myserver | sort -u
-deps:递归列出当前包及其所有直接/间接依赖;-f '{{.ImportPath}}':仅输出标准导入路径(排除 vendor 冗余路径);sort -u:去重,聚焦唯一依赖项。
高风险依赖识别策略
- 优先检查含以下关键词的包:
cron,scheduler,ticker,workerhttp.(*Server).Serve,grpc.(*Server).Servelogrus/hooks,zap/core,prometheus/client_golang
典型污染源对照表
| 包名 | 潜在 goroutine 行为 | 触发条件 |
|---|---|---|
github.com/robfig/cron/v3 |
启动调度器 goroutine | cron.New() 后调用 .Start() |
gopkg.in/natefinch/lumberjack.v2 |
日志轮转监听 goroutine | lumberjack.Logger 配置 MaxAge > 0 |
分析流程
graph TD
A[执行 go list -deps] --> B[提取 import path 列表]
B --> C{是否含高危关键词?}
C -->|是| D[审查 init()/New() 调用链]
C -->|否| E[暂不标记]
3.3 防御:构建module-aware的go.mod校验脚本阻断高危依赖注入
核心校验逻辑
通过解析 go.mod 的 module path 与 require 块,识别非官方源、可疑域名及已知恶意模块(如 github.com/evil-dep)。
自动化校验脚本(Bash + go list)
#!/bin/bash
# 检查是否启用 module-aware 模式并扫描高危依赖
GO111MODULE=on go list -m -json all 2>/dev/null | \
jq -r '.Path | select(test("evil-dep|malware|\\.xyz$"))' | \
read -r bad; if [ -n "$bad" ]; then echo "BLOCKED: $bad"; exit 1; fi
逻辑说明:
go list -m -json all输出所有 module 的结构化元数据;jq过滤含恶意关键词或非常规顶级域(如.xyz)的模块路径;GO111MODULE=on强制启用 module-aware 模式,避免 GOPATH 降级干扰。
高危依赖特征表
| 类型 | 示例 | 风险等级 |
|---|---|---|
| 非标准域名 | github.com/user123/stdlib |
⚠️⚠️⚠️ |
| 已知恶意包 | golang.org/x/exp/unsafe |
⚠️⚠️⚠️⚠️ |
| 版本漂移包 | v0.0.0-20230101000000-abcdef |
⚠️⚠️ |
校验流程(Mermaid)
graph TD
A[读取 go.mod] --> B[解析 require 模块列表]
B --> C{是否匹配黑名单规则?}
C -->|是| D[拒绝构建并报错]
C -->|否| E[继续 CI 流程]
第四章:stress测试——高并发场景下goroutine洁净度的极限压测验证
4.1 stress工具链深度解析:-p、-timeout与-goroutines参数对泄漏暴露的影响机制
stress 工具通过可控并发扰动加速资源泄漏的可观测性,三类核心参数形成“压力强度—持续时间—调度粒度”三角约束:
参数协同作用模型
stress -p 4 -timeout 30s -goroutines 1000 --mem-bytes 1MB
-p 4:启动4个独立工作进程,实现OS级并行,绕过GMP调度器干扰,放大内存分配竞争;-timeout 30s:硬性终止阈值,避免无限泄漏掩盖真实泄漏点;-goroutines 1000:每进程内启动1000个goroutine,密集触发runtime.mheap.allocSpan,加速堆碎片暴露。
泄漏暴露敏感度对比
| 参数组合 | 内存泄漏显现时间 | GC STW增幅 | 适用场景 |
|---|---|---|---|
-p 1 -goroutines 10 |
>5min | +12% | 微弱泄漏基线测试 |
-p 4 -goroutines 1000 |
+310% | 快速定位泄漏热点 |
graph TD
A[启动stress] --> B[按-p创建OS进程]
B --> C[各进程内按-goroutines启动协程池]
C --> D[协程并发调用alloc/memclr]
D --> E{是否超-timeout?}
E -->|是| F[强制dump heap profile]
E -->|否| D
4.2 实战:模拟10万次并发请求+随机panic恢复,验证defer+recover组合的goroutine守恒性
场景构建
启动 100,000 个 goroutine,每个执行含概率 panic 的 HTTP 处理逻辑,并统一用 defer+recover 捕获。
func handleReq(id int) {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine %d recovered: %v", id, r)
}
}()
if rand.Float64() < 0.001 { // 0.1% panic 率
panic(fmt.Sprintf("req-%d failed", id))
}
time.Sleep(1 * time.Millisecond) // 模拟处理
}
逻辑分析:
defer+recover在当前 goroutine 内部生效,panic 不会传播,goroutine 正常退出(非崩溃终止)。id用于追踪生命周期,time.Sleep防止调度器过早回收。
并发控制与观测
使用 sync.WaitGroup 精确计数,配合 runtime.NumGoroutine() 快照比对:
| 阶段 | Goroutine 数量 | 说明 |
|---|---|---|
| 启动前 | ~4 | 主协程 + 系统协程 |
| 并发中 | ~100,004 | 10w 请求 + 主 + 系统 |
| 全部返回后 | ~4 | 严格守恒,无泄漏 |
守恒性验证流程
graph TD
A[启动10w goroutine] --> B{每个执行handleReq}
B --> C[defer+recover拦截panic]
C --> D[正常return退出]
D --> E[runtime.NumGoroutine()回归基线]
4.3 观测:结合goleak+stress+GODEBUG=schedtrace=1三重信号交叉验证泄漏路径
当常规pprof无法定位协程泄漏源头时,需构建多维观测信号面。
三重信号协同逻辑
goleak捕获测试结束时残留 goroutine 的栈快照;stress并发压测放大泄漏节奏,缩短复现窗口;GODEBUG=schedtrace=1输出调度器级时间线,标记 goroutine 创建/阻塞/唤醒事件。
典型验证流程
GODEBUG=schedtrace=1 go test -race -run TestConcurrentWrite \
-timeout 30s -count=1 \
-exec "stress -p 4 -m 200M -time 10s"
参数说明:
-p 4启动4个压力进程模拟竞争;-m 200M限制内存防OOM;-time 10s与测试超时对齐。schedtrace日志将按 10ms 粒度输出 goroutine 状态跃迁,与 goleak 报告中的 goroutine ID 跨日志反查。
| 工具 | 观测维度 | 泄漏特征锚点 |
|---|---|---|
| goleak | goroutine 存活态 | created by xxx.go:123 |
| stress | 时间放大效应 | 泄漏速率随 -p 增长呈非线性 |
| schedtrace | 调度生命周期 | GC 后仍存在 RUNNING 状态 goroutine |
graph TD
A[goleak发现残留goroutine] --> B{ID匹配schedtrace?}
B -->|是| C[定位创建栈+阻塞点]
B -->|否| D[检查test cleanup逻辑]
C --> E[结合stress压测确认复现稳定性]
4.4 治理:基于stress失败用例反向生成goroutine生命周期单元测试模板
当 stress 工具暴露出 goroutine 泄漏(如 runtime.GoroutineProfile 显示持续增长),可反向提取其失败现场的调度特征,构建可复现的生命周期测试骨架。
核心提取逻辑
从 panic 日志与 pprof goroutine trace 中提取:
- 启动点(
go func()调用栈深度) - 阻塞点(
select{}/chan recv/time.Sleep) - 退出条件(显式
return、panic或被context.Cancel中断)
自动生成模板示例
func TestHTTPHandler_GoroutineLifecycle(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // 触发 cleanup 信号
// 模拟 stress 下高并发启动
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
handleRequest(ctx) // 实际业务逻辑,含 channel wait
}()
}
// 等待所有 goroutine 进入阻塞态后触发 cancel
time.Sleep(10 * time.Millisecond)
cancel()
wg.Wait()
// 断言:无残留 goroutine(通过 runtime.NumGoroutine() 基线比对)
if n := runtime.NumGoroutine(); n > baseline+2 {
t.Fatalf("leaked goroutines: got %d, want <= %d", n, baseline+2)
}
}
逻辑分析:该模板强制注入
context控制流,将 stress 中非确定性超时转化为可断言的生命周期边界;baseline应在init()中采集空载 goroutine 数,消除运行时噪声。参数500ms对应 stress 的-timeout阈值,确保 cancel 早于死锁发生。
| 组件 | 作用 | 可配置项 |
|---|---|---|
context.WithTimeout |
提供统一退出信号源 | 超时值、取消时机 |
sync.WaitGroup |
同步启动/等待,避免竞态观测 | 并发数、启动延迟 |
runtime.NumGoroutine |
量化泄漏,替代不可靠的 pprof 扫描 | 基线偏移量(+2 容忍) |
graph TD
A[Stress 失败日志] --> B[提取 goroutine 阻塞栈]
B --> C[识别 channel/select/context 依赖]
C --> D[生成带 cancel 控制的测试骨架]
D --> E[注入基准 goroutine 计数断言]
第五章:通往100% goroutine洁净度的工程化终局
某支付网关的goroutine泄漏根因定位实战
某日生产环境告警:pprof/goroutine?debug=2 显示活跃 goroutine 从常规 800+ 暴增至 120,000+,CPU 持续 98%,服务响应延迟 P99 超 8s。团队通过 runtime.NumGoroutine() 定时上报 + Prometheus 告警触发快照机制,在故障窗口内自动抓取三组堆栈:
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines_0s.txtcurl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines_30s.txtcurl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines_60s.txt
比对发现 92% 的 goroutine 停留在 vendor/github.com/redis/go-redis/v9.(*Client).Subscribe 的 conn.readLoop 中——根源是未对 redis.PubSub.Receive() 设置超时上下文,且订阅者在连接异常后未调用 Close()。
标准化 Goroutine 生命周期治理清单
| 检查项 | 合规示例 | 高危模式 | 自动化检测方式 |
|---|---|---|---|
| Context 绑定 | ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) |
context.Background() 直接使用 |
go vet + custom staticcheck rule ST1024 |
| Channel 关闭保障 | defer close(ch) + select { case <-ctx.Done(): return } |
for range ch 无退出守卫 |
CodeQL 查询 *.go: "for.*range.*chan" and not "select.*ctx.Done" |
生产级 goroutine 安全网关中间件
我们为所有 HTTP handler 注入统一 wrapper:
func GoroutineGuard(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// 启动 goroutine 计数器(每请求独立)
start := runtime.NumGoroutine()
defer func() {
end := runtime.NumGoroutine()
if end-start > 5 {
log.Warn("high-goroutine-growth", "delta", end-start, "path", r.URL.Path)
// 上报至 tracing tag & 触发告警
}
}()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
持续验证闭环流程
flowchart LR
A[CI 构建阶段] --> B[注入 goroutine leak detector]
B --> C[运行 30s 压测:wrk -t4 -c100 -d30s http://127.0.0.1:8080/api]
C --> D{goroutine delta > 10?}
D -- Yes --> E[阻断构建,输出 pprof/goroutine 堆栈]
D -- No --> F[允许发布]
E --> G[自动生成 GitHub Issue,关联 PR]
线上熔断与自愈机制
在核心订单服务中部署 GoroutineThrottler:当 runtime.NumGoroutine() 连续 5 秒超过阈值(动态计算:2 * avg_last_5m + 500),自动触发:
- 拒绝新请求(HTTP 503)
- 强制终止所有非守护型 goroutine(通过
sync.Map管理 goroutine ID +cancel()传播) - 将当前
runtime.Stack()写入/tmp/goroutine_dump_$(date +%s).log并上传 S3
该机制上线后,同类泄漏事故从月均 3.2 次降至 0 次,平均恢复时间从 47 分钟压缩至 83 秒。
全链路可观测性增强
在 OpenTelemetry Collector 中新增 goroutine metric receiver,采集指标:
go_goroutines(原生)go_goroutines_by_function{fn="redis.(*Client).Subscribe"}(通过 stack trace 解析)go_goroutines_leak_rate_per_minute(滑动窗口差分)
告警规则配置为:rate(go_goroutines_leak_rate_per_minute[5m]) > 20触发 PagerDuty。
工程文化落地实践
每个新功能 PR 必须包含 goroutine_profile_test.go,示例如下:
func TestOrderCreate_GoroutineLeak(t *testing.T) {
start := runtime.NumGoroutine()
for i := 0; i < 100; i++ {
go func() { _ = createOrder(context.Background()) }()
}
time.Sleep(100 * time.Millisecond)
if runtime.NumGoroutine()-start > 5 {
t.Fatal("leak detected")
}
} 