Golang:不同结构体类型之间是否可以进行转换?

107

假设我有两个类似的集合,设置如下:

type type1 []struct {
    Field1 string
    Field2 int
}
type type2 []struct {
    Field1 string
    Field2 int
}

如果已知类型1和类型2具有相同的字段,除了编写一个将源中的所有字段复制到目标中的循环之外,是否有一种直接的方法将值从类型1写入类型2?

谢谢。


2
请查看https://dev59.com/doDba4cB1Zd3GeqPKep5#24384133。 - rvignacio
8个回答

98

就您的具体示例而言,您可以轻松地将其转换为playground

t1 := type1{{"A", 1}, {"B", 2}}
t2 := type2(t1)
fmt.Println(t2)

2
虽然这是可能的,但有点荒谬。本质上,您正在更改类型。如果结构不匹配,则代码会抛出异常。 - Richard
51
不,如果结构体的类型不匹配,它不会引发panic,只是无法编译。 - user
22
如果第一个结构体是由库给定的,但你想从第二个结构体生成 JSON 或 XML,那么这将非常有用。 - jumper
1
如果type1和type2是具有相同字段的“struct”,而不是“[]struct”,并且我有一个类型为type1的切片(var mySlice []type1),那么是否有任何简单的方法可以将mySlice转换为[]type2 - sgonzalez

96

为了参考OneOfOne的答案,请参阅规范的Conversions部分。

它指出:

非常量值 x 可以在以下任何情况下转换为类型 T

  • x 可以赋值给 T
  • x 的类型和 T 具有相同的基础类型。
  • x 的类型和 T 都是未命名指针类型,它们的指针基础类型具有相同的基础类型。
  • x 的类型和 T 都是整数或浮点数类型。
  • x 的类型和 T 都是复数类型。
  • x 是整数或字节或符文片段,而 T 是字符串类型。
  • x 是字符串,而 T 是字节或符文片段。

第一个被突出显示的情况就是你的情况。两种类型都具有相同的基础类型。

[]struct { Field1 string Field2 int }

底层类型的定义如下:

如果T是预声明的布尔型、数字型或字符串型之一,或者是一个类型字面量,则相应的底层类型就是T本身。否则,T的底层类型就是它在类型声明中所引用的类型的底层类型。(规范,Types

您正在使用类型字面量来定义您的类型,因此这个类型字面量就是您的底层类型。


这应该是被选为答案的内容,参考值加1。 - OneOfOne
感谢你们两位的回答。但是我不明白为什么我仍然会得到以下错误: cannot convert res (type []struct { Name string "json:\"a.name\"" }) to type ListSociete 使用类型 type Societe struct {Name string}type ListSociete []Societe。我运行了一个 return ListSociete(res)。 这是因为 json 标签 a.name 吗? - Nicolas Marshall
3
请发布另一个包含更多细节的问题,谢谢。 - OneOfOne
没问题。这是链接:https://dev59.com/GoHba4cB1Zd3GeqPP1jl - Nicolas Marshall

22
从 Go 1.8 开始,在将一个结构体类型的值转换为另一个结构体类型时,结构体标签会被忽略。在该版本中,无论结构体标签如何,类型 type1 和 type2 都是可转换的。https://beta.golang.org/doc/go1.8#language

请注意,golang 中的结构体标签是字段声明后的可选字符串文字。这个术语与 C 用法不同,C 中的结构体标签本质上给结构体命名。 - Alexander Klauer

16
尼古拉斯,根据你后面的评论,你在结构体上使用了字段标签;这些标签被视为定义的一部分,因此下面定义的t1和t2是不同的,你不能将t2(t1)转换类型:
type t1 struct {
    Field1 string
}

type t2 struct {
    Field1 string `json:"field_1"`
}

更新:截至Go 1.8版本,此内容已不再适用。


1
真的!那就是我的错误所在。我实际上没有使用我在问题中举例的那些类型。谢谢! - Nicolas Marshall
1
请参阅 https://github.com/golang/go/issues/6858,了解一个备受赞誉但目前尚未实施的提案,以解除此限制。 - user7610
4
自 Go 1.8 版本以来,在转换时会忽略标签。https://golang.org/doc/go1.8#language - Ezequiel Moreno

10
这不是标准的方法,但如果您希望采用灵活的方法将结构体转换为Map,或者想要去除某些结构体属性而又不使用`json:"-",可以使用JSON marshal

具体来说,我所做的是:

type originalStruct []struct {
    Field1 string
    Field2 int
}

targetStruct := make(map[string]interface{}) // `targetStruct` can be anything of your choice

temporaryVariable, _ := json.Marshal(originalStruct)
err = json.Unmarshal(temporaryVariable, &targetStruct) 
if err != nil {
    // Catch the exception to handle it as per your need
}

看起来可能像个技巧,但在我的大多数任务中非常有用。


我真的很喜欢这个解决方案!你(或其他人)能否解释一下为什么它可能是一个hack?因为乍一看它似乎很简单和清晰。 - Carlo Nyte
理想情况下,如果您的项目构建正确,使用类型转换或在结构声明中使用 json: "-" 将是更清晰的实现结构体转换的方式。但是,如果您需要更多的灵活性,则使用映射作为中间状态是有帮助的,这将导致额外的行和处理成本(因为 Marshal 和 Unmarshal 是昂贵的)。请随时纠正我或添加任何其他原因。 - Furqan Rahamath

7

对于已经支持泛型的Go v1.18来说,基本上我只需要创建一个接受任意类型参数并使用json.Marshal / Unmarshal将其转换为另一种类型的方法。

// utils.TypeConverter
func TypeConverter[R any](data any) (*R, error) {
    var result R
    b, err := json.Marshal(&data)
    if err != nil {
      return nil, err
    }
    err = json.Unmarshal(b, &result)
    if err != nil {
      return nil, err
    }
    return &result, err
}

假设我有一个名为models.CreateUserRequest的结构体,我想将其转换为models.User。注意,json标签必须相同。
// models.CreateUserRequest
type CreateUserRequest struct {
   Fullname         string `json:"name,omitempty"`
   RegisterEmail    string `json:"email,omitempty"`
}

// models.User
type User struct {
   Name     string `json:"name,omitempty"`
   Email    string `json:"email,omitempty"`
   Phone    string `json:"phone,omitempty"`
}

我可以像下面这样使用上述的工具方法

user := models.CreateUserRequest {
    Name: "John Doe",
    Email: "johndoe@gmail.com"
}
data, err := utils.TypeConverter[models.User](&user)
if err != nil {
    log.Println(err.Error())
}
log.Println(reflrect.TypeOf(data)) // will output *models.User
log.Println(data)

你可以使用interface{}来完成这个,不需要泛型。 - fatal_error
当结构体的属性部分不同的时候,这种方法非常有用。例如,当数据库模型包含完整的用户信息,而创建新用户时某些属性并不是绝对必要的时候。如果使用您提到的方法,它将无法编译,因为属性不同,而且这也是一个不合理的情况,因为它只是用于转换数据类型。 - wahyudotdev
接口也可以做到这一点(而且编译也没有问题),使用与此处所做的相同的JSON编组/解组技巧。请参见https://dev59.com/8WAf5IYBdhLWcg3weyke#72436927和https://play.golang.com/p/oNH9RA1HiGm以获取示例。 - fatal_error

5

您可以手动使用映射函数,将类型为t1的每个元素映射到类型为t2的元素。这样做是可行的。

func GetT2FromT1(ob1 *t1) *t2 {
     ob2 := &t2 { Field1: t1.Field1, }
     return ob2
}

0

Agniswar Bakshi的回答如果您可以手动编写这些转换,那么速度更快、效果更好,但是这里对Furqan Rahamath的回答进行了扩展。(更完整的示例可在Golang playground上找到)

func Recast(a, b interface{}) error {
    js, err := json.Marshal(a)
    if err != nil {
        return err
    }
    return json.Unmarshal(js, b)
}

// Usage:

type User struct {
    Name string
    PasswordHash string
}

// remove PasswordHash before providing user:
type PrivateOutgoingUser struct {
    Name string
}

u1 := &User{Name: "Alice", PasswordHash: "argon2...."}
u2 := &PrivateOutgoingUser{}
err = Recast(u1, u2)
if err != nil {
    log.Panic("Error recasting u1 to u2", err)
}
log.Println("Limited user:", u2)

还有另一种方法,它使用了JSON标记,速度更快,因为它不需要额外的编组-解组步骤,但灵活性稍逊一些:

type User struct {
    Name string
    PasswordHash string `json:"-"` // - removes the field with JSON
}

user := &User{Name: "Tommy Tester", PasswordHash: "argon2...."}
js, err := json.Marshal(user)
log.Println("Limited user:", string(user))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接