Skip to content

epkgs/copier

Repository files navigation

copier 📦

一个功能强大的 Go 库,用于在不同数据结构之间进行双向数据转换,支持 map→struct、struct→map 和 struct→struct 转换。

✨ 核心功能

  • 双向数据转换:支持 map ↔ struct、struct ↔ struct 之间的无缝转换
  • 类型安全转换:精确的类型检查和转换,确保数据完整性
  • 弱类型输入支持:自动进行类型转换(如字符串 "30" → 整数 30)
  • 灵活的标签配置:支持自定义标签名称和转换规则
  • 嵌入结构体处理:自动处理嵌入结构体(包括指针嵌入)
  • 元数据跟踪:记录转换过程中的关键信息(使用的键、未使用的键、未设置的字段)
  • 字段名称转换:支持不同命名约定之间的转换(如驼峰式 → 蛇形命名)
  • 错误处理:详细的错误信息,便于调试和排查问题

📥 安装指南

使用 Go 模块安装:

go get github.com/epkgs/copier

🚀 快速入门

基本示例:Map → Struct

package main

import (
    "fmt"
    "github.com/epkgs/copier"
)

type Person struct {
    Name   string
    Age    int
    Emails []string
    Extra  map[string]string
}

func main() {
    input := map[string]any{
        "name":   "Edwin",
        "age":    91,
        "emails": []string{"one", "two", "three"},
        "extra": map[string]string{
            "twitter": "mitchellh",
        },
    }

    var result Person
    err := copier.Copy(&result, input)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%#v", result)
    // 输出:
    // main.Person{Name:"Edwin", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}}
}

类型转换示例

package main

import (
    "fmt"
    "github.com/epkgs/copier"
)

type Person struct {
    Name   string
    Age    int
    Emails []string
}

func main() {
    input := map[string]any{
        "name":   123,              // 数字 → 字符串
        "age":    "42",             // 字符串 → 数字
        "emails": map[string]any{}, // 空 map → 空数组
    }

    var result Person
    err := copier.Copy(&result, input, copier.WithWeaklyTypedInput(true))
    if err != nil {
        panic(err)
    }

    fmt.Printf("%#v", result)
    // 输出:main.Person{Name:"123", Age:42, Emails:[]string{}}
}

Struct → Struct 转换

package main

import (
    "fmt"
    "github.com/epkgs/copier"
)

// 源结构体
type User struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Email     string `json:"email"`
    Age       int    `json:"age"`
}

// 目标结构体
type Profile struct {
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

func main() {
    user := User{
        FirstName: "John",
        LastName:  "Doe",
        Email:     "john.doe@example.com",
        Age:       30,
    }

    var profile Profile
    err := copier.Copy(&profile, user)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%#v", profile)
    // 输出:main.Profile{Email:"john.doe@example.com", Age:30}
}

Struct → Map 转换

package main

import (
    "fmt"
    "github.com/epkgs/copier"
)

type User struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Email     string `json:"email"`
    Age       int    `json:"age"`
    Active    bool   `json:"active"`
}

func main() {
    user := User{
        FirstName: "John",
        LastName:  "Doe",
        Email:     "john.doe@example.com",
        Age:       30,
        Active:    true,
    }

    var result map[string]any
    err := copier.Copy(&result, user)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%#v", result)
    // 输出:map[string]interface {}{"active":true, "age":30, "email":"john.doe@example.com", "first_name":"John", "last_name":"Doe"}
}

📚 API 参考

核心函数

Copy(dst any, src any, opts ...Option) error

将源数据转换并分配到目标数据结构中。

  • 参数

    • dst:目标数据结构(必须是指针)
    • src:源数据结构(可以是 map、struct 等)
    • opts:可选的配置选项函数,用于自定义转换行为
  • 返回值

    • error:转换过程中发生的错误,可能包含多个错误信息

配置选项

配置函数

库提供了一系列配置函数,用于自定义转换行为:

  1. WithWeaklyTypedInput(enable bool)

    • 如果为 true,将执行弱类型转换(如字符串 "30" → 整数 30)
    • 默认值:false
  2. WithTagName(tagName string)

    • 用于字段映射的标签名称(默认:"json")
    • 支持从 json / mapstructure 标签中获取映射名称和 omitempty 属性
    • 示例:copier.WithTagName("mapstructure")
  3. WithIncludeIgnored(include bool)

    • 如果为 true,将包含被忽略的字段(通过 "-" 标签标记)
    • 默认值:false
  4. WithFieldNameConverter(structObj any, converter func(fieldName string) string)

    • 自定义 struct 字段名称转换函数,如未设定,默认为小驼峰模式
  5. WithTypeConverter[Dst any, Src any](fn func(src Src) (dst Dst, err error))

    • 自定义类型转换器,用于在解码过程中转换特定类型的字段
  6. WithMetadata(meta *Metadata)

    • 设置转换元数据,记录转换过程中的信息
  7. WithDeepCopy(enable bool)

    • 如果为 true,将进行深层拷贝,否则仅拷贝第一层
    • 默认值:false

Metadata 结构体

type Metadata struct {
    // 成功分配的键列表
    Keys []string
    
    // 源数据中未使用的键列表
    Unused []string
    
    // 目标结构中未设置的字段列表
    Unset []string
}

🎯 使用场景

1. 获取更新字段

package main

import (
    "fmt"
    "github.com/epkgs/copier"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

type User struct {
    Name string
    Age  int
}

type UpdateUserRequest struct {
    Name *string
    Age  *int
}

func UpdateUser(c *gin.Context){
    var req UpdateUserRequest
    _ = c.ShouldBindJSON(&req) // 简单展示,忽略绑定错误

    var user User
    var md copier.Metadata
    _ = copier.Copy(&user, req, copier.WithMetadata(&md)) // 简单展示,忽略转换错误

    gorm.G[User](db).Where("id = ?", 111).Select(md.Keys...).Updates(ctx, user) // 使用 md.Keys 过滤更新字段
}

❓ 常见问题解答

1. 为什么转换失败并提示 "target must be a pointer"?

这是因为目标参数必须是一个指针,以便库可以修改其内容。请确保将目标参数的地址传递给 Copy 函数:

// 错误
var result Person
err := copier.Copy(result, input)

// 正确
var result Person
err := copier.Copy(&result, input)

2. 如何处理不同的命名约定?

可以使用 WithFieldNameConverter 函数来自定义字段名称转换:

import (
    "strings"
    "github.com/epkgs/copier"
)

// 示例:将结构体字段转换为蛇形命名
func toSnakeCase(fieldName string) string {
    var result strings.Builder
    for i, r := range fieldName {
        if i > 0 && r >= 'A' && r <= 'Z' {
            result.WriteRune('_')
        }
        result.WriteRune(r)
    }
    return strings.ToLower(result.String())
}

// 使用示例
err := copier.Copy(&result, input, 
    copier.WithFieldNameConverter(resultType{}, toSnakeCase),
)

3. 如何忽略某些字段?

可以在结构体标签中使用 -

type User struct {
    ID       int    `json:"id"`
    Password string `json:"-"` // 这个字段将被忽略
}

4. 在 tag 里标记了 "-",但仍希望包含 tag 忽略的字段,可使用 WithIncludeIgnored 配置选项:

err := copier.Copy(
    &result, 
    input, 
    copier.WithIncludeIgnored(User{}, []string{"Password"}),
)

4. 为什么嵌入结构体的字段没有被正确转换?

库会自动处理嵌入结构体(包括指针嵌入)。请确保嵌入结构体的字段是导出的(首字母大写):

type Base struct {
    ID int
}

type User struct {
    Base // 嵌入结构体,字段 ID 将被导出
    Name string
}

🤝 贡献指南

我们欢迎任何形式的贡献!如果您想为这个库做出贡献,请按照以下步骤进行:

  1. Fork 仓库:在 GitHub 上 fork 这个仓库
  2. 创建分支:为您的功能或修复创建一个新的分支
  3. 实现功能:编写代码并确保通过所有测试
  4. 提交代码:使用清晰的提交信息提交您的更改
  5. 创建 Pull Request:在 GitHub 上创建一个 Pull Request,描述您的更改

开发环境设置

# 克隆仓库
git clone https://github.com/epkgs/copier.git
cd copier

# 安装依赖
go mod tidy

# 运行测试
go test ./...

代码规范

  • 遵循 Go 语言的标准代码规范
  • 确保所有新功能都有相应的测试
  • 保持代码简洁、清晰和可维护

📄 许可证

本库使用 MIT 许可证,详情请查看 LICENSE 文件。

📧 联系方式

如果您有任何问题或建议,请在 GitHub 上创建一个 Issue 或提交一个 Pull Request。


感谢您使用 copier 库!我们希望它能帮助您更轻松地处理数据转换任务。🎉

About

GO 数据拷贝

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages