Of course, you might have already guessed that the gconv
module actually uses reflection for conversions involving complex types such as struct
. While it provides a great convenience for developers, it does come at the cost of performance. For struct
conversions, if developers have clearly defined conversion rules and are concerned about performance costs, they can implement the UnmarshalValue
interface for specific structs
to achieve custom conversion. When using the gconv
module for conversion, whether the struct
is directly the conversion object or a property of the conversion object, gconv
will automatically recognize the implemented UnmarshalValue
interface and use it for type conversion instead of using reflection.
Common deserialization interfaces in the standard library, such as UnmarshalText(text []byte) error
, are also supported, and they are used in a similar manner to UnmarshalValue
, with different parameters.
Interface Definition
// apiUnmarshalValue is the interface for custom defined types customizing value assignment.
// Note that only pointer can implement interface apiUnmarshalValue.
type apiUnmarshalValue interface {
UnmarshalValue(interface{}) error
}
As you can see, custom types can implement the UnmarshalValue
method for custom type conversion. The input parameter here is of interface{}
type, and developers can use type assertions or other methods for type conversion in practical use cases.
It's important to note that because the UnmarshalValue
type conversion modifies the properties of the current object, the receiver of the interface implementation must be a pointer type.
Correct interface implementation example (using pointer receiver):
func (c *Receiver) UnmarshalValue(interface{}) error
Incorrect interface implementation example (using value receiver):
func (c Receiver) UnmarshalValue(interface{}) error
Usage Examples
1. Custom Data Table Query Result struct
Conversion
Data table structure:
CREATE TABLE `user` (
id bigint unsigned NOT NULL AUTO_INCREMENT,
passport varchar(45),
password char(32) NOT NULL,
nickname varchar(45) NOT NULL,
create_time timestamp NOT NULL,
PRIMARY KEY (id)
) ;
Example code:
package main
import (
"fmt"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"reflect"
)
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime *gtime.Time
}
// Implement UnmarshalValue interface for custom struct conversion
func (user *User) UnmarshalValue(value interface{}) error {
if record, ok := value.(gdb.Record); ok {
*user = User{
Id: record["id"].Int(),
Passport: record["passport"].String(),
Password: "",
Nickname: record["nickname"].String(),
CreateTime: record["create_time"].GTime(),
}
return nil
}
return gerror.Newf(`unsupported value type for UnmarshalValue: %v`, reflect.TypeOf(value))
}
func main() {
var (
err error
users []*User
)
array := garray.New(true)
for i := 1; i <= 10; i++ {
array.Append(g.Map{
"id": i,
"passport": fmt.Sprintf(`user_%d`, i),
"password": fmt.Sprintf(`pass_%d`, i),
"nickname": fmt.Sprintf(`name_%d`, i),
"create_time": gtime.NewFromStr("2018-10-24 10:00:00").String(),
})
}
// Insert data
_, err = g.Model("user").Data(array).Insert()
if err != nil {
panic(err)
}
// Query data
err = g.Model("user").Order("id asc").Scan(&users)
if err != nil {
panic(err)
}
g.Dump(users)
}
After execution, the terminal output:
[
{
Id: 1,
Passport: "user_1",
Password: "",
Nickname: "name_1",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 2,
Passport: "user_2",
Password: "",
Nickname: "name_2",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 3,
Passport: "user_3",
Password: "",
Nickname: "name_3",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 4,
Passport: "user_4",
Password: "",
Nickname: "name_4",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 5,
Passport: "user_5",
Password: "",
Nickname: "name_5",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 6,
Passport: "user_6",
Password: "",
Nickname: "name_6",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 7,
Passport: "user_7",
Password: "",
Nickname: "name_7",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 8,
Passport: "user_8",
Password: "",
Nickname: "name_8",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 9,
Passport: "user_9",
Password: "",
Nickname: "name_9",
CreateTime: "2018-10-24 10:00:00",
},
{
Id: 10,
Passport: "user_10",
Password: "",
Nickname: "name_10",
CreateTime: "2018-10-24 10:00:00",
},
]
As you can see, the custom UnmarshalValue
type conversion method does not use reflection, which greatly improves conversion performance. You can try increasing the data volume (e.g., 1 million
) and compare the time cost of type conversion without UnmarshalValue
.
2. Custom Binary TCP Data Unpacking
An example of unpacking a TCP communication data packet.
package main
import (
"errors"
"fmt"
"github.com/gogf/gf/v2/crypto/gcrc32"
"github.com/gogf/gf/v2/encoding/gbinary"
"github.com/gogf/gf/v2/util/gconv"
)
type Pkg struct {
Length uint16 // Total length.
Crc32 uint32 // CRC32.
Data []byte
}
// NewPkg creates and returns a package with given data.
func NewPkg(data []byte) *Pkg {
return &Pkg{
Length: uint16(len(data) + 6),
Crc32: gcrc32.Encrypt(data),
Data: data,
}
}
// Marshal encodes the protocol struct to bytes.
func (p *Pkg) Marshal() []byte {
b := make([]byte, 6+len(p.Data))
copy(b, gbinary.EncodeUint16(p.Length))
copy(b[2:], gbinary.EncodeUint32(p.Crc32))
copy(b[6:], p.Data)
return b
}
// UnmarshalValue decodes bytes to protocol struct.
func (p *Pkg) UnmarshalValue(v interface{}) error {
b := gconv.Bytes(v)
if len(b) < 6 {
return errors.New("invalid package length")
}
p.Length = gbinary.DecodeToUint16(b[:2])
if len(b) < int(p.Length) {
return errors.New("invalid data length")
}
p.Crc32 = gbinary.DecodeToUint32(b[2:6])
p.Data = b[6:]
if gcrc32.Encrypt(p.Data) != p.Crc32 {
return errors.New("crc32 validation failed")
}
return nil
}
func main() {
var p1, p2 *Pkg
// Create a demo pkg as p1.
p1 = NewPkg([]byte("123"))
fmt.Println(p1)
// Convert bytes from p1 to p2 using gconv.Struct.
err := gconv.Struct(p1.Marshal(), &p2)
if err != nil {
panic(err)
}
fmt.Println(p2)
}
After execution, the terminal output:
&{9 2286445522 [49 50 51]}
&{9 2286445522 [49 50 51]}