先交代一下背景,不然你会觉得我在瞎说
我上一个项目做的是 API 网关,Go 写的,日均 2000 万次请求,峰值 QPS 大概 1.2 万。选框架的时候团队吵了差不多两周,一半人拿着 TechEmpower 的榜单说 Fiber 排那么前,Gin 落后好几名,另一半说别搞事用 Gin。我当时站前者,差点就把 Fiber 推上生产了。后来我们拿一个小服务真跑了三个月,又灰头土脸换回来了。
所以这篇文章不是那种「梳理一下各自优劣」的八股。我只想讲一件事:性能排行是最不适合拿来选 Go 框架的东西,而且这个结论我是真金白银换来的。
先泼一盆冷水:你看到的那个 bench 数字,跟你的服务没关系
TechEmpower 最新一轮的 plaintext 测试里,跑 fasthttp 的应用能跑到几百万 req/s,net/http 系的通常在一百多万到两百万这个量级。差距看着吓人,对吧。但你先看看那个测试条件:40 多核的机器、开了 pipelining、响应体是 13 个字节的 Hello World。
真实业务长什么样?我给你算笔账。一次请求里至少一次 Redis(0.5ms 起步)+ 一次 MySQL(2ms 很常见)+ JSON 序列化(几十微秒)。Gin 一个请求的框架开销大概 20~40μs,Fiber 大概 10~15μs。差了 25μs,放在一条 2.5ms 的链路上占 1%。你说这 1% 值不值得你用中间件生态的灵活性去换?
我在一台 4 核 8G 的机器上用 wrk -t4 -c256 -d30s 跑过 hello world:Gin 大概 12 万 req/s,Fiber 能到 30 万以上。但只要往 handler 里塞一次数据库查询,两者的差距立刻塌到 10% 以内。框架的性能优势,在你写出第一行业务代码的瞬间就基本清零了。
真正该看的东西叫「抽象漏水程度」
这是我这些年最想跟人讲的一个观点:选后端框架的第一性问题不是快不快,是它的抽象漏不漏水,以及漏出来的水你能不能接住。
Go 生态里有个很反直觉的现象——性能越高的框架,抽象漏得越狠。Fiber 之所以快,是因为底下是 fasthttp,而 fasthttp 为了减少分配,把 RequestCtx 整个对象做了池化复用。代价是什么呢?
第一,它不实现 net/http 的 http.Handler 接口。otelhttp、net/http/pprof、gorilla/sessions、http.MaxBytesReader 这些你顺手的玩意,全部得换。OpenTelemetry 官方只维护 net/http 的 instrumentation,Fiber 得用社区维护的 contrib 包,出了 bug 你只能自己去提 PR。
第二,c.Body() 返回的 slice 在 handler 返回之后会被下一个请求覆盖。这是 fasthttp 文档里白纸黑字写的,但我见过至少三个人在 goroutine 里留着这个 slice,然后去查「为什么我的日志内容偶尔会串到别的请求上」。
第三,fasthttp 服务端不支持 HTTP/2。如果你的客户端要用 h2c,或者你前面没有一层 Nginx / ALB 去终结 TLS 和协议,这条直接判死刑。
Fiber 团队其实很努力在填这些坑,v3 也加了不少标准兼容的东西。但你选它之前,得先问自己一句:这个坑,我们团队谁负责背?
Gin 也不是白莲花,我踩的那三个坑
很多人把 Gin 当默认选项,觉得用的人多就稳。稳是稳,但它藏着几个非常隐蔽的坑,我是凌晨两点排查出来的。
坑一:c.Request.Context() 会随请求结束被 cancel。 我在 handler 里起了个 goroutine 去写审计日志,用的是 c.Request.Context()。结果是大部分日志写进去了,偶尔丢几条,报 context canceled。原因很简单——HTTP 请求返回后,标准库会 cancel 掉这个 context,你的后台 goroutine 拿着一个已经死掉的 ctx 去做 IO,必挂。正确做法是 context.WithoutCancel(c.Request.Context())(Go 1.21+)或者干脆新起一个 context。
坑二:c.Abort() 不等于 return。 这两个东西经常被混着用。Abort() 做的是把中间件索引设成 63,跳过后面所有的 handler,但当前这个函数里 Abort() 下面的代码照样会跑。我见过一个鉴权中间件,Abort() 之后还在往响应里写东西,结果客户端收到了两个 JSON body 拼在一起。
坑三:中间件的顺序敏感度极高。 Gin 的 c.Next() 语义是「先执行我前面的逻辑,跳到下一个中间件,回来再执行我后面的逻辑」,跟洋葱一样。你把 Recovery 放在 Logger 后面,panic 的时候日志就没了;把 CORS 放在 Auth 后面,预检请求直接被 401 拦下来。这套东西你看文档觉得懂了,真写起来还是要出错几次。
Echo 和 chi:被低估的两个选项
Echo 我用了大概两年,它最舒服的地方是错误处理是集中的——HTTPErrorHandler 一个口子出去,团队里谁写业务都别想自己拼一个 c.JSON(500, ...) 糊弄过去,错误码格式天然统一。代价是它定义了自己的 HTTPError 类型,你想和标准库的 errors.Is / errors.As 混着用,得绕一下。
chi 是被严重低估的一个。它的中间件签名就是标准的 func(http.Handler) http.Handler,也就是说你在 GitHub 上随手扒的任何 net/http 中间件都能直接往 Router 上挂,pprof、otelhttp、httprate、secure 全都能用。性能上它比 Gin 慢一点,但那个差距跟前面说的一样,业务一上来就没了。
而且有件事很多人没注意到:Go 1.22 之后标准库的 net/http.ServeMux 支持了方法和路径通配符,你可以直接写 mux.HandleFunc("GET /users/{id}", handler),还能用 r.PathValue("id") 取值。2023 年之前,这是大家用 chi 或者 Gin 的最主要理由之一,现在这个理由没了大半。
我的判断是:如果你只是想要个路由 + 三五个中间件,标准库 + log/slog + 手写一个 Recovery 就够了,连依赖都不用加。chi 现在真正的价值是 Route group 和它那套现成的中间件库。
我总结的一份 5 步选型清单
不讲道理,直接给步骤,你照着走一遍基本就有答案了。
第 1 步,看你的服务前面有没有一层反向代理。 有 Nginx、ALB、Envoy 终结 TLS 的话,HTTP/2 和连接复用的锅不用你背,Fiber 那点劣势就不存在了。没有的话,直接排除 fasthttp 系。
第 2 步,看你团队的可观测性方案。 如果你们全套 OpenTelemetry,并且希望用官方 contrib 包,选 net/http 系(Gin / Echo / chi / 标准库),省事程度差一个量级。
第 3 步,数一下你们要用几个第三方中间件。 超过 5 个,别碰 Fiber。少于等于 3 个,那 Fiber 完全能用,而且写起来确实爽。
第 4 步,看招聘难度。 这个角度很少人提,但真的真实。Gin 你在招聘网站上写出来,候选人几乎都写过;写 Fiber,一半人得先花两天看文档。你团队越大,这个成本越不能忽略。
第 5 步,也是最重要的一条——问问自己会不会用到 context.Context 的取消链路。 如果你们的请求链路里有超时控制、有跨服务传递 trace、有 goroutine 传递,那 fasthttp 的 RequestCtx 不是 context.Context 这件事会一直恶心你。这一条我认为是 2024 年之后最关键的分水岭,比性能重要得多。
最后说一个可能不太受欢迎的观点
这几年 Go 社区一直在讨论「要不要用框架」,吵来吵去没个结果。我的看法比较激进:Go 生态的框架红利期已经过去了,现在的大头在标准库和中间件上,不在 Router 上。
你想,Go 1.22 把路由语义补上了,log/slog 把日志补上了,errors.Join 和 %w 把错误链补上了,context 从 1.7 就在了。这些凑齐之后,一个框架到底还能帮你做什么?无非是参数绑定和校验(这个确实有用,Gin 的 binding:"required" 我至今觉得是真香)、依赖注入、和一些约定。但这些东西的复杂度,跟它带来的版本升级风险比,值不值,你自己掂量。
我现在的默认选择是这样的:小服务直接用标准库,中型服务上 chi,团队大、人多、要统一风格就上 Gin 或者 Echo。至于 Fiber,等哪天你的压测报告真的被框架开销卡住了,再考虑它也不迟——那种日子,我到现在还没遇到。