Type Conversion - Custom Type Conversion

The gconv conversion component provides powerful type conversion functionality, allowing developers to customize conversion logic between types. It offers two main custom conversion method registration mechanisms: RegisterTypeConverterFunc and RegisterAnyConverterFunc.

Type Conversion - Custom Type Conversion - 图1 info

Custom type conversion does not support custom conversion of basic types (such as int, string, etc.), and only supports complex data structure types (such as slice, map, struct, etc.). This design is to improve conversion efficiency.

RegisterTypeConverterFunc

Function Definition

The RegisterTypeConverterFunc method is used to register conversion functions between specific types, defined as follows:

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

The signature of the conversion function must conform to the following format:

func(T1) (*T2, error)

Where T1 needs to be a non-pointer object, and T2 needs to be a pointer type. If the types are incorrect, the conversion method registration will report an error.

Type Conversion - Custom Type Conversion - 图2 tip

The design of requiring the input parameter (T1) to be a non-pointer object is to ensure the safety of the input parameter and to avoid modifying the input parameter within the conversion method, which could cause problems outside the scope.

Usage Examples

Struct Type Conversion

The most common application scenario for RegisterTypeConverterFunc is type conversion between structs. Let‘s look at an example using RegisterTypeConverterFunc:

package main

import (
    "fmt"
    "time"

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

// UserInfo custom type
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)
    }
    // Calculate age
    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()
    // Register custom type conversion function - from UserInfo to UserDTO
    err := converter.RegisterTypeConverterFunc(userInfoT2UserDTO)
    if err != nil {
        fmt.Println("Failed to register conversion function:", err)
        return
    }

    // Use custom type conversion
    userInfo := UserInfo{
        ID:       101,
        Name:     "Zhang San",
        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("Conversion failed:", err)
    } else {
        fmt.Printf("Custom type conversion result: %#v\n", userDTO)
        // Output similar to: Custom type conversion result: main.UserDTO{UserID:"U101", UserName:"Zhang San", Age:35}
    }

    // Test error handling
    invalidUser := UserInfo{ID: -1, Name: "Invalid User"}
    err = converter.Scan(invalidUser, &userDTO)
    if err != nil {
        fmt.Println("Expected error:", err) // Output error message: invalid user ID: -1
    }
}

In this example code, two type conversion scenarios are demonstrated: custom struct conversion, and automatic conversion of structs as properties. The conversion method uses the general struct conversion method gconv.Scan, which in its internal implementation will automatically determine if there is a custom type conversion function, will prioritize using the custom type conversion function, otherwise will follow the default conversion logic.

Alias Type Conversion

We can also use RegisterTypeConverterFunc to implement conversion of alias types. Here is an example:

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 custom conversion function from MyTime to Dst
func MyTimeToDstConverter(src MyTime) (dst *Dst, err error) {
    if src == nil {
        return &Dst{B: ""}, nil
    }
    return &Dst{B: src.String()}, nil
}

func main() {
    // Register custom conversion function
    err := gconv.RegisterTypeConverterFunc(MyTimeToDstConverter)
    if err != nil {
        panic(err)
    }

    // Create test data
    now := gtime.Now()
    src := Src{A: now}

    // Execute conversion
    var dst *Dst
    err = gconv.Scan(src, &dst)
    if err != nil {
        panic(err)
    }

    fmt.Println("Original time:", now)
    fmt.Println("Conversion result:", dst.B)
}

RegisterAnyConverterFunc

RegisterAnyConverterFunc provides a more flexible type conversion mechanism, allowing one conversion function to handle conversions between multiple types. This is especially useful when dealing with complex data structure conversion needs.

Function Definition

The RegisterAnyConverterFunc method provides a more flexible type conversion mechanism, allowing handling of conversions between multiple types, defined as follows:

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

Where the definition of AnyConvertFunc is:

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

This approach allows developers to register a general conversion function that can handle conversions between multiple types.

Usage Examples

Struct Conversion Example

Here is a basic example using RegisterAnyConverterFunc, demonstrating how to implement custom struct conversion:

package main

import (
    "fmt"
    "reflect"

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

// UserInput source struct
type UserInput struct {
    Name     string
    Age      int
    IsActive bool
}

// UserModel target struct
type UserModel struct {
    ID       int
    FullName string
    Age      int
    Status   int
}

func userInput2Model(in any, out reflect.Value) error {
    // Check if output type is UserModel
    if out.Type() == reflect.TypeOf(&UserModel{}) {
        // Handle conversion from UserInput to 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("conversion from %T to UserModel not supported", in)
    }
    return fmt.Errorf("conversion to %s not supported", out.Type())
}

func main() {
    // Create converter
    converter := gconv.NewConverter()

    // Register custom conversion function
    converter.RegisterAnyConverterFunc(userInput2Model, reflect.TypeOf(UserModel{}))

    // Test conversion
    var (
        model UserModel
        input = UserInput{Name: "Zhang San", Age: 30, IsActive: true}
    )
    err := converter.Scan(input, &model)
    if err != nil {
        fmt.Println("Conversion failed:", err)
    } else {
        fmt.Printf(
            "Conversion result: ID=%d, Name=%s, Age=%d, Status=%d\n",
            model.ID, model.FullName, model.Age, model.Status,
        )
    }
}

Complex Nested Struct Conversion Example

Here is a more complex example with nested data structures, demonstrating how to use RegisterAnyConverterFunc to handle complex nested struct conversions:

package main

import (
    "fmt"
    "reflect"

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

// UserDetail source data structure
type UserDetail struct {
    UserID  int
    Name    string
    Address Address
}

// UserDTO target data structure
type UserDTO struct {
    ID          string
    DisplayName string
    Location    string
}

type Address struct {
    City    string
    Country string
}

func userDetail2DTO(in any, out reflect.Value) error {
    // Check if output type is UserDTO
    if out.Type() == reflect.TypeOf(&UserDTO{}) {
        // Handle conversion from UserDetail to 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("conversion from %T to UserDTO not supported", in)
    }
    return fmt.Errorf("conversion to %s not supported", out.Type())
}

func main() {
    // Create converter
    converter := gconv.NewConverter()

    // Register custom conversion function
    converter.RegisterAnyConverterFunc(userDetail2DTO, reflect.TypeOf(UserDTO{}))

    // Test conversion
    var (
        dto    UserDTO
        detail = UserDetail{
            UserID: 12345,
            Name:   "Zhang San",
            Address: Address{
                City:    "Beijing",
                Country: "China",
            },
        }
    )
    if err := converter.Scan(detail, &dto); err != nil {
        fmt.Println("Conversion failed:", err)
    } else {
        fmt.Printf(
            "Conversion result: ID=%s, Name=%s, Address=%s\n",
            dto.ID, dto.DisplayName, dto.Location,
        )
    }
}

This example demonstrates how to use RegisterAnyConverterFunc to implement complex nested struct conversion, including conversion from UserDetail to UserDTO, and conversion from nested map to UserDTO. This kind of complex data structure conversion is the main application scenario for RegisterAnyConverterFunc.

Comparison of Two Conversion Functions

The two registration methods each have their advantages, disadvantages, and applicable scenarios:

RegisterTypeConverterFunc

Advantages:

  1. Type safety : Since the function signature has already defined the input and output types, the compiler can perform type checking at compile time.
  2. Simple to use : Definition and usage are relatively intuitive, no need to manually handle reflection.
  3. Suitable for specific type conversion : When you only need to handle conversion between two specific types, this method is clearer.

Disadvantages:

  1. Limited flexibility : Each function can only handle conversion from one specific type to another specific type.
  2. Multiple functions needed : If you need to handle conversions of multiple types, you need to define and register multiple functions.

RegisterAnyConverterFunc

Advantages:

  1. Highly flexible : One function can handle conversions from multiple input types to a specific output type.
  2. Code reuse : Related conversion logic can be centralized in one function, reducing duplicate code.
  3. Dynamic judgment : Can dynamically decide how to convert based on input type at runtime.

Disadvantages:

  1. Reduced type safety : Using reflection and interface types, the compiler cannot check for type errors.
  2. Complex to use : Need to manually handle reflection and type assertions, increasing complexity.