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

发布时间:2026/7/18 21:52:33
Go-Zero项目开发4: 用户服务搜索、详情与统一错误处理 纲要用户详情查询基于GetUserInfo接口实现使用模型FindOne按主键查询用户搜索功能按手机号精确查询复用FindOneByPhone按用户名模糊查询新增FindByName按 ID 集合批量查询新增FindByIds统一错误处理体系自定义错误码与描述映射基于go-zero的errx包创建标准错误对象RPC 拦截器实现错误日志记录与客户端友好响应在业务逻辑中替换原始错误应用统一错误码用户详情查询在user.proto中已定义GetUserInfo方法其请求参数为uid响应为用户模型。业务逻辑非常简单根据uid调用模型层的FindOne方法若用户不存在则返回“用户不存在”错误。在apps/user/rpc/internal/logic/getuserinfologic.go中实现packagelogicimport(contexterrorsim-system/apps/user/rpc/internal/svcim-system/apps/user/rpc/userim-system/common/modelgithub.com/zeromicro/go-zero/core/stores/sqlxgithub.com/zeromicro/go-zero/core/logx)typeGetUserInfoLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewGetUserInfoLogic(ctx context.Context,svcCtx*svc.ServiceContext)*GetUserInfoLogic{returnGetUserInfoLogic{ctx:ctx,svcCtx:svcCtx,Logger:logx.WithContext(ctx),}}varErrUserNotFounderrors.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}returnuser.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(contextfmtstringsgithub.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))placeholdersplaceholders[: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(contextim-system/apps/user/rpc/internal/svcim-system/apps/user/rpc/userim-system/common/modelgithub.com/zeromicro/go-zero/core/logx)typeFindUserLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewFindUserLogic(ctx context.Context,svcCtx*svc.ServiceContext)*FindUserLogic{returnFindUserLogic{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)ifenil{usersappend(users,u)}errecasein.Username!:// 用户名模糊查询users,errl.svcCtx.UserModel.FindByName(l.ctx,in.Username)caselen(in.Ids)0:// ID集合查询users,errl.svcCtx.UserModel.FindByIds(l.ctx,in.Ids)default:returnnil,errors.New(搜索条件不能为空)}iferr!nil{returnnil,err}// 转换为响应结构体resp:user.FindUserResponse{}for_,u:rangeusers{resp.Usersappend(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(// 通用错误码前三位为业务标识后三位为具体错误CodeServerError100001CodeParamInvalid100002CodeDBError100003CodeUserNotFound100004CodeUserExists100005CodePasswordWrong100006CodeTokenGenerateErr100007)varcodeMsgmap[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)}returnuser.LoginResponse{Uid:u.Uid,Token:tokenStr,},nil}RPC 拦截器统一处理为将错误码信息写入 gRPC 响应的status中同时将详细错误记录到日志我们编写一个 gRPC 拦截器。在common/interceptor/rpc_err_interceptor.go中packageinterceptorimport(contextim-system/common/errxgithub.com/zeromicro/go-zero/core/errorxgithub.com/zeromicro/go-zero/core/logxgoogle.golang.org/grpcgoogle.golang.org/grpc/codesgoogle.golang.org/grpc/status)// RpcErrInterceptor 服务端统一错误拦截器funcRpcErrInterceptor(ctx context.Context,reqinterface{},info*grpc.UnaryServerInfo,handler grpc.UnaryHandler)(interface{},error){resp,err:handler(ctx,req)iferrnil{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(flagfmtim-system/apps/user/rpc/internal/configim-system/apps/user/rpc/internal/serverim-system/apps/user/rpc/internal/svcim-system/apps/user/rpc/userim-system/common/interceptorgithub.com/zeromicro/go-zero/core/confgithub.com/zeromicro/go-zero/zrpcgoogle.golang.org/grpc)varconfigFileflag.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 服务等其他微服务中确保整个系统的可维护性与用户体验一致性。