CloudWeGo-Hertz
1. 为什么选择 Hertz
如果你已经熟悉 Gin,可能会问:为什么还需要 Hertz?
1.1 Gin 的局限性
Gin 基于 Go 标准库的 net/http,这意味着:
- 网络模型:使用 Go 原生的 goroutine-per-connection 模型,在高并发场景下会产生大量协程切换开销
- 内存分配:每次请求都会分配新的
http.Request和http.ResponseWriter,GC 压力较大 - 扩展性:标准库的抽象层次较高,难以深度优化网络层
1.2 Hertz 的核心优势
Hertz 是字节跳动开源的 HTTP 框架,属于 CloudWeGo 微服务生态的一部分。它的设计目标是:
底层逻辑:Hertz 不依赖 net/http,而是基于自研的网络库 Netpoll(epoll/kqueue 封装),实现了:
- 零拷贝:通过
nocopyAPI 减少内存拷贝 - 对象池化:
RequestContext等核心对象复用,降低 GC 压力 - 协议层优化:HTTP/1.1 解析器手写优化,性能超过标准库
与 Gin 的 API 兼容性:Hertz 刻意保持了与 Gin 相似的 API 设计,迁移成本低。
2. 核心架构
2.1 分层设计
Hertz 采用三层架构:
应用层 (Handler/Middleware) ↓协议层 (HTTP Parser) ↓网络层 (Netpoll/Go Net)底层逻辑:这种分层使得 Hertz 可以灵活切换网络库。默认使用 Netpoll,但也支持降级到标准库(通过 WithTransport 选项)。
2.2 核心数据结构
RequestContext
Hertz 的核心是 app.RequestContext,它对应 Gin 的 gin.Context。
type RequestContext struct { conn network.Conn Request protocol.Request Response protocol.Response // ...}关键差异:
- Gin 的
Context包装了标准库的*http.Request - Hertz 的
RequestContext直接持有底层连接和自定义的Request/Response对象
底层逻辑:这种设计让 Hertz 可以:
- 复用
RequestContext对象(通过sync.Pool) - 直接操作底层 buffer,避免多次拷贝
- 支持流式读写(标准库的
http.Request.Body只能读一次)
3. 快速上手
3.1 基础示例
package main
import ( "context" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/server")
func main() { h := server.Default()
h.GET("/ping", func(ctx context.Context, c *app.RequestContext) { c.JSON(200, map[string]string{"msg": "pong"}) })
h.Spin() // 启动服务}与 Gin 的对比:
// Ginr.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"msg": "pong"})})
// Hertzh.GET("/ping", func(ctx context.Context, c *app.RequestContext) { c.JSON(200, map[string]string{"msg": "pong"})})核心差异:Hertz 的 Handler 多了一个 context.Context 参数,这是为了:
- 支持超时控制(通过
ctx.Done()) - 传递链路追踪信息(OpenTelemetry)
- 与 Kitex(gRPC 框架)保持一致的接口风格
3.2 参数绑定
Hertz 支持多种参数绑定方式,与 Gin 类似但更强大。
type LoginReq struct { Username string `json:"username" query:"username"` Password string `json:"password" query:"password"`}
h.POST("/login", func(ctx context.Context, c *app.RequestContext) { var req LoginReq // 自动根据 Content-Type 选择绑定方式 if err := c.Bind(&req); err != nil { c.JSON(400, map[string]string{"error": err.Error()}) return } c.JSON(200, req)})底层逻辑:Bind 方法会检查 Content-Type 头:
application/json→ JSON 解析application/x-www-form-urlencoded→ Form 解析multipart/form-data→ Multipart 解析
零拷贝优化:Hertz 提供 c.Query() 和 c.PostForm() 等方法,返回的字符串直接引用底层 buffer,避免拷贝。但要注意:这些字符串在请求结束后会失效,如需持久化需要手动拷贝。
4. 源码走读
4.1 Engine 结构
type Engine struct { trees MethodTrees // 路由树 maxParams uint16 allNoRoute HandlersChain allNoMethod HandlersChain noRoute HandlersChain noMethod HandlersChain pool sync.Pool // RequestContext 对象池 // ...}核心字段解析:
trees:每个 HTTP 方法(GET/POST/…)对应一棵前缀树(Radix Tree)pool:RequestContext对象池,每次请求从池中取出,处理完归还
底层逻辑:对象池化是 Hertz 性能优化的关键。每次请求的处理流程:
1. 从 pool 获取 RequestContext2. 重置字段(清空上次请求的数据)3. 解析 HTTP 请求4. 路由匹配 + 执行 Handler5. 写回响应6. 归还 RequestContext 到 pool4.2 路由树结构
Hertz 使用 Radix Tree(基数树)存储路由,与 Gin 相同。
type node struct { path string indices string children []*node handlers HandlersChain priority uint32 nType nodeType // ...}示例:注册以下路由
h.GET("/user/:id", handler1)h.GET("/user/:id/profile", handler2)h.GET("/user/admin", handler3)会构建如下树结构:
root └─ /user/ ├─ :id (handler1) │ └─ /profile (handler2) └─ admin (handler3)底层逻辑:
:id是参数节点,可以匹配任意值admin是静态节点,优先级更高(通过priority字段控制)- 匹配时先尝试静态节点,再尝试参数节点
5. 中间件机制
5.1 中间件执行流程
Hertz 的中间件模型与 Gin 完全一致,使用 Next() 方法控制执行流程。
func Logger() app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { start := time.Now()
c.Next(ctx) // 执行后续中间件和 Handler
latency := time.Since(start) fmt.Printf("[%s] %s %v\n", c.Method(), c.Path(), latency) }}
h.Use(Logger())底层逻辑:中间件和 Handler 被存储在 HandlersChain([]HandlerFunc)中。执行时通过索引遍历:
type RequestContext struct { handlers HandlersChain index int8 // 当前执行到第几个 Handler // ...}
func (c *RequestContext) Next(ctx context.Context) { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](ctx, c) c.index++ }}调用 c.Next(ctx) 会递增索引并执行下一个 Handler,形成洋葱模型。
5.2 中间件注册方式
// 全局中间件h.Use(middleware1, middleware2)
// 路由组中间件v1 := h.Group("/v1", authMiddleware){ v1.GET("/users", getUsers)}
// 单个路由中间件h.GET("/admin", adminMiddleware, adminHandler)底层逻辑:注册时,中间件会被合并到 HandlersChain 中:
[全局中间件...] + [路由组中间件...] + [路由中间件...] + [Handler]6. 与 Kitex 集成
Hertz 和 Kitex 是 CloudWeGo 生态的两大支柱,分别处理 HTTP 和 RPC。
6.1 为什么需要集成
在微服务架构中,常见场景:
- 网关层:使用 Hertz 接收 HTTP 请求
- 服务层:使用 Kitex 进行 RPC 调用
6.2 集成示例
假设你有一个 Kitex 定义的用户服务:
// 在 Hertz Handler 中调用 Kitex 客户端func GetUserHandler(ctx context.Context, c *app.RequestContext) { userID := c.Param("id")
// 调用 Kitex 客户端 client, _ := userservice.NewClient("user-service") resp, err := client.GetUser(ctx, &user.GetUserRequest{ UserId: userID, })
if err != nil { c.JSON(500, map[string]string{"error": err.Error()}) return }
c.JSON(200, resp)}底层逻辑:
- Hertz 的
context.Context可以直接传递给 Kitex,保持链路追踪信息 - Kitex 客户端会自动进行服务发现、负载均衡、超时控制
6.3 统一的 IDL 管理
推荐使用 Thrift 或 Protobuf 定义接口,同时生成 Hertz 和 Kitex 代码:
# 生成 Kitex 服务端代码kitex -module example.com/project -service user-service user.thrift
# 生成 Hertz HTTP 接口代码(基于同一份 IDL)hz new -module example.com/project -idl user.thrift这样可以保证 HTTP 接口和 RPC 接口的数据结构完全一致。
7. 性能优化要点
7.1 零拷贝 API
Hertz 提供了 nocopy 系列 API,避免不必要的内存拷贝。
// 不推荐:会拷贝字符串username := c.Query("username")go func() { fmt.Println(username) // 危险!请求结束后 username 可能失效}()
// 推荐:手动拷贝username := string(c.Query("username"))go func() { fmt.Println(username) // 安全}()底层逻辑:Hertz 的 Query()、PostForm() 等方法返回的字符串直接引用底层 buffer。这个 buffer 在请求结束后会被复用,因此:
- 在当前请求的生命周期内使用是安全的
- 如果需要在 goroutine 中使用或持久化存储,必须手动拷贝
7.2 流式处理
对于大文件上传/下载,使用流式 API 避免一次性加载到内存。
h.POST("/upload", func(ctx context.Context, c *app.RequestContext) { file, err := c.FormFile("file") if err != nil { c.String(400, "upload failed") return }
// 流式保存文件 src, _ := file.Open() defer src.Close()
dst, _ := os.Create("./uploads/" + file.Filename) defer dst.Close()
io.Copy(dst, src) // 流式拷贝,不会占用大量内存 c.String(200, "success")})7.3 连接池配置
Hertz 默认使用 Netpoll,可以通过选项优化连接池:
h := server.New( server.WithMaxRequestBodySize(4 * 1024 * 1024), // 限制请求体大小 server.WithIdleTimeout(60 * time.Second), // 空闲连接超时 server.WithReadTimeout(3 * time.Second), // 读超时 server.WithWriteTimeout(3 * time.Second), // 写超时)底层逻辑:
MaxRequestBodySize:防止恶意大请求耗尽内存IdleTimeout:及时回收空闲连接,减少资源占用ReadTimeout/WriteTimeout:防止慢客户端攻击
7.4 对象池化
除了 RequestContext,你也可以为自己的对象使用 sync.Pool:
var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) },}
func handler(ctx context.Context, c *app.RequestContext) { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }()
// 使用 buf 进行操作}8. Hertz vs Gin:迁移指南
如果你要从 Gin 迁移到 Hertz,主要改动点:
| 特性 | Gin | Hertz |
|---|---|---|
| Handler 签名 | func(*gin.Context) | func(context.Context, *app.RequestContext) |
| 启动方法 | r.Run(":8080") | h.Spin() |
| 参数绑定 | c.ShouldBind(&req) | c.Bind(&req) |
| 获取参数 | c.Query("key") | c.Query("key") (相同) |
| JSON 响应 | c.JSON(200, data) | c.JSON(200, data) (相同) |
核心差异:
- Handler 多了
context.Context参数 - 字符串可能引用底层 buffer(需注意生命周期)
- 默认使用 Netpoll(可切换到标准库)
9. 总结
Hertz 是为高性能场景设计的 HTTP 框架,核心优势在于:
- 网络层优化:基于 Netpoll,支持 epoll/kqueue,减少协程切换开销
- 内存优化:对象池化 + 零拷贝 API,降低 GC 压力
- 生态集成:与 Kitex 无缝配合,适合构建微服务网关
适用场景:
- 高并发 HTTP 服务(QPS > 10k)
- 需要与 Kitex 集成的微服务网关
- 对延迟敏感的业务(P99 < 10ms)
通过本文,你应该已经掌握了 Hertz 的核心原理和使用方法。接下来可以在实际项目中尝试使用,体会它在高并发场景下的性能优势。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


