# 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 - 单个结构体转换 ```go func StructToStruct(src, dst interface{}) error ``` **参数:** - `src` - 源结构体(可以是指针或值类型) - `dst` - 目标结构体(必须是指针) **示例:** ```go import "app/common/converter" // 单个 models 转 pb user, _ := m.FindOne(ctx, userId) pbUser := &pb.Users{} converter.StructToStruct(user, pbUser) // 或直接点对点转换 pbUser := &pb.Users{} _ = converter.StructToStruct(user, pbUser) ``` ### 2. SliceToSlice - 切片转换 ```go func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error) ``` **参数:** - `src` - 源切片 - `dstSliceType` - 目标切片类型(用于推导元素类型) **示例:** ```go // 多个 models 转 pb users := []*models.Users{user1, user2, user3} pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{}) pbUsers := pbUsersIface.([]*pb.Users) ``` ### 3. UserModelToPb - Users 专用转换(推荐) ```go func UserModelToPb(user *models.Users) *pb.Users ``` 简化的 Users model 转 pb 的快捷函数。 **示例:** ```go user, _ := m.FindOne(ctx, userId) pbUser := converter.UserModelToPb(user) ``` ### 4. UserModelsToPb - Users 批量转换(推荐) ```go func UserModelsToPb(users []*models.Users) []*pb.Users ``` 简化的批量转换快捷函数。 **示例:** ```go users, _ := m.FindAll(ctx) pbUsers := converter.UserModelsToPb(users) ``` ## 使用场景 ### 场景 1:在 Logic 层直接转换 ```go 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:批量操作 ```go 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:搜索/过滤结果 ```go 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*` 类型时,转换器会自动处理: ```go // 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 时间戳: ```go // 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.go` 的 `assignValue` 函数中添加: ```go // 处理自定义类型 MyType -> int32 的转换 if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 { mt := srcField.Interface().(MyType) dstField.SetInt(int64(mt.SomeIntField)) return nil } ``` ## 性能考虑 - 反射操作相对于直接赋值会有性能开销(通常很小) - 如果需要转换大量数据(>10000 条),考虑性能测试 - 对于热点代码路径,可以写针对性的转换函数 ## 错误处理 ```go err := converter.StructToStruct(src, dst) if err != nil { log.Printf("转换失败: %v", err) // 处理错误 } ``` 大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。 ## 常见问题 **Q: 字段名必须完全相同吗?** A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 `UserId`,pb 字段也必须是 `UserId`。 **Q: 如果某个字段转换失败怎么办?** A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。 **Q: 能否自定义字段映射规则(比如 `db_name` -> `pbName`)?** A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。 **Q: 转换速度快吗?** A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。 ## 相关文件 - `generic.go` - 通用转换函数核心实现 - `user_converter.go` - Users model 专用转换函数(示例)