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 中实现:

go 复制代码
package logic 
 
import (
	"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"
)
 
type GetUserInfoLogic struct {
	ctx    context.Context 
	svcCtx *svc.ServiceContext 
	logx.Logger 
}
 
func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic {
	return &GetUserInfoLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}
 
var ErrUserNotFound = errors.New("用户不存在")
 
func (l *GetUserInfoLogic) GetUserInfo(in *user.GetUserInfoRequest) (*user.GetUserInfoResponse, error) {
	u, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Uid)
	if err != nil {
		if errors.Is(err, sqlx.ErrNotFound) {
			return nil, ErrUserNotFound 
		}
		return nil, 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 中新增方法:

go 复制代码
package model 
 
import (
	"context"
	"fmt"
	"strings"
	"github.com/zeromicro/go-zero/core/stores/sqlx"
)
 
// FindByName 按用户名模糊查询 
func (m *defaultUserModel) FindByName(ctx context.Context, name string) ([]*User, error) {
	query := fmt.Sprintf("select %s from %s where username like ? limit 20", userRows, m.table)
	var resp []*User 
	err := m.conn.QueryRowsCtx(ctx, &resp, query, "%"+name+"%")
	return resp, err 
}
 
// FindByIds 按ID集合批量查询 
func (m *defaultUserModel) FindByIds(ctx context.Context, ids []string) ([]*User, error) {
	if len(ids) == 0 {
		return nil, 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))
	for i, id := range ids {
		args[i] = id 
	}
	var resp []*User 
	err := m.conn.QueryRowsCtx(ctx, &resp, query, args...)
	return resp, err 
}

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

业务逻辑实现

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

go 复制代码
package logic 
 
import (
	"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"
)
 
type FindUserLogic struct {
	ctx    context.Context 
	svcCtx *svc.ServiceContext 
	logx.Logger 
}
 
func NewFindUserLogic(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) {
	var users []*model.User 
	var err error 
 
	switch {
	case in.Phone != "":
		// 手机号精确查询 
		u, e := l.svcCtx.UserModel.FindOneByPhone(l.ctx, in.Phone)
		if e == nil {
			users = append(users, u)
		}
		err = e 
	case in.Username != "":
		// 用户名模糊查询 
		users, err = l.svcCtx.UserModel.FindByName(l.ctx, in.Username)
	case len(in.Ids) > 0:
		// ID集合查询 
		users, err = l.svcCtx.UserModel.FindByIds(l.ctx, in.Ids)
	default:
		return nil, errors.New("搜索条件不能为空")
	}
 
	if err != nil {
		return nil, err 
	}
 
	// 转换为响应结构体 
	resp := &user.FindUserResponse{}
	for _, u := range users {
		resp.Users = append(resp.Users, &user.GetUserInfoResponse{
			Uid:      u.Uid,
			Username: u.Username,
			Phone:    u.Phone,
			Avatar:   u.Avatar,
			Gender:   int64(u.Gender),
		})
	}
	return resp, nil 
}

统一错误处理设计

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

错误码定义

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

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

自定义错误对象

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

go 复制代码
package errx 
 
import (
	"github.com/zeromicro/go-zero/core/errorx"
)
 
// NewCodeError 创建带错误码的错误 
func NewCodeError(code int) error {
	return errorx.NewCodeError(code, GetMsg(code))
}
 
// NewCodeErrorf 创建带错误码和自定义描述的错误(描述用于日志,错误码对应信息返回给客户端)
func NewCodeErrorf(code int, format string, args ...interface{}) error {
	return errorx.NewCodeErrorf(code, GetMsg(code), format, args...)
}
 
// 快捷错误方法 
func DBError() error {
	return NewCodeError(CodeDBError)
}
 
func UserNotFound() error {
	return NewCodeError(CodeUserNotFound)
}
 
func UserExists() error {
	return NewCodeError(CodeUserExists)
}
 
func PasswordWrong() error {
	return NewCodeError(CodePasswordWrong)
}
 
func TokenGenerateErr() error {
	return NewCodeError(CodeTokenGenerateErr)
}

业务逻辑中使用统一错误

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

go 复制代码
func (l *LoginLogic) Login(in *user.LoginRequest) (*user.LoginResponse, error) {
	u, err := l.svcCtx.UserModel.FindOneByPhone(l.ctx, in.Phone)
	if err != nil {
		if errors.Is(err, sqlx.ErrNotFound) {
			return nil, errx.UserNotFound() // 使用统一错误 
		}
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "根据手机号查询用户失败: %v", err)
	}
 
	if !crypt.CheckPassword(u.Password, in.Password) {
		return nil, 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)
	if err != nil {
		return nil, 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 中:

go 复制代码
package interceptor 
 
import (
	"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 服务端统一错误拦截器 
func RpcErrInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	resp, err := handler(ctx, req)
	if err == nil {
		return resp, nil 
	}
 
	// 记录日志 
	logx.WithContext(ctx).Errorf("rpc err: %v", err)
 
	// 尝试提取自定义错误码 
	if codeErr := errorx.CodeErrorFrom(err); codeErr != nil {
		// 构造 gRPC 状态,将错误码和消息返回给客户端 
		return nil, status.Errorf(codes.Code(codeErr.Code), codeErr.Error())
	}
 
	// 非自定义错误,统一返回内部错误 
	return nil, status.Errorf(codes.Internal, errx.GetMsg(errx.CodeServerError))
}

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

go 复制代码
package main 
 
import (
	"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"
)
 
var configFile = flag.String("f", "etc/user.yaml", "the config file")
 
func main() {
	flag.Parse()
 
	var c 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))
 
	defer s.Stop()
 
	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}

这一段是否有问题啊?

错误处理流程

Model Logic Interceptor Client Model Logic Interceptor Client #mermaid-svg-h8TJezq9qNmzgFl6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h8TJezq9qNmzgFl6 .error-icon{fill:#552222;}#mermaid-svg-h8TJezq9qNmzgFl6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h8TJezq9qNmzgFl6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h8TJezq9qNmzgFl6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h8TJezq9qNmzgFl6 .marker.cross{stroke:#333333;}#mermaid-svg-h8TJezq9qNmzgFl6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h8TJezq9qNmzgFl6 p{margin:0;}#mermaid-svg-h8TJezq9qNmzgFl6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h8TJezq9qNmzgFl6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h8TJezq9qNmzgFl6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-h8TJezq9qNmzgFl6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-h8TJezq9qNmzgFl6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-h8TJezq9qNmzgFl6 .sequenceNumber{fill:white;}#mermaid-svg-h8TJezq9qNmzgFl6 #sequencenumber{fill:#333;}#mermaid-svg-h8TJezq9qNmzgFl6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-h8TJezq9qNmzgFl6 .messageText{fill:#333;stroke:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h8TJezq9qNmzgFl6 .labelText,#mermaid-svg-h8TJezq9qNmzgFl6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .loopText,#mermaid-svg-h8TJezq9qNmzgFl6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h8TJezq9qNmzgFl6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-h8TJezq9qNmzgFl6 .noteText,#mermaid-svg-h8TJezq9qNmzgFl6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-h8TJezq9qNmzgFl6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h8TJezq9qNmzgFl6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h8TJezq9qNmzgFl6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h8TJezq9qNmzgFl6 .actorPopupMenu{position:absolute;}#mermaid-svg-h8TJezq9qNmzgFl6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-h8TJezq9qNmzgFl6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h8TJezq9qNmzgFl6 .actor-man circle,#mermaid-svg-h8TJezq9qNmzgFl6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-h8TJezq9qNmzgFl6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} gRPC RequestDB QueryError (sqlx.ErrNotFound)Wrap with errx.UserNotFound()Return CodeErrorLog error detailsgRPC status with Code and Message

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

测试验证

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

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

总结

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

相关推荐
我星期八休息17 小时前
网络编程—应用层HTTP协议
linux·运维·开发语言·前端·网络·网络协议·http
梅雅达编程笔记17 小时前
零基础学 Python 第14章 | 模块、包与第三方库
开发语言·python·django·numpy·pandas
Mr__Miss17 小时前
Java泛型完全指南:从入门到精通
java·开发语言·python
赵庆明老师17 小时前
Vben精讲:14-Vben远程加载语言包
开发语言·vben
hehelm18 小时前
AI大模型接入SDK—通用模块设计
linux·开发语言·c++
梅雅达编程笔记19 小时前
零基础学 Python 第15章 | 类与对象:面向对象编程入门
开发语言·python·django·numpy·pandas
行思理20 小时前
微信支付“商家转账用户确认模式”,新手教程
java·开发语言·微信
麻瓜老宋20 小时前
AI开发C语言应用按步走,表达式计算器calc的第一步,中缀表达式词法分析
c语言·开发语言·atomcode
灯澜忆梦21 小时前
GO_文件处理---字符串操作
开发语言·golang