news 2026/7/18 21:52:28

Go-Zero项目开发4: 用户服务搜索、详情与统一错误处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go-Zero项目开发4: 用户服务搜索、详情与统一错误处理

纲要

  • 用户详情查询
    • 基于GetUserInfo接口实现
    • 使用模型FindOne按主键查询
  • 用户搜索功能
    • 按手机号精确查询(复用FindOneByPhone
    • 按用户名模糊查询(新增FindByName
    • 按 ID 集合批量查询(新增FindByIds
  • 统一错误处理体系
    • 自定义错误码与描述映射
    • 基于go-zeroerrx包创建标准错误对象
    • RPC 拦截器实现错误日志记录与客户端友好响应
    • 在业务逻辑中替换原始错误,应用统一错误码

用户详情查询

user.proto中已定义GetUserInfo方法,其请求参数为uid,响应为用户模型。业务逻辑非常简单:根据uid调用模型层的FindOne方法,若用户不存在则返回“用户不存在”错误。

apps/user/rpc/internal/logic/getuserinfologic.go中实现:

packagelogicimport("context""errors""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/model""github.com/zeromicro/go-zero/core/stores/sqlx""github.com/zeromicro/go-zero/core/logx")typeGetUserInfoLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewGetUserInfoLogic(ctx context.Context,svcCtx*svc.ServiceContext)*GetUserInfoLogic{return&GetUserInfoLogic{ctx:ctx,svcCtx:svcCtx,Logger:logx.WithContext(ctx),}}varErrUserNotFound=errors.New("用户不存在")func(l*GetUserInfoLogic)GetUserInfo(in*user.GetUserInfoRequest)(*user.GetUserInfoResponse,error){u,err:=l.svcCtx.UserModel.FindOne(l.ctx,in.Uid)iferr!=nil{iferrors.Is(err,sqlx.ErrNotFound){returnnil,ErrUserNotFound}returnnil,err}return&user.GetUserInfoResponse{Uid:u.Uid,Username:u.Username,Phone:u.Phone,Avatar:u.Avatar,Gender:int64(u.Gender),},nil}

用户搜索功能

搜索接口FindUser支持三种方式:手机号精确查询、用户名模糊查询、ID 集合批量查询。模型层需要提供对应的方法。

模型扩展

common/model/usermodel_ext.go中新增方法:

packagemodelimport("context""fmt""strings""github.com/zeromicro/go-zero/core/stores/sqlx")// FindByName 按用户名模糊查询func(m*defaultUserModel)FindByName(ctx context.Context,namestring)([]*User,error){query:=fmt.Sprintf("select %s from %s where username like ? limit 20",userRows,m.table)varresp[]*User err:=m.conn.QueryRowsCtx(ctx,&resp,query,"%"+name+"%")returnresp,err}// FindByIds 按ID集合批量查询func(m*defaultUserModel)FindByIds(ctx context.Context,ids[]string)([]*User,error){iflen(ids)==0{returnnil,nil}placeholders:=strings.Repeat("?,",len(ids))placeholders=placeholders[:len(placeholders)-1]query:=fmt.Sprintf("select %s from %s where uid in (%s)",userRows,m.table,placeholders)args:=make([]interface{},len(ids))fori,id:=rangeids{args[i]=id}varresp[]*User err:=m.conn.QueryRowsCtx(ctx,&resp,query,args...)returnresp,err}

注意:userRows为生成代码中定义的字段列表常量,直接使用即可。

业务逻辑实现

finduserlogic.go中,根据请求参数的不同,路由到对应的查询方法。

packagelogicimport("context""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/model""github.com/zeromicro/go-zero/core/logx")typeFindUserLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewFindUserLogic(ctx context.Context,svcCtx*svc.ServiceContext)*FindUserLogic{return&FindUserLogic{ctx:ctx,svcCtx:svcCtx,Logger:logx.WithContext(ctx),}}func(l*FindUserLogic)FindUser(in*user.FindUserRequest)(*user.FindUserResponse,error){varusers[]*model.Uservarerrerrorswitch{casein.Phone!="":// 手机号精确查询u,e:=l.svcCtx.UserModel.FindOneByPhone(l.ctx,in.Phone)ife==nil{users=append(users,u)}err=ecasein.Username!="":// 用户名模糊查询users,err=l.svcCtx.UserModel.FindByName(l.ctx,in.Username)caselen(in.Ids)>0:// ID集合查询users,err=l.svcCtx.UserModel.FindByIds(l.ctx,in.Ids)default:returnnil,errors.New("搜索条件不能为空")}iferr!=nil{returnnil,err}// 转换为响应结构体resp:=&user.FindUserResponse{}for_,u:=rangeusers{resp.Users=append(resp.Users,&user.GetUserInfoResponse{Uid:u.Uid,Username:u.Username,Phone:u.Phone,Avatar:u.Avatar,Gender:int64(u.Gender),})}returnresp,nil}

统一错误处理设计

随着服务增多,业务错误若直接返回原始error,既不利于日志追踪,也不便于客户端统一解析。我们需要构建一套标准错误体系:包含错误码、描述、日志记录,并通过 RPC 拦截器自动处理。

错误码定义

common/errx包中定义错误码与描述映射。

packageerrxconst(// 通用错误码,前三位为业务标识,后三位为具体错误CodeServerError=100001CodeParamInvalid=100002CodeDBError=100003CodeUserNotFound=100004CodeUserExists=100005CodePasswordWrong=100006CodeTokenGenerateErr=100007)varcodeMsg=map[int]string{CodeServerError:"服务器内部错误",CodeParamInvalid:"请求参数有误",CodeDBError:"数据库繁忙,请稍后再试",CodeUserNotFound:"用户不存在",CodeUserExists:"手机号已注册",CodePasswordWrong:"密码错误",CodeTokenGenerateErr:"令牌生成失败",}funcGetMsg(codeint)string{ifmsg,ok:=codeMsg[code];ok{returnmsg}returncodeMsg[CodeServerError]}

自定义错误对象

利用go-zero提供的github.com/zeromicro/go-zero/core/errorx包(或errx),创建带有错误码的错误类型。在common/errx中继续封装。

packageerrximport("github.com/zeromicro/go-zero/core/errorx")// NewCodeError 创建带错误码的错误funcNewCodeError(codeint)error{returnerrorx.NewCodeError(code,GetMsg(code))}// NewCodeErrorf 创建带错误码和自定义描述的错误(描述用于日志,错误码对应信息返回给客户端)funcNewCodeErrorf(codeint,formatstring,args...interface{})error{returnerrorx.NewCodeErrorf(code,GetMsg(code),format,args...)}// 快捷错误方法funcDBError()error{returnNewCodeError(CodeDBError)}funcUserNotFound()error{returnNewCodeError(CodeUserNotFound)}funcUserExists()error{returnNewCodeError(CodeUserExists)}funcPasswordWrong()error{returnNewCodeError(CodePasswordWrong)}funcTokenGenerateErr()error{returnNewCodeError(CodeTokenGenerateErr)}

业务逻辑中使用统一错误

以登录逻辑为例,将所有直接返回errors.New的地方替换为errx中的错误。

func(l*LoginLogic)Login(in*user.LoginRequest)(*user.LoginResponse,error){u,err:=l.svcCtx.UserModel.FindOneByPhone(l.ctx,in.Phone)iferr!=nil{iferrors.Is(err,sqlx.ErrNotFound){returnnil,errx.UserNotFound()// 使用统一错误}returnnil,errx.NewCodeErrorf(errx.CodeDBError,"根据手机号查询用户失败: %v",err)}if!crypt.CheckPassword(u.Password,in.Password){returnnil,errx.PasswordWrong()}expire:=time.Duration(l.svcCtx.Config.JWT.ExpireInSeconds)*time.Second tokenStr,err:=jwt.GenerateToken(u.Uid,[]byte(l.svcCtx.Config.JWT.Secret),expire)iferr!=nil{returnnil,errx.NewCodeErrorf(errx.CodeTokenGenerateErr,"生成token失败: %v",err)}return&user.LoginResponse{Uid:u.Uid,Token:tokenStr,},nil}

RPC 拦截器统一处理

为将错误码信息写入 gRPC 响应的status中,同时将详细错误记录到日志,我们编写一个 gRPC 拦截器。

common/interceptor/rpc_err_interceptor.go中:

packageinterceptorimport("context""im-system/common/errx""github.com/zeromicro/go-zero/core/errorx""github.com/zeromicro/go-zero/core/logx""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/status")// RpcErrInterceptor 服务端统一错误拦截器funcRpcErrInterceptor(ctx context.Context,reqinterface{},info*grpc.UnaryServerInfo,handler grpc.UnaryHandler)(interface{},error){resp,err:=handler(ctx,req)iferr==nil{returnresp,nil}// 记录日志logx.WithContext(ctx).Errorf("rpc err: %v",err)// 尝试提取自定义错误码ifcodeErr:=errorx.CodeErrorFrom(err);codeErr!=nil{// 构造 gRPC 状态,将错误码和消息返回给客户端returnnil,status.Errorf(codes.Code(codeErr.Code),codeErr.Error())}// 非自定义错误,统一返回内部错误returnnil,status.Errorf(codes.Internal,errx.GetMsg(errx.CodeServerError))}

然后在服务启动时注册拦截器,在 apps/user/rpc/user.go 或对应的 main 函数中:

packagemainimport("flag""fmt""im-system/apps/user/rpc/internal/config""im-system/apps/user/rpc/internal/server""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/interceptor""github.com/zeromicro/go-zero/core/conf""github.com/zeromicro/go-zero/zrpc""google.golang.org/grpc")varconfigFile=flag.String("f","etc/user.yaml","the config file")funcmain(){flag.Parse()varc config.Config conf.MustLoad(*configFile,&c)s:=zrpc.MustNewServer(c.RpcServerConf,func(grpcServer*grpc.Server){user.RegisterUserServer(grpcServer,server.NewUserServer(svc.NewServiceContext(c)))},zrpc.WithUnaryServerInterceptor(interceptor.RpcErrInterceptor))defers.Stop()fmt.Printf("Starting rpc server at %s...\n",c.ListenOn)s.Start()}

这一段是否有问题啊?

错误处理流程

ModelLogicInterceptorClientModelLogicInterceptorClientgRPC RequestDB QueryError (sqlx.ErrNotFound)Wrap with errx.UserNotFound()Return CodeErrorLog error detailsgRPC status with Code and Message

拦截器捕获所有从handler返回的错误,记录完整错误堆栈到日志,同时向客户端返回仅包含错误码和友好描述的 gRPC 状态,既方便运维排查,又避免敏感信息泄露。

测试验证

完成上述编码后,启动用户服务,使用grpcurl或单元测试验证:

  • 查询存在的用户,返回正确信息。
  • 查询不存在的用户,返回 gRPC 状态码为NotFound或自定义错误码,消息为“用户不存在”。
  • 搜索功能:分别测试手机号、用户名、ID 集合三种情况,确认结果正确。
  • 登录错误:输入错误密码,返回“密码错误”。

总结

至此,用户服务的搜索、详情查询及统一错误处理体系已构建完成。这套错误处理模式可快速复制到社交服务、IM 服务等其他微服务中,确保整个系统的可维护性与用户体验一致性。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 21:52:16

企业数字化技术解析与行业应用指南

行业整体现状:从搜索到推荐的流量迁徙当前,企业数字化营销正经历一轮深层次的结构性迁移。根据QuestMobile发布的《2024年人工智能应用生态报告》,用户在生成式AI大模型中的高频问答场景已覆盖购物决策、本地生活服务、工业采购等多个领域。传…

作者头像 李华
网站建设 2026/7/18 21:50:13

金融客户问私有化IM定制,我给了这4个方案

金融客户问私有化IM定制,我给了这4个方案 金融行业对数据安全和合规的要求极高,私有化IM系统定制开发成为刚需。本文基于实际项目经验,给出4个经过验证的方案,帮你快速匹配客户需求。 问题定义:金融客户为何需要私有化…

作者头像 李华
网站建设 2026/7/18 21:48:45

场外个股期权90结构和9090结构区别解析:参与比例如何影响收益

文章来源:期小衍前面一篇文章,我们讲了90结构为什么看起来权利金更高,也解释了名义权利金和净期权费的区别。今天继续讲一个更容易被混淆的问题:90结构和9090结构到底有什么区别?很多人在看场外个股期权报价时&#xf…

作者头像 李华
网站建设 2026/7/18 21:43:38

知识图谱与向量搜索的融合:提升检索准确率的架构设计

知识图谱与向量搜索的融合:提升检索准确率的架构设计 一、向量搜索的盲区 向量搜索(Vector Search)是 RAG 系统的主流检索方式——将文档向量化后,用相似度匹配找到与用户问题最接近的内容。向量搜索在「语义相似」上很擅长——用户问「怎么退钱」,能检索到「退款流…

作者头像 李华
网站建设 2026/7/18 21:43:21

m4s-converter:B站缓存视频无损转换终极指南,永久保存珍贵内容

m4s-converter:B站缓存视频无损转换终极指南,永久保存珍贵内容 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾因…

作者头像 李华
网站建设 2026/7/18 21:40:06

实验室的“净产出”公式:有效实验=总实验-采购损耗

实验室的产出,不取决于你做了多少实验,而取决于你做了多少“有效的实验”。总实验减去失败、重做、无效数据,才是你真正的“净产出”。而采购,恰恰是影响“净产出”最大的变量之一。采购损耗有多种表现形式: 因为试剂质…

作者头像 李华