项目地址:https://github.com/gone-io/gone
原文地址:https://github.com/gone-io/goner/blob/main/gin/benchmark_test.md
Gone-Gin 性能测试报告
文章目录
简介
Gone-Gin 是基于 Gin 框架的扩展,它提供了更加便捷的依赖注入和参数绑定功能,使得开发者可以更加专注于业务逻辑的实现,而不必关心请求参数的解析和绑定过程。本文档将介绍 Gone-Gin 的实现原理以及与原生 Gin 的性能对比。
实现原理
核心组件
HTTP 注入器 (HttpInjector)
- 负责将 HTTP 请求中的参数注入到处理函数的参数中
- 支持从不同来源(Body、Header、Query、Param、Cookie)注入参数
- 通过反射机制实现参数的自动绑定和类型转换
代理 (Proxy)
- 将用户定义的处理函数转换为 Gin 兼容的处理函数
- 支持多种函数签名,提供更灵活的编程模式
- 处理函数执行结果的统一响应处理
响应处理器 (Responer)
- 负责将处理函数的返回值转换为 HTTP 响应
- 支持多种返回值类型,包括结构体、Map、切片、错误等
- 提供统一的响应格式,简化错误处理
- 支持自定义响应包装函数,灵活定制返回格式
- 内置对业务错误和系统错误的处理机制
- 支持流式响应(SSE)和文件下载等高级功能
工作流程
- 用户定义处理函数,使用
gone:"http,xxx"
标签指定参数来源 - Gone-Gin 通过代理机制将用户函数转换为 Gin 兼容的处理函数
- 请求到达时,HTTP 注入器解析请求并将参数注入到用户函数中
- 用户函数执行完毕后,结果被响应处理器(Responer)统一处理并返回给客户端
响应处理机制
响应处理器(Responer)是 Gone-Gin 的核心组件之一,它负责将处理函数的返回值转换为 HTTP 响应。相比原生 Gin 需要手动构造响应,Gone-Gin 的响应处理机制更加智能和灵活:
自动类型识别:
- 结构体、Map、切片、数组等复杂类型自动转换为 JSON 响应
- 基本类型(如字符串、数字等)自动转换为文本响应
- 错误类型自动处理为错误响应,并根据错误类型设置适当的状态码
统一响应格式:
- 默认提供统一的响应格式
{"code": 0, "msg": "", "data": ...}
- 支持通过配置关闭统一格式,直接返回原始数据
- 支持自定义响应包装函数,灵活定制返回格式
- 默认提供统一的响应格式
错误处理机制:
- 区分业务错误(BusinessError)和系统错误(InnerError)
- 业务错误会返回给客户端,包含错误码、错误信息和相关数据
- 系统错误会被适当处理,避免敏感信息泄露,同时记录详细日志
高级功能支持:
- 支持流式响应(SSE),自动处理 Channel 类型的返回值
- 支持文件下载,自动处理 io.Reader 类型的返回值
- 支持多返回值处理,自动识别和处理错误
性能测试
测试环境
goos: darwin
goarch: arm64
cpu: Apple M1 Pro
测试用例
我们设计了四个基准测试用例来比较 Gone-Gin 和原生 Gin 的性能:
- BenchmarkProcessRequestWithInject:使用 Gone-Gin 处理完整的 HTTP 请求
- BenchmarkProcessRequestWithOriGin:使用原生 Gin 处理完整的 HTTP 请求
- BenchmarkProxyGinHandlerFunc:测试 Gone-Gin 代理机制的性能
- BenchmarkCallOriGinHandlerFunc:测试原生 Gin 处理函数的性能
测试代码示例
请求结构体定义
type Req struct {
A int `json:"a,omitempty"`
B int `json:"b,omitempty"`
C int `json:"c,omitempty"`
D int `json:"d,omitempty"`
E string `json:"e,omitempty"`
F string `json:"f,omitempty"`
}
Gone-Gin 处理函数
// Gone-gin 的 http 处理函数
func (c *ctr) httpHandler(in struct {
req *Req `gone:"http,body"`
}) string {
return "ok"
}
原生 Gin 处理函数
// 原生 Gin 处理函数
func originHandler(c *gin.Context) {
var req Req
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
c.String(http.StatusOK, "ok")
}
基准测试函数
// BenchmarkProcessRequestWithInject 测试 使用 gone-gin 处理请求
func BenchmarkProcessRequestWithInject(b *testing.B) {
_ = os.Setenv("GONE_SERVER_SYS-MIDDLEWARE_DISABLE", "true")
_ = os.Setenv("GONE_SERVER_RETURN_WRAPPED-DATA", "false")
gone.
NewApp(gin.Load, tracer.Load).
Load(&ctr{}).
Run(func(httpHandler http.Handler) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
b.StopTimer()
request := buildRequest()
response := buildResponse()
b.StartTimer()
httpHandler.ServeHTTP(response, request)
}
})
}
// BenchmarkProcessRequestWithOriGin 测试 使用 原生 gin 处理请求
func BenchmarkProcessRequestWithOriGin(b *testing.B) {
engine := gin.New()
engine.
POST("/api/test", originHandler)
b.ResetTimer()
for n := 0; n < b.N; n++ {
b.StopTimer()
request := buildRequest()
response := buildResponse()
b.StartTimer()
engine.ServeHTTP(response, request)
}
}
HTTP 注入器核心代码
// HTTP 注入器的核心实现 - 处理 Body 参数注入
func (s *httpInjector) injectBody(kind, key string, field reflect.StructField) (fn BindFieldFunc, err error) {
if s.isInjectedBody {
return nil, cannotInjectBodyMoreThanOnce(field.Name)
}
t := field.Type
switch t.Kind() {
case reflect.Struct, reflect.Map, reflect.Slice:
return func(ctx *gin.Context, structVale reflect.Value) error {
v := fieldByIndexFromStructValue(structVale, field.Index, field.IsExported(), field.Type)
body := reflect.New(t).Interface()
if err := ctx.ShouldBind(body); err != nil {
return NewParameterError(err.Error())
}
v.Set(reflect.ValueOf(body).Elem())
return nil
}, nil
case reflect.Pointer:
return func(ctx *gin.Context, structVale reflect.Value) error {
v := fieldByIndexFromStructValue(structVale, field.Index, field.IsExported(), field.Type)
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
if err := ctx.ShouldBind(v.Interface()); err != nil {
return NewParameterError(err.Error())
}
return nil
}, nil
// ... 其他类型处理
default:
return nil, unsupportedAttributeType(field.Name)
}
}
代理机制核心代码
// 代理机制的核心实现 - 构建代理函数
func (p *proxy) buildProxyFn(x HandlerFunc, funcName string, last bool) gin.HandlerFunc {
m := make(map[int]*bindStructFuncAndType)
args, err := p.funcInjector.InjectFuncParameters(
x,
func(pt reflect.Type, i int, injected bool) any {
switch pt {
case ctxPointType, ctxType, goneContextPointType, goneContextType:
return placeholder{
Type: pt,
}
}
p.injector.StartBindFuncs()
return nil
},
func(pt reflect.Type, i int, injected bool) any {
m[i] = &bindStructFuncAndType{
Fn: p.injector.BindFuncs(),
Type: pt,
}
return nil
},
)
// ... 错误处理
fv := reflect.ValueOf(x)
return func(context *gin.Context) {
// ... 性能统计
parameters := make([]reflect.Value, 0, len(args))
for i, arg := range args {
// ... 参数处理
if f, ok := m[i]; ok {
parameter, err := f.Fn(context, arg)
if err != nil {
p.responser.Failed(context, err)
return
}
parameters = append(parameters, parameter)
} else {
parameters = append(parameters, arg)
}
}
// 调用用户函数
values := fv.Call(parameters)
// 处理返回值
var results []any
for i := 0; i < len(values); i++ {
// ... 返回值处理
}
p.responser.ProcessResults(context, context.Writer, last, funcName, results...)
}
}
测试结果
BenchmarkProcessRequestWithInject-8 261738 4124 ns/op
BenchmarkProcessRequestWithOriGin-8 370796 3166 ns/op
BenchmarkProxyGinHandlerFunc-8 278386 3942 ns/op
BenchmarkCallOriGinHandlerFunc-8 387363 3016 ns/op
结果分析
完整请求处理性能:
- Gone-Gin: 4124 ns/op
- 原生 Gin: 3166 ns/op
- 性能差距: 约 30%
处理函数性能:
- Gone-Gin 代理: 3942 ns/op
- 原生 Gin 函数: 3016 ns/op
- 性能差距: 约 31%
从测试结果可以看出,Gone-Gin 相比原生 Gin 有一定的性能损耗,在同一个数量级上,这主要是由于以下原因:
- 反射机制:Gone-Gin 使用反射来实现参数注入和类型转换,这会带来一定的性能开销
- 代理层:Gone-Gin 增加了代理层来转换用户函数,增加了函数调用链的长度
- 额外功能:Gone-Gin 提供了更多的功能,如参数自动绑定、类型转换等,这些功能会带来额外的性能开销
优化建议
虽然 Gone-Gin 相比原生 Gin 有一定的性能损耗,但在大多数业务场景下,这种性能差距是可以接受的,因为它带来了更好的开发体验和更高的开发效率。如果需要进一步优化性能,可以考虑以下方面:
- 减少反射使用:尽可能减少反射的使用,或者使用缓存来减少反射的开销
- 优化代理层:简化代理层的实现,减少函数调用链的长度
- 按需加载:根据实际需求,只加载必要的功能,减少不必要的性能开销
- 使用更高效的数据结构:在处理大量请求时,使用更高效的数据结构来存储和处理数据
结论
Gone-Gin 通过提供更便捷的依赖注入和参数绑定功能,大大提高了开发效率,虽然相比原生 Gin 有一定的性能损耗,但在大多数业务场景下,这种性能差距是可以接受的。在选择使用 Gone-Gin 还是原生 Gin 时,需要根据实际业务需求和性能要求来权衡。
对于追求极致性能的场景,可以考虑使用原生 Gin;而对于注重开发效率和代码可维护性的场景,Gone-Gin 是一个更好的选择。