Golang中struct如何优雅地相互转换?

作者: rainlin 分类: 程序语言 发布时间: 2023-05-08 10:55

为什么需要对struct进行转换?

一般而言,不同服务的协议,在生成桩代码后会放在不同仓库中。由于协议引用,相同的数据结构也会重新生成一份。而实践中,一些打包服务需要将上游的数据结构进行透传。由于数据结构所在包不同,被视为了不同的类型,所以无法直接赋值,此时就需要将上游的数据结构转换成当前服务response中的对应结构体

现有的项目里也存在大量的struct的转换,里面的逻辑多是挨个成员拷贝赋值,代码量很多,开发效率较低。直觉告诉我,这种事情应该有更优雅的实现方式,于是开始调研。

如何进行优雅地转换?

先说结论,目前有以下6种方案:

  1. 手撸代码进行转换。目前项目现状,开发效率较低。
  2. golang类型直接转换。开发效率高,性能也高,但适用场景较窄。
  3. 利用三方库,先序列化再反序列化。适用绝大多数场景,性能较低。
  4. 专门的拷贝三方库。适用绝大多数场景,利用反射用于拷贝,性能较低。
  5. 利用unsafe.pointer进行转换。指针转换,效率几乎无损耗,但存在风险。
  6. goverter转换。利用工具自动生成转换代码,效率高,略有学习成本。

测试结构体

// 定义两个相似的结构体
type ANested struct {
   A string
   B int
   C *string
   D []int
   E []*string
}

type A struct {
   Nested *ANested
   A      string
}
type BNested struct {
   A string
   B int
   C *string
   D []int
   E []*string
}
type B struct {
   Nested *BNested
   A      string `json:"A"`
}

A、B成员中的ANested与BNested是不同类型。

1.手撸转换代码

func convertByHand(t *A) {
  var p1 B
  p1.Nested = &BNested{}
  p1.Nested.A = t.Nested.A
  p1.Nested.B = t.Nested.B
  p1.Nested.C = t.Nested.C
  p1.Nested.D = t.Nested.D
  p1.Nested.E = t.Nested.E
  p1.A = t.A
}

就不介绍了,和现状一致。只要手速够快,开发效率也能提上去(此处放置一个大狗头)。

2.golang类型直接转换

调研发现,golang是支持struct类型直接转换的,看示例:

func main() {
   // A与B的元素名及类型完全一致,tag可以不一致
   m1 := ANested{A: "hello world"}
   m2 := BNested(m1)
   fmt.Println(m2)
 }

golang中若结构体的成员类型均一致,则可直接进行转换,go1.8后会忽略tag,转换限制进一步宽松。
这个方式为golang编译器支持,性能较高。但存在一个很大的问题,就是当struct元素的类型不一致时,即便其内部结构一样,也无法转换。

func main() {
   t1 := A{A: "hello world", Nested: &ANested{
      A: "",
      B: 12,
      D: []int{1, 2, 3, 4, 5},
   },
   }
   // 编译报错
   _ = B(t1)
 }

golang社区也有人提出过支持嵌套类型自动转换,但没得到官方的支持,原因见:https://github.com/golang/go/issues/46205

3. 将struct进行序列化,然后再逆序列化转换

基本思路是用三方库将struct序列化为[]byte,然后再反序列化到新的结构体中。
该方案不受方案2的限制,数据结构中相同的成员可被直接赋值,即便不同,也可以通过三方库的tag映射进行转换。
示例:

func jsonProcess(t *A) {
  // json序列化方式
  bs2, _ := json.Marshal(t)
  var p1 B
  _ = json.Unmarshal(bs2, &p1)
}

上面的示例,用了json的序列化方式进行转换,可自由替换序列化方式提高性能,benchmark见后续。

4. 用于拷贝的三方库

在调研的过程中也发现了一些用于拷贝的库,也可用于这里的转换,如copier(https://github.com/jinzhu/copier)
这里没有深入研究原理,大体看了下代码应该是用反射来遍历field,还有诸多的特性,如根据method来copy等。
示例:

func copierProcess(t *A) {
  var p1 B
  _ = copier.Copy(&p1, t)
}

copier使用起来比较方便,只需要一行代码即可,但由于未对该库做深入调研,需自行评估使用。

5. 利用unsafe.pointer进行转换

当结构体数据内存布局相同时,可尝试使用unsafe.pointer进行转换,示例:

func main() {
   t1 := A{A: "hello world", Nested: &ANested{
      A: "",
      B: 12,
      D: []int{1, 2, 3, 4, 5},
   },
   }
   m := (*B)(unsafe.Pointer(&t1))
   fmt.Printf("m:[%+v]", m)
}

该方案性能几乎无损耗,但存在风险。如果两个结构体的内存布局存在差异,在访问时,可能会导致panic。对于业务场景,如果转换的一方的数据结构发生变更,导致双方内存布局不一致,则有可能会出现问题。
该方案未在实际场景尝试,这里仅列出,不做推荐。

6. 转换代码生成工具

方案1的性能很高,但开发效率过低,能否把开发效率提上去呢?自然也是有的:
goverter:https://github.com/jmattheis/goverter
这个工具可以自动生成转换代码,并且支持自定义field映射。
示例:

// goverter:converter
type Converter interface {
   ConvertA(*A) *B
}

type ConverterImpl struct{}

func (c *ConverterImpl) ConvertA(source *A) *B {
   var pMainB *B
   if source != nil {
      var mainB B
      mainB.Nested = c.pMainANestedToPMainBNested((*source).Nested)
      mainB.A = (*source).A
      pMainB = &mainB
   }
   return pMainB
}
func (c *ConverterImpl) pMainANestedToPMainBNested(source *ANested) *BNested {
   var pMainBNested *BNested
   if source != nil {
      var mainBNested BNested
      mainBNested.A = (*source).A
      mainBNested.B = (*source).B
      var pString *string
      if (*source).C != nil {
         xstring := *(*source).C
         pString = &xstring
      }
      mainBNested.C = pString
      var intList []int
      if (*source).D != nil {
         intList = make([]int, len((*source).D))
         for i := 0; i < len((*source).D); i++ {
            intList[i] = (*source).D[i]
         }
      }
      mainBNested.D = intList
      var pStringList []*string
      if (*source).E != nil {
         pStringList = make([]*string, len((*source).E))
         for j := 0; j < len((*source).E); j++ {
            var pString2 *string
            if (*source).E[j] != nil {
               xstring2 := *(*source).E[j]
               pString2 = &xstring2
            }
            pStringList[j] = pString2
         }
      }
      mainBNested.E = pStringList
      pMainBNested = &mainBNested
   }
   return pMainBNested
}

func goverterProcess(t *A) {
   c := ConverterImpl{}
   _ = c.ConvertA(t)
}

生成转换代码,可参照库文档操作。

Benchmark

package main

import (
   "encoding/json"
   "fmt"
   "time"

   "github.com/jinzhu/copier"
   "github.com/vmihailenco/msgpack/v5"
)

var (
   referAsAB = referas.NewReferAs(new(A), new(B)).(func(*A) *B)
)

// goverter:converter
type Converter interface {
   ConvertA(*A) *B
}

type ConverterImpl struct{}

func (c *ConverterImpl) ConvertA(source *A) *B {
   var pMainB *B
   if source != nil {
      var mainB B
      mainB.Nested = c.pMainANestedToPMainBNested((*source).Nested)
      mainB.A = (*source).A
      pMainB = &mainB
   }
   return pMainB
}
func (c *ConverterImpl) pMainANestedToPMainBNested(source *ANested) *BNested {
   var pMainBNested *BNested
   if source != nil {
      var mainBNested BNested
      mainBNested.A = (*source).A
      mainBNested.B = (*source).B
      var pString *string
      if (*source).C != nil {
         xstring := *(*source).C
         pString = &xstring
      }
      mainBNested.C = pString
      var intList []int
      if (*source).D != nil {
         intList = make([]int, len((*source).D))
         for i := 0; i < len((*source).D); i++ {
            intList[i] = (*source).D[i]
         }
      }
      mainBNested.D = intList
      var pStringList []*string
      if (*source).E != nil {
         pStringList = make([]*string, len((*source).E))
         for j := 0; j < len((*source).E); j++ {
            var pString2 *string
            if (*source).E[j] != nil {
               xstring2 := *(*source).E[j]
               pString2 = &xstring2
            }
            pStringList[j] = pString2
         }
      }
      mainBNested.E = pStringList
      pMainBNested = &mainBNested
   }
   return pMainBNested
}

type ANested struct {
   A string
   B int
   C *string
   D []int
   E []*string
}

type A struct {
   Nested *ANested
   A      string
}
type BNested struct {
   A string
   B int
   C *string
   D []int
   E []*string
}
type B struct {
   Nested *BNested
   A      string `json:"A"`
}

// StringPtr returns Pointer of string
func StringPtr(val string) *string {
   return &val
}
func main() {
   // A的与B的的类型不一致
   t1 := A{A: "hello world", Nested: &ANested{
      A: "",
      B: 12,
      C: StringPtr("hello world"),
      D: []int{1, 2, 3, 4, 5},
      E: []*string{StringPtr("hello world"),
         StringPtr("hello world"),
         StringPtr("hello world"),
         StringPtr("hello world"),
         StringPtr("hello world"),
         StringPtr("hello world"),
         StringPtr("hello world")},
   },
   }
   convertByHand(&t1)
   goverterProcess(&t1)
   msgpackProcess(&t1)
   jsonProcess(&t1)
   copierProcess(&t1)
}

func convertByHand(t *A) {
   s1 := time.Now()
   for i := 0; i < 100000; i++ {
      var p1 B
      p1.Nested = &BNested{}
      p1.Nested.A = t.Nested.A
      p1.Nested.B = t.Nested.B
      p1.Nested.C = t.Nested.C
      p1.Nested.D = t.Nested.D
      p1.Nested.E = t.Nested.E
      p1.A = t.A
   }
   d := time.Since(s1)
   fmt.Printf("byhand elapse:%v \n", d)
}

func copierProcess(t *A) {
   s1 := time.Now()
   for i := 0; i < 100000; i++ {
      var p1 B
      _ = copier.Copy(&p1, t)
      //if err != nil {
      // fmt.Printf("err:%v", err)
      //}
   }
   d := time.Since(s1)
   fmt.Printf("copier elapse:%v \n", d)
}

func msgpackProcess(t *A) {
   s1 := time.Now()
   for i := 0; i < 100000; i++ {
      // msgpack序列化方式
      bs1, _ := msgpack.Marshal(t)
      var p1 B
      _ = msgpack.Unmarshal(bs1, &p1)
      // fmt.Printf("p1:[%+v],err:[%+v]", p1, err)
   }
   d := time.Since(s1)
   fmt.Printf("msgpack elapse:%v \n", d)
}

func jsonProcess(t *A) {
   s1 := time.Now()
   for i := 0; i < 100000; i++ {
      // json序列化方式
      bs2, _ := json.Marshal(t)
      var p1 B
      _ = json.Unmarshal(bs2, &p1)
      // fmt.Printf("p1:[%+v],err:[%+v]", p1, err)
   }
   d := time.Since(s1)

   fmt.Printf("json elapse:%v \n", d)
}

func goverterProcess(t *A) {
   c := ConverterImpl{}
   s1 := time.Now()
   for i := 0; i < 100000; i++ {
      // json序列化方式
      _ = c.ConvertA(t)
      // fmt.Printf("p1:[%+v],err:[%+v]", p1, err)
   }
   d := time.Since(s1)

   fmt.Printf("goverter elapse:%v \n", d)
}

在ebian 10 linux 5.4,AMD 16核,64G测试数据:
– byhand elapse:42.608µs
– goverter elapse:64.776683ms
– msgpack elapse:675.421475ms
– json elapse:858.326929ms
– copier elapse:449.271056ms

观察数据,可以发现:
1. 手撸代码最高性能较高,这是因为仅仅是浅拷贝操作,不涉及大量数据拷贝。
2. goverter性能第二,这是因为goverter生成的代码会做deep copy,相比于前者效率稍低。
3. copier的效率性能第三,猜测是因为运行时的大量反射操作影响了效率。
4. 序列化方案效率最低,msgp效率会相对较高。

另外,当转换的数据量(silce,map元素数量)变多时:
1. pointer转换不会增加耗时,因为本质上是指针转换,耗时与数据大小无关。
2. 手撸代码耗时增加较少,浅拷贝不会去拷贝slice,map元素。
3. 其余方案与数据量正相关,耗时会增长。

结论

综上建议:
1. 一般情况下,我们可以选择goverter的方案,平衡开发效率与性能;
2. 极端重性能的情况下,可以考虑手撸转换代码,另外如果能确保内存布局一致的情况下,可以尝试unsafe转换,但需要慎用;
3. 简单测试场景,可以采用序列化/copier方案,开发效率更高;

参考

https://tip.golang.org/doc/go1.8#language
嵌套讨论:https://github.com/golang/go/issues/46205
转换:https://go.dev/ref/spec#Conversion

 

 

本文链接: http://rainlin.top/archives/215
转载请注明转载自: Rainlin Home

发表评论

邮箱地址不会被公开。 必填项已用*标注