纲要
- 项目回顾:
user-rpc已完成,亟需暴露 HTTP 接口 - 搭建
user-api服务- 创建
user.api与userapi.api文件 - 使用
goctl api go生成代码 - 配置路由、中间件与 RPC 客户端
- 创建
- 实现 API 业务逻辑
- 注册接口:调用
user-rpc.Register - 登录接口:调用
user-rpc.Login - 用户详情接口:从 JWT 中提取
uid,调用user-rpc.GetUserInfo
- 注册接口:调用
- 统一响应输出设计
- 标准响应结构体(状态码、消息、数据)
- 自定义
okHandler与errorHandler - 区分业务错误、gRPC 错误与未知错误
- 在
main.go中注册自定义 handler
- 测试验证
- 使用 API 调试工具测试注册、登录、详情
- 错误场景下的响应格式验证
背景
在之前的内容中,我们基于 go-zero@latest 完成了 user-rpc 微服务的开发,实现了注册、登录、搜索、详情等 gRPC 接口,并深入探讨了缓存与数据库一致性机制。现在需要向上延伸一层:为前端或其他服务提供 RESTful API。本篇文章将构建 user-api 服务,将 RPC 能力封装为 HTTP 接口,并设计统一的响应格式以提升客户端对接体验。
项目结构更新
在 apps/user/ 下新增 api 目录,与 rpc 并列。整体结构如下:
apps/user/
├── api/
│ ├── internal/
│ │ ├── config/
│ │ ├── handler/
│ │ ├── logic/
│ │ ├── svc/
│ │ └── types/
│ ├── user.api
│ └── userapi.api
└── rpc/
├── internal/
└── user.proto
定义 API 描述文件
go-zero 使用 .api 文件定义 HTTP 服务,包括路由、请求/响应结构体、中间件等。创建 apps/user/api/user.api:
type (
RegisterReq {
Phone string `json:"phone"`
Username string `json:"username"`
Password string `json:"password"`
Avatar string `json:"avatar,optional"`
Gender int32 `json:"gender,optional"`
}
RegisterResp {
Uid string `json:"uid"`
Token string `json:"token"`
}
LoginReq {
Phone string `json:"phone"`
Password string `json:"password"`
}
LoginResp {
Uid string `json:"uid"`
Token string `json:"token"`
}
GetUserInfoReq {
// uid 从 JWT 中获取,不对外暴露
}
GetUserInfoResp {
Uid string `json:"uid"`
Username string `json:"username"`
Phone string `json:"phone"`
Avatar string `json:"avatar"`
Gender int32 `json:"gender"`
}
)
@server(
prefix: /v1/user
)
service user-api {
@handler RegisterHandler
post /register (RegisterReq) returns (RegisterResp)
@handler LoginHandler
post /login (LoginReq) returns (LoginResp)
@handler GetUserInfoHandler
get /info (GetUserInfoReq) returns (GetUserInfoResp)
}
同时创建 userapi.api 文件(内容相同或为空,根据 goctl 要求)。运行代码生成命令:
$ goctl api go -api apps/user/api/user.api -dir apps/user/api -style goZero
该命令会生成 handler、logic、svc、types 等目录及对应文件,并自动注册路由。
配置与依赖注入
编辑 apps/user/api/internal/config/config.go,加入 user-rpc 客户端配置:
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
}
对应的 etc/userapi.yaml:
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 192.168.1.10:2379
Key: user.rpc
在 svc/servicecontext.go 中创建 RPC 客户端代理:
package svc
import (
"im-system/apps/user/api/internal/config"
"im-system/apps/user/rpc/user"
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
UserRpc user.UserClient
}
func NewServiceContext(c config.Config) *ServiceContext {
client := user.NewUserClient(zrpc.MustNewClient(c.UserRpc).Conn())
return &ServiceContext{
Config: c,
UserRpc: client,
}
}
实现 API 业务逻辑
注册接口
在 internal/logic/registerlogic.go 中:
package logic
import (
"context"
"im-system/apps/user/api/internal/svc"
"im-system/apps/user/api/internal/types"
"im-system/apps/user/rpc/user"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
rpcResp, err := l.svcCtx.UserRpc.Register(l.ctx, &user.RegisterRequest{
Phone: req.Phone,
Username: req.Username,
Password: req.Password,
Avatar: req.Avatar,
Gender: req.Gender,
})
if err != nil {
return nil, err
}
return &types.RegisterResp{
Uid: rpcResp.Uid,
Token: rpcResp.Token,
}, nil
}
登录接口
类似地,调用 UserRpc.Login 即可。
用户详情接口
该接口需要从 JWT 令牌中获取当前用户 uid,而非通过请求体传递。我们在注册与登录时已经将 uid 写入 JWT 的载荷中,并在 user-api 的 JWT 中间件解析后存入 context。借助 go-zero 的 ctxdata 机制,可以从 context 中提取 uid。
首先在 svc 或公共包中定义获取方法:
package svc
import (
"context"
"github.com/zeromicro/go-zero/core/ctxdata"
)
func GetUidFromCtx(ctx context.Context) string {
data, ok := ctxdata.FromContext(ctx)
if !ok {
return ""
}
uid, _ := data["uid"].(string)
return uid
}
然后在 getuserinfologic.go 中:
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (resp *types.GetUserInfoResp, err error) {
uid := svc.GetUidFromCtx(l.ctx)
if uid == "" {
return nil, errors.New("未获取到用户标识")
}
rpcResp, err := l.svcCtx.UserRpc.GetUserInfo(l.ctx, &user.GetUserInfoRequest{Uid: uid})
if err != nil {
return nil, err
}
return &types.GetUserInfoResp{
Uid: rpcResp.Uid,
Username: rpcResp.Username,
Phone: rpcResp.Phone,
Avatar: rpcResp.Avatar,
Gender: rpcResp.Gender,
}, nil
}
统一响应格式设计
目前,API 的错误信息直接透传 gRPC 的错误字符串,不利于客户端标准化解析。我们约定所有响应包含三个字段:
code:状态码(0 表示成功,其他为错误码)msg:消息描述data:业务数据
自定义 Handler
在 go-zero 的 rest 服务中,可通过自定义 okHandler 和 errorHandler 来控制输出格式。新建 common/response/response.go:
package response
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/status"
"im-system/common/errx"
)
type Body struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func Success(w http.ResponseWriter, v interface{}) {
httpx.WriteJson(w, http.StatusOK, &Body{
Code: 0,
Msg: "success",
Data: v,
})
}
func Error(w http.ResponseWriter, serviceName string, err error) {
var (
code = errx.CodeServerError
msg = errx.GetMsg(code)
)
// 尝试获取业务自定义错误码
if codeErr := errx.FromError(err); codeErr != nil {
code = codeErr.Code
msg = codeErr.Msg
} else {
// 尝试解析 gRPC 错误
st, ok := status.FromError(err)
if ok {
code = int(st.Code())
msg = st.Message()
}
}
// 记录错误日志
logx.Errorf("[%s] api error: %v", serviceName, err)
httpx.WriteJson(w, http.StatusOK, &Body{
Code: code,
Msg: msg,
})
}
其中 errx.FromError 和 CodeError 为前文定义的错误体系(在 common/errx 中)。若没有可用 go-zero 的 errorx 包直接替换。
注册自定义 Handler
在 apps/user/api/user.go 的 main 函数中注册:
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
// 注册路由
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 设置自定义成功和错误处理器
httpx.SetOkHandler(func(w http.ResponseWriter, v interface{}) {
response.Success(w, v)
})
httpx.SetErrorHandler(func(err error) (int, interface{}) {
response.Error(w, "user-api", err)
return http.StatusOK, nil // 返回 nil 避免重复写入
})
fmt.Printf("Starting api server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
httpx.SetErrorHandler 签名为 func(err error) (int, interface{}),提供的返回值会被 go-zero 再次写入响应。为避免重复,我们在 Error 函数内部已经完成 WriteJson,所以这里返回 http.StatusOK, nil 即可。
请求流程示意
测试验证
使用 Postman 或类似工具进行测试。
-
注册:
POST /v1/user/register,Body:{"phone":"13800138000","username":"test","password":"123456"}。返回code=0,data包含uid和token。 -
登录:正常密码返回 token;错误密码返回
code为业务错误码(如100006),msg为“密码错误”。 -
用户详情:在请求头添加
Authorization: Bearer <token>,调用GET /v1/user/info,返回当前用户信息。
如果未登录或 token 过期,JWT 中间件会拦截并返回 gRPC 状态码对应的错误信息,由统一错误处理器格式化为标准 JSON。
小结
本篇文章完成了从 user-rpc 到 user-api 的完整对接,要点总结:
- 使用
goctl api go快速生成 API 服务骨架。 - 通过
ServiceContext注入 RPC 客户端,实现 HTTP 到 gRPC 的透明转发。 - 利用 JWT 中间件从 token 中提取
uid,实现无状态的身份识别。 - 自定义响应处理器,统一输出格式,并智能区分业务错误、gRPC 错误和未知错误。
- 整个流程遵循
go-zero推荐的开发范式,易于扩展到其他微服务。
至此,用户服务已具备完备的对外 API,为后续的社交与 IM 模块提供了可靠的基础。在接下来的文章中,我们将继续按照同样的模式构建社交服务和 IM 服务,并引入 BFF 层进行业务聚合。

2万+

被折叠的 条评论
为什么被折叠?



