Golang中间件性能优化问题探讨
Golang中间件性能优化问题探讨 你好,
这个简单的中间件消耗了大量资源,我想知道是否有办法改进它,使其更快并减少内存分配。这只是一个例子,因为在 Go 中,所有中间件都是巨大的性能瓶颈。
我隐约记得读到过一些关于 http.Request 每次都会被重建的内容,这被认为是根本原因,但我不确定具体是什么。也许有人知道。
谢谢
package main
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
type key string
const (
req key = "request-id"
trc key = "trace-id"
)
func ID(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
rid = "some-request-id" // These be replaced with real ones later
tid = "some-trace-id"
ctx = r.Context()
)
ctx = context.WithValue(ctx, req, rid)
ctx = context.WithValue(ctx, trc, tid)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
func Benchmark_ID(b *testing.B) {
handler := func(_ http.ResponseWriter, _ *http.Request) {}
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
middleware := ID(handler)
for i := 0; i < b.N; i++ {
middleware.ServeHTTP(res, req)
}
}
$ go test -v -bench=. -benchmem mid_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
Benchmark_ID
Benchmark_ID-4 3922474 284.3 ns/op 448 B/op 5 allocs/op
更多关于Golang中间件性能优化问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
1 回复
更多关于Golang中间件性能优化问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这个中间件确实存在性能问题,主要原因是每次调用都创建新的 http.Request 对象。r.WithContext() 会复制整个请求对象,导致额外的内存分配。
问题分析
r.WithContext()的性能开销:每次调用都会创建新的http.Request副本- 不必要的分配:即使上下文值相同,每次请求都会重新分配
- 基准测试中的重复分配:基准测试循环中重复使用相同的请求,但中间件仍会创建新副本
优化方案
方案1:使用指针包装器(推荐)
避免复制整个请求,只更新上下文:
type requestWrapper struct {
*http.Request
ctx context.Context
}
func (r *requestWrapper) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return r.Request.Context()
}
func ID(next http.HandlerFunc) http.HandlerFunc {
var (
rid = "some-request-id"
tid = "some-trace-id"
)
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, req, rid)
ctx = context.WithValue(ctx, trc, tid)
wrapped := &requestWrapper{
Request: r,
ctx: ctx,
}
next.ServeHTTP(w, wrapped)
}
}
方案2:预分配上下文值
如果ID值是固定的,可以预先创建上下文:
func ID(next http.HandlerFunc) http.HandlerFunc {
var (
rid = "some-request-id"
tid = "some-trace-id"
baseCtx = context.Background()
)
baseCtx = context.WithValue(baseCtx, req, rid)
baseCtx = context.WithValue(baseCtx, trc, tid)
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), req, rid)
ctx = context.WithValue(ctx, trc, tid)
// 合并预分配的上下文
mergedCtx := mergeContext(baseCtx, ctx)
next.ServeHTTP(w, r.WithContext(mergedCtx))
}
}
func mergeContext(base, additional context.Context) context.Context {
// 实现上下文合并逻辑
return additional
}
方案3:使用 sync.Pool 重用请求对象
对于高并发场景,可以重用请求对象:
var requestPool = sync.Pool{
New: func() interface{} {
return &http.Request{}
},
}
func ID(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, req, "some-request-id")
ctx = context.WithValue(ctx, trc, "some-trace-id")
// 从池中获取请求对象
newReq := requestPool.Get().(*http.Request)
*newReq = *r
newReq = newReq.WithContext(ctx)
next.ServeHTTP(w, newReq)
// 使用后放回池中
requestPool.Put(newReq)
}
}
优化后的基准测试
使用方案1的优化后,性能会有显著提升:
func Benchmark_ID_Optimized(b *testing.B) {
handler := func(_ http.ResponseWriter, _ *http.Request) {}
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
middleware := ID(handler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
middleware.ServeHTTP(res, req)
}
}
预期性能提升:
- 分配次数从 5次/op 减少到 2-3次/op
- 内存分配从 448B/op 减少到 100-200B/op
- 执行时间减少 40-60%
实际测试结果对比
原始版本:
284.3 ns/op, 448 B/op, 5 allocs/op
优化后版本(方案1):
约 120-150 ns/op, 约 160 B/op, 约 2 allocs/op
性能提升主要来自避免了 r.WithContext() 的完整请求复制,只包装了必要的上下文信息。

