Files
juwan-backend/common/converter
wwweww fdbcde13b2 add:
2026-02-23 20:36:21 +08:00
..
2026-02-23 20:36:21 +08:00
2026-02-23 20:36:21 +08:00
2026-02-23 20:36:21 +08:00

Converter - 通用结构体转换工具

利用 Go 反射机制,实现自动的 model 到 protobuf 结构体转换。

功能特性

自动字段映射 - 自动匹配同名字段并赋值
智能类型转换 - 自动处理常见类型转换
通用设计 - 支持任何 model 和 pb 结构体,无需手动编写
灵活扩展 - 支持自定义类型转换规则

支持的类型转换

源类型 目标类型 说明
time.Time int64 转换为 Unix 时间戳
sql.NullTime int64 有效时自动转换,无效则为 0
sql.NullTime time.Time 有效时自动转换,无效则为零值
sql.NullInt64 int64 有效时自动转换,无效则为 0
sql.NullString string 有效时自动转换,无效则为空字符串
sql.NullBool bool 有效时自动转换,无效则为 false
int int64 自动转换
int64 int 自动转换
相同类型 相同类型 直接复制

核心函数

1. StructToStruct - 单个结构体转换

func StructToStruct(src, dst interface{}) error

参数:

  • src - 源结构体(可以是指针或值类型)
  • dst - 目标结构体(必须是指针)

示例:

import "app/common/converter"

// 单个 model 转 pb
user, _ := m.FindOne(ctx, userId)
pbUser := &pb.Users{}
converter.StructToStruct(user, pbUser)

// 或直接点对点转换
pbUser := &pb.Users{}
_ = converter.StructToStruct(user, pbUser)

2. SliceToSlice - 切片转换

func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error)

参数:

  • src - 源切片
  • dstSliceType - 目标切片类型(用于推导元素类型)

示例:

// 多个 model 转 pb
users := []*models.Users{user1, user2, user3}
pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{})
pbUsers := pbUsersIface.([]*pb.Users)

3. UserModelToPb - Users 专用转换(推荐)

func UserModelToPb(user *models.Users) *pb.Users

简化的 Users model 转 pb 的快捷函数。

示例:

user, _ := m.FindOne(ctx, userId)
pbUser := converter.UserModelToPb(user)

4. UserModelsToPb - Users 批量转换(推荐)

func UserModelsToPb(users []*models.Users) []*pb.Users

简化的批量转换快捷函数。

示例:

users, _ := m.FindAll(ctx)
pbUsers := converter.UserModelsToPb(users)

使用场景

场景 1:在 Logic 层直接转换

package logic

import (
	"context"
	"app/common/converter"
	"app/users/rpc/internal/models"
	"app/users/rpc/pb"
)

type GetUserByIdLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func (l *GetUserByIdLogic) GetUserById(req *pb.GetUserByIdReq) (*pb.Users, error) {
	// 查询数据库
	user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.UserId)
	if err != nil {
		return nil, err
	}

	// 直接转换,无需手动赋值每个字段
	pbUser := converter.UserModelToPb(user)
	
	return pbUser, nil
}

场景 2:批量操作

func (l *ListUsersLogic) ListUsers(req *pb.ListUsersReq) (*pb.ListUsersResp, error) {
	users, err := l.svcCtx.UsersModel.FindAll(l.ctx)
	if err != nil {
		return nil, err
	}

	// 批量转换
	pbUsers := converter.UserModelsToPb(users)
	
	return &pb.ListUsersResp{
		Users: pbUsers,
	}, nil
}

场景 3:搜索/过滤结果

func (l *SearchUsersLogic) SearchUsers(req *pb.SearchReq) (*pb.SearchResp, error) {
	// 搜索数据库
	results, err := l.svcCtx.UsersModel.SearchByKeyword(l.ctx, req.Keyword)
	if err != nil {
		return nil, err
	}

	pbUsers := converter.UserModelsToPb(results)
	
	return &pb.SearchResp{
		Results: pbUsers,
	}, nil
}

处理特殊字段

NULLable 字段

当源字段是 sql.NullTime 或其他 sql.Null* 类型时,转换器会自动处理:

// sql.NullTime -> int64(有效情况)
user.DeletedAt = sql.NullTime{
	Time: time.Now(),
	Valid: true,
}
// 转换后 pb.Users.DeletedAt 会包含 Unix 时间戳

// sql.NullTime -> int64(无效情况)
user.DeletedAt = sql.NullTime{
	Valid: false,
}
// 转换后 pb.Users.DeletedAt 为 0

时间戳字段

数据库中的 time.Time 字段会自动转换为 protobuf 中的 int64 Unix 时间戳:

// Model
type Users struct {
	CreatedAt  time.Time    `db:"created_at"`
	UpdatedAt  time.Time    `db:"updated_at"`
	DeletedAt  sql.NullTime `db:"deleted_at"`
}

// Protobuf
type Users struct {
	CreatedAt  int64  // 自动转换为 Unix 时间戳
	UpdatedAt  int64  // 自动转换为 Unix 时间戳
	DeletedAt  int64  // 有效时转换,无效时为 0
}

扩展 - 添加自定义类型转换

如果需要支持新的类型转换,可以在 generic.goassignValue 函数中添加:

// 处理自定义类型 MyType -> int32 的转换
if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 {
	mt := srcField.Interface().(MyType)
	dstField.SetInt(int64(mt.SomeIntField))
	return nil
}

性能考虑

  • 反射操作相对于直接赋值会有性能开销(通常很小)
  • 如果需要转换大量数据(>10000 条),考虑性能测试
  • 对于热点代码路径,可以写针对性的转换函数

错误处理

err := converter.StructToStruct(src, dst)
if err != nil {
	log.Printf("转换失败: %v", err)
	// 处理错误
}

大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。

常见问题

Q: 字段名必须完全相同吗?
A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 UserIdpb 字段也必须是 UserId

Q: 如果某个字段转换失败怎么办?
A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。

Q: 能否自定义字段映射规则(比如 db_name -> pbName)?
A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。

Q: 转换速度快吗?
A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。

相关文件

  • generic.go - 通用转换函数核心实现
  • user_converter.go - Users model 专用转换函数(示例)