Go-Zero项目开发6: 构建用户API服务与统一响应处理

纲要

  • 项目回顾:user-rpc 已完成,亟需暴露 HTTP 接口
  • 搭建 user-api 服务
    • 创建 user.apiuserapi.api 文件
    • 使用 goctl api go 生成代码
    • 配置路由、中间件与 RPC 客户端
  • 实现 API 业务逻辑
    • 注册接口:调用 user-rpc.Register
    • 登录接口:调用 user-rpc.Login
    • 用户详情接口:从 JWT 中提取 uid,调用 user-rpc.GetUserInfo
  • 统一响应输出设计
    • 标准响应结构体(状态码、消息、数据)
    • 自定义 okHandlererrorHandler
    • 区分业务错误、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 

该命令会生成 handlerlogicsvctypes 等目录及对应文件,并自动注册路由。

配置与依赖注入

编辑 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-zeroctxdata 机制,可以从 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-zerorest 服务中,可通过自定义 okHandlererrorHandler 来控制输出格式。新建 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.FromErrorCodeError 为前文定义的错误体系(在 common/errx 中)。若没有可用 go-zeroerrorx 包直接替换。

注册自定义 Handler

apps/user/api/user.gomain 函数中注册:

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 即可。

请求流程示意

数据库user-rpc (gRPC)JWT中间件user-api (HTTP)客户端数据库user-rpc (gRPC)JWT中间件user-api (HTTP)客户端POST /v1/user/loginLogin(phone, password)查询用户用户信息Token + Uid{"code":0, "msg":"success", "data":{"uid":"...","token":"..."}}GET /v1/user/info (Authorization: Bearer <token>)解析 Token,提取 uid 存入 context从 context 获取 uidGetUserInfo(uid)FindOne(uid)用户详情用户信息{"code":0, "data":{...}}

测试验证

使用 Postman 或类似工具进行测试。

  • 注册POST /v1/user/register,Body: {"phone":"13800138000","username":"test","password":"123456"}。返回 code=0data 包含 uidtoken

  • 登录:正常密码返回 token;错误密码返回 code 为业务错误码(如 100006),msg 为“密码错误”。

  • 用户详情:在请求头添加 Authorization: Bearer <token>,调用 GET /v1/user/info,返回当前用户信息。

如果未登录或 token 过期,JWT 中间件会拦截并返回 gRPC 状态码对应的错误信息,由统一错误处理器格式化为标准 JSON。

小结

本篇文章完成了从 user-rpcuser-api 的完整对接,要点总结:

  • 使用 goctl api go 快速生成 API 服务骨架。
  • 通过 ServiceContext 注入 RPC 客户端,实现 HTTP 到 gRPC 的透明转发。
  • 利用 JWT 中间件从 token 中提取 uid,实现无状态的身份识别。
  • 自定义响应处理器,统一输出格式,并智能区分业务错误、gRPC 错误和未知错误。
  • 整个流程遵循 go-zero 推荐的开发范式,易于扩展到其他微服务。

至此,用户服务已具备完备的对外 API,为后续的社交与 IM 模块提供了可靠的基础。在接下来的文章中,我们将继续按照同样的模式构建社交服务和 IM 服务,并引入 BFF 层进行业务聚合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值