【电商项目】商品搜索开发复盘(1):根据需求拆解搜索接口的设计逻辑
2026/8/26 18:21:12
Goroutine泄漏是Go应用中最隐蔽的性能问题。一个泄漏的goroutine不仅消耗内存(每个约2-8KB),还会占用文件描述符、数据库连接等系统资源。随着时间推移,泄漏的goroutine会逐渐耗尽系统资源,导致OOM。本文将教你如何检测、定位和预防goroutine泄漏。
// 泄漏模式1:向无缓冲channel发送,没有接收者funcleakySender(){ch:=make(chanint)gofunc(){ch<-42// 永久阻塞,goroutine泄漏}()// ch从未被接收}// 泄漏模式2:从无缓冲channel接收,没有发送者funcleakyReceiver(){ch:=make(chanint)gofunc(){<-ch// 永久阻塞,goroutine泄漏}()// 没有发送者}// 泄漏模式:time.After在select中funcleakyTimer(){for{select{case<-time.After(time.Second):// 每次创建新TimerdoWork()}}// time.After创建的Timer在未触发前不会被GC}// 修复funcfixedTimer(){timer:=time.NewTimer(time.Second)defertimer.Stop()for{select{case<-timer.C:doWork()timer.Reset(time.Second)}}}// 泄漏模式:goroutine永不退出funcleakyBackground(){gofunc(){for{select{casedata:=<-inputCh:process(data)// 缺少退出机制!}}}()}// 修复:使用context或done channelfuncfixedBackground(ctx context.Context){gofunc(){for{select{casedata:=<-inputCh:process(data)case<-ctx.Done():return}}}()}// 最简单的监控funcmonitorGoroutines(){ticker:=time.NewTicker(10*time.Second)deferticker.Stop()forrangeticker.C{count:=runtime.NumGoroutine()log.Printf("当前goroutine数量: %d",count)ifcount>alarmThreshold{log.Printf("警告:goroutine数量超过阈值!")}}}import_"net/http/pprof"// 访问 http://localhost:6060/debug/pprof/goroutine?debug=1// 查看所有goroutine的堆栈// 代码级别获取funcdumpGoroutines(){pprof.Lookup("goroutine").WriteTo(os.Stderr,1)}import"go.uber.org/goleak"funcTestMain(m*testing.M){goleak.VerifyTestMain(m)}funcTestWorkerPool(t*testing.T){defergoleak.VerifyNone(t)pool:=NewWorkerPool(5)pool.Start()pool.Stop()// 确保所有goroutine已退出}// 集成到服务的监控中间件funcGoroutineMonitorMiddleware(thresholdint)func(http.Handler)http.Handler{returnfunc(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){before:=runtime.NumGoroutine()next.ServeHTTP(w,r)after:=runtime.NumGoroutine()ifafter-before>threshold{log.Printf("请求后goroutine增加异常: +%d, URL: %s",after-before,r.URL.Path)}})}}funcworker(ctx context.Context,input<-chanWork){for{select{casework:=<-input:process(work)case<-ctx.Done():return// 干净退出}}}import"golang.org/x/sync/errgroup"funcprocessBatch(ctx context.Context,items[]Item)error{g,ctx:=errgroup.WithContext(ctx)for_,item:=rangeitems{item:=item g.Go(func()error{returnprocessItem(ctx,item)})}returng.Wait()// 等待所有goroutine完成或第一个错误}ctx,cancel:=context.WithTimeout(context.Background(),10*time.Second)defercancel()// 确保被调用funcDiagnoseGoroutineLeak(){// 获取goroutine profileprofile:=pprof.Lookup("goroutine")varbuf bytes.Buffer profile.WriteTo(&buf,1)// 按goroutine状态统计lines:=strings.Split(buf.String(),"\n")running:=0waiting:=0for_,line:=rangelines{ifstrings.Contains(line,"[running]"){running++}elseifstrings.Contains(line,"["){waiting++}}fmt.Printf("Running: %d, Waiting: %d, Total: %d\n",running,waiting,runtime.NumGoroutine())}