跳到主要内容
版本:2.9.x(Latest)

gconv转换组件提供了强大的类型转换功能,允许开发者自定义类型之间的转换逻辑,提供了两种主要的自定义转换方法注册机制:RegisterTypeConverterFuncRegisterAnyConverterFunc

信息

自定义类型转换类型不支持基础类型(如 intstring 等)的自定义转换,仅支持复杂数据结构类型(如 slicemapstruct 等),这种设计是为了提高转换效率。

RegisterTypeConverterFunc

函数定义

RegisterTypeConverterFunc方法用于注册特定类型之间的转换函数,定义如下:

func RegisterTypeConverterFunc(fn interface{}) (err error)

转换函数的签名必须符合以下格式:

func(T1) (*T2, error)

其中 T1 需要为非指针对象, T2 需要为指针类型,如果类型不正确转换方法注册将会报错。

提示

输入参数(T1)必须为非指针对象的设计,目的是为了保证输入参数的安全,尽可能避免在转换方法内部修改输入参数引起作用域外问题。

使用示例

结构体类型转换

RegisterTypeConverterFunc最常见的应用场景是结构体之间的类型转换。我们来看一个使用RegisterTypeConverterFunc的例子:

package main

import (
"fmt"
"time"

"github.com/gogf/gf/v2/util/gconv"
)

// UserInfo 自定义类型
type UserInfo struct {
ID int
Name string
Birthday time.Time
}

type UserDTO struct {
UserID string
UserName string
Age int
}

func userInfoT2UserDTO(in UserInfo) (*UserDTO, error) {
if in.ID <= 0 {
return nil, fmt.Errorf("invalid user ID: %d", in.ID)
}
// 计算年龄
age := time.Now().Year() - in.Birthday.Year()
return &UserDTO{
UserID: fmt.Sprintf("U%d", in.ID),
UserName: in.Name,
Age: age,
}, nil
}

func main() {
converter := gconv.NewConverter()
// 注册自定义类型转换函数 - 从UserInfo到UserDTO
err := converter.RegisterTypeConverterFunc(userInfoT2UserDTO)
if err != nil {
fmt.Println("注册转换函数失败:", err)
return
}

// 使用自定义类型转换
userInfo := UserInfo{
ID: 101,
Name: "张三",
Birthday: time.Date(1990, 5, 15, 0, 0, 0, 0, time.Local),
}

var userDTO UserDTO
err = converter.Scan(userInfo, &userDTO)
if err != nil {
fmt.Println("转换失败:", err)
} else {
fmt.Printf("自定义类型转换结果: %#v\n", userDTO)
// 输出类似: 自定义类型转换结果: main.UserDTO{UserID:"U101", UserName:"张三", Age:35}
}

// 测试错误处理
invalidUser := UserInfo{ID: -1, Name: "无效用户"}
err = converter.Scan(invalidUser, &userDTO)
if err != nil {
fmt.Println("预期的错误:", err) // 输出错误信息: invalid user ID: -1
}
}

在该示例代码中,演示了两种类型转换场景:自定义结构体转换,以及结构体作为属性的自动转换。转换方法使用的是通用的结构体转换方法 gconv.Scan,该方法在内部实现中会自动判断如果存在自定义类型转换函数,会优先使用自定义类型转换函数,否则会走默认的转换逻辑。

别名类型转换

我们也可以使用 RegisterTypeConverterFunc 实现 别名类型 的转换。下面是一个示例:

package main

import (
"fmt"

"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)

type MyTime = *gtime.Time

type Src struct {
A MyTime
}

type Dst struct {
B string
}

type SrcWrap struct {
Value Src
}

type DstWrap struct {
Value Dst
}

// MyTimeToDstConverter 自定义MyTime到Dst的转换函数
func MyTimeToDstConverter(src MyTime) (dst *Dst, err error) {
if src == nil {
return &Dst{B: ""}, nil
}
return &Dst{B: src.String()}, nil
}

func main() {
// 注册自定义转换函数
err := gconv.RegisterTypeConverterFunc(MyTimeToDstConverter)
if err != nil {
panic(err)
}

// 创建测试数据
now := gtime.Now()
src := Src{A: now}

// 执行转换
var dst *Dst
err = gconv.Scan(src, &dst)
if err != nil {
panic(err)
}

fmt.Println("原始时间:", now)
fmt.Println("转换结果:", dst.B)
}

RegisterAnyConverterFunc

RegisterAnyConverterFunc 提供了更灵活的类型转换机制,允许一个转换函数处理多种类型的转换。这在处理复杂的数据结构转换需求时特别有用。

函数定义

RegisterAnyConverterFunc方法提供了更灵活的类型转换机制,允许处理多种类型的转换,定义如下:

func RegisterAnyConverterFunc(f AnyConvertFunc, types ...reflect.Type)

其中AnyConvertFunc的定义为:

// AnyConvertFunc is the function type for converting any to specified type.
type AnyConvertFunc func(from any, to reflect.Value) error

这种方式允许开发者注册一个通用的转换函数,可以处理多种类型之间的转换。

使用示例

结构体转换示例

下面是一个使用 RegisterAnyConverterFunc 的基本示例,演示如何实现自定义结构体转换:

package main

import (
"fmt"
"reflect"

"github.com/gogf/gf/v2/util/gconv"
)

// UserInput 源结构体
type UserInput struct {
Name string
Age int
IsActive bool
}

// UserModel 目标结构体
type UserModel struct {
ID int
FullName string
Age int
Status int
}

func userInput2Model(in any, out reflect.Value) error {
// 检查输出类型是否为 UserModel
if out.Type() == reflect.TypeOf(&UserModel{}) {
// 处理 UserInput 到 UserModel 的转换
if input, ok := in.(UserInput); ok {
model := UserModel{
ID: 1,
FullName: input.Name,
Age: input.Age,
Status: 0,
}
if input.IsActive {
model.Status = 1
}
out.Elem().Set(reflect.ValueOf(model))
return nil
}
return fmt.Errorf("不支持从 %T 转换到 UserModel", in)
}
return fmt.Errorf("不支持转换到 %s", out.Type())
}

func main() {
// 创建转换器
converter := gconv.NewConverter()

// 注册自定义转换函数
converter.RegisterAnyConverterFunc(userInput2Model, reflect.TypeOf(UserModel{}))

// 测试转换
var (
model UserModel
input = UserInput{Name: "张三", Age: 30, IsActive: true}
)
err := converter.Scan(input, &model)
if err != nil {
fmt.Println("转换失败:", err)
} else {
fmt.Printf(
"转换结果: ID=%d, 名称=%s, 年龄=%d, 状态=%d\n",
model.ID, model.FullName, model.Age, model.Status,
)
}
}

复杂结构体嵌套转换示例

下面是一个带有嵌套数据结构的较复杂示例,演示如何使用 RegisterAnyConverterFunc 处理复杂的嵌套结构体转换:

package main

import (
"fmt"
"reflect"

"github.com/gogf/gf/v2/util/gconv"
)

// UserDetail 源数据结构
type UserDetail struct {
UserID int
Name string
Address Address
}

// UserDTO 目标数据结构
type UserDTO struct {
ID string
DisplayName string
Location string
}

type Address struct {
City string
Country string
}

func userDetail2DTO(in any, out reflect.Value) error {
// 检查输出类型是否为 UserDTO
if out.Type() == reflect.TypeOf(&UserDTO{}) {
// 处理 UserDetail 到 UserDTO 的转换
if detail, ok := in.(UserDetail); ok {
dto := UserDTO{
ID: fmt.Sprintf("USER-%d", detail.UserID),
DisplayName: detail.Name,
Location: fmt.Sprintf("%s, %s", detail.Address.City, detail.Address.Country),
}
out.Elem().Set(reflect.ValueOf(dto))
return nil
}
return fmt.Errorf("不支持从 %T 转换到 UserDTO", in)
}
return fmt.Errorf("不支持转换到 %s", out.Type())
}

func main() {
// 创建转换器
converter := gconv.NewConverter()

// 注册自定义转换函数
converter.RegisterAnyConverterFunc(userDetail2DTO, reflect.TypeOf(UserDTO{}))

// 测试转换

var (
dto UserDTO
detail = UserDetail{
UserID: 12345,
Name: "张三",
Address: Address{
City: "北京",
Country: "中国",
},
}
)
if err := converter.Scan(detail, &dto); err != nil {
fmt.Println("转换失败:", err)
} else {
fmt.Printf(
"转换结果: ID=%s, 名称=%s, 地址=%s\n",
dto.ID, dto.DisplayName, dto.Location,
)
}
}

这个示例演示了如何使用 RegisterAnyConverterFunc 实现复杂的嵌套结构体转换,包括从 UserDetailUserDTO 的转换,以及从嵌套 mapUserDTO 的转换。这种复杂的数据结构转换是 RegisterAnyConverterFunc 的主要应用场景。

两种转换函数的比较

两种注册方式各有其优缺点和适用场景:

RegisterTypeConverterFunc

优点:

  1. 类型安全:由于函数签名已经定义了输入和输出类型,编译器可以在编译时进行类型检查。
  2. 使用简单:定义和使用相对直观,不需要手动处理反射。
  3. 适合特定类型转换:当只需要处理两个特定类型之间的转换时,这种方式更加清晰。

缺点:

  1. 灵活性有限:每个函数只能处理一种特定类型到另一种特定类型的转换。
  2. 需要多个函数:如果需要处理多种类型的转换,则需要定义和注册多个函数。

RegisterAnyConverterFunc

优点:

  1. 高度灵活:一个函数可以处理多种输入类型到特定输出类型的转换。
  2. 代码重用:可以在一个函数中集中处理相关的转换逻辑,减少重复代码。
  3. 动态判断:可以在运行时根据输入类型动态决定如何转换。

缺点:

  1. 类型安全性降低:使用反射和接口类型,编译器无法检查类型错误。
  2. 使用复杂:需要手动处理反射和类型断言,增加了复杂性。
  3. 错误处理更复杂:需要手动处理各种类型错误和边界情况,易出错。

选择建议

  1. 当只需要处理两个特定类型之间的转换时,使用 RegisterTypeConverterFunc
  2. 当需要处理多种输入类型到同一输出类型的转换,或者需要基于输入值的内容动态决定转换逻辑时,使用 RegisterAnyConverterFunc
  3. 在大型项目中,可以结合使用这两种方式,简单的类型转换使用 RegisterTypeConverterFunc,复杂的多类型转换使用 RegisterAnyConverterFunc

最佳实践

  1. 在启动时注册:将自定义转换函数的注册放在应用程序的启动过程中,避免在运行时重复注册。

  2. 错误处理:始终返回有意义的错误信息,便于调试和问题跟踪。

  3. 性能考虑:对于频繁调用的转换函数,尽量优化性能,避免不必要的反射操作。

  4. 保持兼容性:在更新自定义转换函数时,确保其与现有代码的兼容性。

  5. 测试覆盖:为自定义转换函数编写全面的单元测试,确保其在各种情况下都能正常工作。