在Golang日常开发中,结构体数据拷贝是非常常见的需求,如果是同类型结构体,手动赋值还比较方便,但如果是不同类型但字段相似的结构体,逐个字段赋值就非常繁琐。这时可以利用反射机制实现通用拷贝函数,适配多种结构体拷贝场景。

核心实现思路
通用拷贝函数的核心逻辑是通过反射获取源数据和目标数据的类型、值信息,然后遍历源数据的字段,匹配目标数据中同名字段,将源字段的值拷贝到目标字段。主要步骤分为以下几步:
- 校验源数据和目标数据的类型,确保两者都是结构体或者结构体指针
- 通过反射获取源数据和目标数据的实际值,处理指针类型的入参
- 遍历源结构体的所有字段,获取字段名和可导出状态
- 在目标结构体中查找同名字段,判断字段类型是否可赋值
- 将源字段的值拷贝到目标字段,跳过不可导出字段和类型不匹配的字段
完整代码实现
下面是使用Golang反射实现的通用拷贝函数完整代码,包含基础的类型校验和字段拷贝逻辑:
package main
import (
"fmt"
"reflect"
)
// CopyStruct 通用结构体拷贝函数,src为源数据,dst为目标数据指针
func CopyStruct(src, dst interface{}) error {
// 获取src的反射值
srcValue := reflect.ValueOf(src)
// 如果是指针,获取指向的值
if srcValue.Kind() == reflect.Ptr {
srcValue = srcValue.Elem()
}
// 校验src必须是结构体
if srcValue.Kind() != reflect.Struct {
return fmt.Errorf("src must be struct or struct pointer")
}
// 获取dst的反射值
dstValue := reflect.ValueOf(dst)
// dst必须是指针
if dstValue.Kind() != reflect.Ptr {
return fmt.Errorf("dst must be struct pointer")
}
// 获取指针指向的值
dstElem := dstValue.Elem()
// 校验dst指向的必须是结构体
if dstElem.Kind() != reflect.Struct {
return fmt.Errorf("dst must point to struct")
}
// 获取src的类型信息
srcType := srcValue.Type()
// 遍历src的所有字段
for i := 0; i < srcValue.NumField(); i++ {
srcField := srcValue.Field(i)
srcFieldType := srcType.Field(i)
// 跳过不可导出字段
if !srcFieldType.IsExported() {
continue
}
// 在dst中查找同名字段
dstField := dstElem.FieldByName(srcFieldType.Name)
// 如果dst中不存在该字段,跳过
if !dstField.IsValid() {
continue
}
// 如果dst字段不可导出,跳过
if !dstField.CanSet() {
continue
}
// 判断src字段的值是否可以赋值给dst字段
if srcField.Type().AssignableTo(dstField.Type()) {
dstField.Set(srcField)
}
}
return nil
}
// 源结构体
type UserSrc struct {
Name string
Age int
Email string
}
// 目标结构体
type UserDst struct {
Name string
Age int
Email string
}
// 字段不同的目标结构体
type UserDst2 struct {
Name string
Age int
Email string
Address string // 源结构体中不存在的字段
phone string // 不可导出字段,不会被拷贝
}
func main() {
src := UserSrc{
Name: "张三",
Age: 25,
Email: "test@ipipp.com",
}
var dst UserDst
err := CopyStruct(src, &dst)
if err != nil {
fmt.Println("拷贝失败:", err)
return
}
fmt.Printf("拷贝结果: %+v\n", dst)
var dst2 UserDst2
err = CopyStruct(src, &dst2)
if err != nil {
fmt.Println("拷贝失败:", err)
return
}
fmt.Printf("拷贝到不同结构体结果: %+v\n", dst2)
}代码说明
上述代码中的CopyStruct函数首先会对入参类型做校验,确保源数据是结构体或结构体指针,目标数据是结构体指针。接着通过反射遍历源结构体的所有可导出字段,在目标结构体中查找同名字段,仅当字段类型可直接赋值时才执行拷贝操作。
需要注意,如果源结构体和目标结构体的对应字段类型不同,比如一个是int一个是int64,这种实现不会做类型转换,会直接跳过该字段。如果需要支持类型转换,可以在判断赋值逻辑处添加类型转换的相关处理。
注意事项
- 反射会有一定的性能损耗,如果对性能要求极高的场景,不建议频繁使用通用拷贝函数,可手动编写专用拷贝逻辑
- 该实现仅支持浅拷贝,如果结构体包含切片、映射、指针等引用类型字段,拷贝后源和目标会共享这些引用类型的数据
- 不可导出字段不会被拷贝,这是Golang反射的机制限制,无法修改不可导出字段的值
- 如果目标结构体中不存在源结构体的某个字段,该字段会被自动跳过,不会报错