CloudWeGo-Hertz

2143 字
11 分钟
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 封装),实现了:

  • 零拷贝:通过 nocopy API 减少内存拷贝
  • 对象池化: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 可以:

  1. 复用 RequestContext 对象(通过 sync.Pool)
  2. 直接操作底层 buffer,避免多次拷贝
  3. 支持流式读写(标准库的 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 的对比:

// Gin
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "pong"})
})
// Hertz
h.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 获取 RequestContext
2. 重置字段(清空上次请求的数据)
3. 解析 HTTP 请求
4. 路由匹配 + 执行 Handler
5. 写回响应
6. 归还 RequestContext 到 pool

4.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 代码:

Terminal window
# 生成 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,主要改动点:

特性GinHertz
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) (相同)

核心差异:

  1. Handler 多了 context.Context 参数
  2. 字符串可能引用底层 buffer(需注意生命周期)
  3. 默认使用 Netpoll(可切换到标准库)

9. 总结#

Hertz 是为高性能场景设计的 HTTP 框架,核心优势在于:

  1. 网络层优化:基于 Netpoll,支持 epoll/kqueue,减少协程切换开销
  2. 内存优化:对象池化 + 零拷贝 API,降低 GC 压力
  3. 生态集成:与 Kitex 无缝配合,适合构建微服务网关

适用场景:

  • 高并发 HTTP 服务(QPS > 10k)
  • 需要与 Kitex 集成的微服务网关
  • 对延迟敏感的业务(P99 < 10ms)

通过本文,你应该已经掌握了 Hertz 的核心原理和使用方法。接下来可以在实际项目中尝试使用,体会它在高并发场景下的性能优势。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CloudWeGo-Hertz
https://www.lansganbs.cn/posts/项目开发/cloudwego-hertz/
作者
Zowely
发布于
2026-02-01
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zowely
红叶最多情,一舞寄相思。
公告
欢迎来到我的博客!这里分享计算机等相关内容。