学习目标
学完本章你应该能够:
- 说清为什么「单份 Thrift IDL 作为唯一可信源」能避免 HTTP 网关与 RPC 服务接口不一致。
- 讲清一份 IDL 如何同时承载三层信息:RPC 服务定义、HTTP 路由绑定、统一参数验证规则。
- 用
kitex和hz两条命令基于同一份 IDL 生成 RPC 与 HTTP 两层代码,并理解产物目录结构。 - 在 Kitex 层用全局中间件做兜底参数验证,在 Hertz 层用
BindAndValidate做入口验证,理解「分层验证职责」。 - 讲清为什么 RPC 层也要验证(不能只靠网关),以及生产环境如何把硬编码地址换成服务发现。
前置知识:Go 基础与 module 机制;了解 HTTP 与 RPC 的基本区别;知道 go install 与 go.mod 的基本用法即可。
本章你会动手做的事:
- 写一份含
api.query/api.body/api.vd注解的user.thrift,并生成两层代码。 - 在 Kitex 的
main.go注册ValidatorMiddleware,让非法参数在 RPC 层就被拦截。 - 启动 RPC 与网关,
curl一个user_id=0的请求,确认返回 400 校验错误。
一、前置环境准备
先安装所有必需的工具,确保版本匹配:
类比:这套工具链就像「翻译团队」。
thriftgo是总翻译,kitex/hz是两个不同语种的资深译员,而thrift-gen-validator是专门负责「校对语法(参数规则)」的质检员。你只要写好一份中文稿(IDL),三个译员就能各出各的版本,而且校对规则大家共用。
# 1. 安装Thrift代码生成核心
go install github.com/cloudwego/thriftgo@latest
# 2. 安装Kitex RPC代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
# 3. 安装Hertz HTTP代码生成工具
go install github.com/cloudwego/hertz/cmd/hz@latest
# 4. 安装参数验证代码生成插件(给Kitex用,与Hertz验证注解兼容)
go install github.com/cloudwego/thrift-gen-validator@latest
初始化项目(替换为你自己的模块名):
mkdir user-demo && cd user-demo
go mod init github.com/yourname/user-demo
二、编写共用 Thrift IDL(用户模块示例)
创建 idl/user.thrift,一份文件同时包含三层定义:
- RPC 服务接口定义(给 Kitex 用)
- HTTP 路由与参数绑定规则(给 Hertz 用,api.*注解)
- 统一参数验证规则(api.vd注解,两边工具都可识别)
flowchart LR
IDL[user.thrift 唯一可信源]
IDL --> L1[RPC 服务定义
service UserService]
IDL --> L2[HTTP 路由绑定
api.get / api.post
api.query / api.body]
IDL --> L3[统一参数验证
api.vd 注解]
L1 --> K[kitex 生成 RPC 代码]
L2 --> H[hz 生成 HTTP 代码]
L3 --> K
L3 --> Hnamespace go user
// 用户实体
struct User {
1: i64 id (api.vd="min=1,message=用户ID必须大于0")
2: string username (api.vd="required,min=3,max=20,message=用户名长度3-20位")
3: string email (api.vd="required,email,message=邮箱格式不正确")
}
// 查询用户请求
struct GetUserReq {
// api.query:HTTP层从query参数取值,RPC层正常传参
1: i64 user_id (api.query="user_id", api.vd="min=1,message=用户ID必须大于0")
}
// 创建用户请求
struct CreateUserReq {
// api.body:HTTP层从JSON Body取值
1: string username (api.body="username", api.vd="required,min=3,max=20")
2: string password (api.body="password", api.vd="required,min=6,message=密码至少6位")
3: string email (api.body="email", api.vd="required,email")
}
struct CreateUserResp {
1: User user
}
// 统一服务定义:Kitex生成RPC服务,Hertz生成HTTP路由
service UserService {
// HTTP GET /user/get?user_id=123
GetUser(1: GetUserReq req) (api.get="/user/get")
// HTTP POST /user/create,Body为JSON
CreateUser(1: CreateUserReq req) (api.post="/user/create")
}
三、基于同一份 IDL 生成两层代码
1. 生成 Kitex RPC 层代码(带验证标签)
执行命令生成 RPC 服务骨架、客户端 SDK、带validate标签的结构体:
kitex -module github.com/yourname/user-demo \
-service user-rpc \
-plugin validator \
-out ./cmd/user-rpc \
idl/user.thrift
- module:必须和 go.mod 的模块名完全一致,避免导入路径错误
- plugin validator:启用验证插件,自动给结构体生成validate标签
- out:RPC 服务骨架输出目录,kitex_gen代码会自动生成在项目根目录
2. 生成 Hertz HTTP 层代码(带验证标签)
执行命令生成 HTTP 网关的路由、Handler 骨架、带vd标签的结构体:
hz new -module github.com/yourname/user-demo \
-out ./cmd/gateway \
-idl ../../idl/user.thrift
后续修改 IDL 后,用hz update -idl ../../idl/user.thrift增量更新代码,不会覆盖你写的业务逻辑。
最终项目结构
flowchart TB
IDL[idl/user.thrift] -->|kitex -plugin validator| RPC[cmd/user-rpc
RPC 服务 + Handler]
IDL -->|hz new| GW[cmd/gateway
HTTP 网关 + 路由]
IDL -->|自动生成| GEN[kitex_gen/user
共用结构体]
RPC --> GEN
GW --> GEN
RPC -->|监听 8888| RUN((运行))
GW -->|监听 8080| RUNuser-demo/
├── idl/
│ └── user.thrift # 唯一的接口定义文件
├── kitex_gen/ # Kitex生成的RPC结构体、客户端、服务端代码
│ └── user/
├── cmd/
│ ├── user-rpc/ # Kitex RPC服务端入口与Handler
│ │ ├── main.go
│ │ └── handler.go
│ └── gateway/ # Hertz HTTP网关入口与路由
│ ├── main.go
│ └── biz/
│ ├── handler/ # HTTP业务Handler
│ └── router/ # 自动生成的路由
└── go.mod
四、实现 Kitex RPC 服务端(带全局参数验证)
1. 编写全局验证中间件
RPC 层做兜底校验,避免内部调用绕过网关导致非法参数。创建 cmd/user-rpc/middleware/validator.go:
package middleware
import (
"context"
"github.com/cloudwego/kitex/pkg/endpoint"
"github.com/go-playground/validator/v10"
)
// 全局单例validator,避免重复初始化
var validate = validator.New()
// ValidatorMiddleware RPC服务端参数验证中间件
func ValidatorMiddleware() endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
// 自动读取结构体的validate标签验证
if err = validate.Struct(req); err != nil {
return err
}
return next(ctx, req, resp)
}
}
}
2. 注册中间件并实现业务 Handler
修改 cmd/user-rpc/main.go,注册验证中间件:
package main
import (
"github.com/cloudwego/kitex/pkg/rpcinfo"
"github.com/cloudwego/kitex/server"
"github.com/yourname/user-demo/cmd/user-rpc/middleware"
"github.com/yourname/user-demo/kitex_gen/user/userservice"
"log"
)
func main() {
svr := userservice.NewServer(
new(UserServiceImpl),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "user-rpc"}),
server.WithMiddleware(middleware.ValidatorMiddleware()), // 注册全局验证中间件
)
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
修改 cmd/user-rpc/handler.go 实现业务逻辑(验证已经由中间件完成,Handler 只处理业务):
package main
import (
"context"
"github.com/yourname/user-demo/kitex_gen/user"
)
type UserServiceImpl struct{}
func (s *UserServiceImpl) GetUser(ctx context.Context, req *user.GetUserReq) (resp *user.User, err error) {
// 业务逻辑:查询数据库等,参数已经验证完成
return &user.User{
Id: req.UserId,
Username: "test_user",
Email: "test@example.com",
}, nil
}
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *user.CreateUserReq) (resp *user.CreateUserResp, err error) {
// 业务逻辑:创建用户,参数已经验证完成
return &user.CreateUserResp{
User: &user.User{
Id: 1001,
Username: req.Username,
Email: req.Email,
},
}, nil
}
五、实现 Hertz HTTP 网关(带参数验证 + RPC 调用)
类比:两层验证就像「机场安检 + 登机口复核」。Hertz 网关是安检口,先把明显不合规的行李(格式错误、必填缺失)拦在门外;Kitex RPC 是登机口复核,防止有人绕开安检口(比如内部服务直接调 RPC)登机。两道关职责不同,但都必不可少。
sequenceDiagram
participant C as 客户端
participant G as Hertz 网关
participant R as Kitex RPC
C->>G: GET /user/get?user_id=0
G->>G: BindAndValidate(入口验证)
alt 验证失败
G-->>C: 400 校验错误
else 验证通过
G->>R: GetUser(req)
R->>R: ValidatorMiddleware 兜底验证
alt 验证失败
R-->>G: error
G-->>C: 500
else 校验通过
R-->>G: User
G-->>C: 200 data
end
end1. 初始化 RPC 客户端
创建 cmd/gateway/biz/rpc/user.go,封装 Kitex RPC 客户端:
package rpc
import (
"github.com/cloudwego/kitex/client"
"github.com/yourname/user-demo/kitex_gen/user/userservice"
"log"
)
var UserClient userservice.Client
func InitRPC() {
c, err := userservice.NewClient(
"user-rpc",
client.WithHostPorts("127.0.0.1:8888"), // RPC服务地址,生产用服务发现
)
if err != nil {
log.Fatalf("RPC客户端初始化失败: %v", err)
}
UserClient = c
}
2. 实现 HTTP Handler(入口验证 + RPC 调用)
修改 cmd/gateway/biz/handler/user_service.go,Hertz 层做入口参数校验,拦截非法请求:
package handler
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/common/utils"
"github.com/cloudwego/hertz/pkg/protocol/consts"
"github.com/yourname/user-demo/cmd/gateway/biz/rpc"
"github.com/yourname/user-demo/cmd/gateway/hertz_gen/user"
kitexUser "github.com/yourname/user-demo/kitex_gen/user"
)
type UserServiceHandler struct{}
func NewUserServiceHandler() *UserServiceHandler {
return &UserServiceHandler{}
}
func (h *UserServiceHandler) GetUser(ctx context.Context, c *app.RequestContext) {
// 1. HTTP层自动绑定参数+验证(读取vd标签)
var req user.GetUserReq
if err := c.BindAndValidate(&req); err != nil {
c.JSON(consts.StatusBadRequest, utils.H{"code": 400, "msg": err.Error()})
return
}
// 2. 调用RPC服务(同一份IDL,字段一一对应直接转换)
rpcResp, err := rpc.UserClient.GetUser(ctx, &kitexUser.GetUserReq{UserId: req.UserId})
if err != nil {
c.JSON(consts.StatusInternalServerError, utils.H{"code": 500, "msg": err.Error()})
return
}
c.JSON(consts.StatusOK, utils.H{"code": 0, "data": rpcResp})
}
func (h *UserServiceHandler) CreateUser(ctx context.Context, c *app.RequestContext) {
var req user.CreateUserReq
if err := c.BindAndValidate(&req); err != nil {
c.JSON(consts.StatusBadRequest, utils.H{"code": 400, "msg": err.Error()})
return
}
rpcResp, err := rpc.UserClient.CreateUser(ctx, &kitexUser.CreateUserReq{
Username: req.Username,
Password: req.Password,
Email: req.Email,
})
if err != nil {
c.JSON(consts.StatusInternalServerError, utils.H{"code": 500, "msg": err.Error()})
return
}
c.JSON(consts.StatusOK, utils.H{"code": 0, "data": rpcResp})
}
3. 网关启动入口初始化 RPC
修改 cmd/gateway/main.go,启动前初始化 RPC 客户端:
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/yourname/user-demo/cmd/gateway/biz/rpc"
"github.com/yourname/user-demo/cmd/gateway/biz/router"
)
func main() {
rpc.InitRPC() // 初始化RPC客户端
h := server.Default(server.WithHostPorts(":8080"))
router.Register(h)
h.Spin()
}
六、运行与验证
- 启动 RPC 服务:go run ./cmd/user-rpc(默认监听 8888 端口)
- 启动 HTTP 网关:go run ./cmd/gateway(监听 8080 端口)
- 测试验证效果:
# 测试参数校验失败(user_id为0不符合min=1规则)
curl "http://localhost:8080/user/get?user_id=0"
# 返回:{"code":400,"msg":"Key: 'GetUserReq.UserId' Error:Field validation for 'UserId' failed on the 'min' tag"}
# 测试正常请求
curl "http://localhost:8080/user/get?user_id=123"
七、最佳实践
- IDL 唯一原则:接口变更只修改 Thrift 文件,重新生成代码,彻底避免 HTTP 与 RPC 接口不一致的问题。
- 分层验证职责:HTTP 层做格式校验(必填、长度、正则),RPC 层做业务校验(如用户唯一性、权限),边界清晰且双重兜底。
- 自定义验证规则:复杂验证逻辑(跨字段、业务规则)编写统一的 validator 自定义函数,在两层同时注册,保证规则完全一致。
- 生产环境优化:RPC 服务注册到服务发现中心(如 etcd、nacos),网关通过服务名发现实例,不要硬编码地址。
自测题与动手练习
自测题(合上书能答出来,才算懂):
- 为什么强调「单份 Thrift IDL 作为唯一可信源」?如果网关和 RPC 各写一份接口定义,长期会带来什么问题?
- 一份 IDL 里
api.query、api.body、api.vd三类注解分别给谁用?api.vd为什么能被 Kitex 和 Hertz 两边共用? kitex命令带-plugin validator和不带,生成的结构体有什么区别?为什么 RPC 层还需要验证?- 为什么「只靠网关验证」不够?什么场景下非法参数会绕过 Hertz 直奔 Kitex?
- 本章
main.go里 RPC 地址是硬编码的127.0.0.1:8888,生产环境该换成什么?为什么?
动手练习(建议真做一遍):
- 把
user.thrift的CreateUserReq.password改成min=10,重新kitex/hz update,确认两层都生效新规则。 - 写一个新的
validator自定义函数(比如「用户名不能包含空格」),在两层同时注册,验证跨层规则一致。 - 故意跳过网关、用 Kitex 生成的客户端直连 RPC 发一个
user_id=0的请求,确认ValidatorMiddleware仍能拦下。
本章小结
- 单份 Thrift IDL 同时承载 RPC 定义、HTTP 路由绑定、参数验证规则三层信息,
kitex与hz各自生成代码,从根本上杜绝接口不一致。 - 两层验证职责清晰:Hertz 网关做入口格式校验(必填/长度/正则),Kitex RPC 做兜底校验,防止内部调用绕过网关。
- 验证规则通过
api.vd注解统一声明,配合thrift-gen-validator插件两边共用,复杂规则可用自定义 validator 函数保证一致。 - 生产环境 RPC 地址应接入服务发现(etcd/nacos),避免硬编码。
- 掌握了「单份 IDL 驱动多端代码」的思路后,下一篇可以延伸到多服务、多 IDL 的仓库组织与代码生成流水线。