2
0

More than 3 years have passed since last update.

copier.Copyで変更できない構造体間のコピーには中間構造体を用いるとよい

Posted at

対処方法

package main

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

// 変更してはいけない START

type Domain struct {
    Name string
    Age int
    Weight float32
    BodyHeight float32

}

type OutSider struct {
    Name string
    Age int64
    BodyWeight float64
    Height *OutSiderHeight
}


type OutSiderHeight struct {
    Height float64
    BodyHeight float64
}

// 変更してはいけない END

// ポイントはコピー先と全く同じ名前、型の構造体にすること
type Middle struct {
    Name string
    Age int
    Weight float32 `copier:"BodyWeight"`
    BodyHeight float32 `copier:"-"`
}

func(m *Middle) Height(h *OutSiderHeight) {
    if h != nil {
        m.BodyHeight = float32(h.BodyHeight)
    }
}


func main() {
    var err error
    outSider := &OutSider{
        Name: "sasakuna",
        Age: 30,
        BodyWeight: 23.95,
        Height: &OutSiderHeight{
            Height: 168.9,
            BodyHeight: 99.999,
        },
    }
    domain1 := Domain{}
    domain2 := Domain{}

    // このままだと`Weight`と`BodyHeight`がコピーできない
    err = copier.Copy(&domain1, outSider)
    fmt.Println("Domain: ", domain1)
    fmt.Println("Error: ", err)

    // 構造体をほかのものと見せかけることでタグを外付けできる
    err = copier.Copy((*Middle)(&domain2), outSider)
    fmt.Println("Domain: ", domain2)
    fmt.Println("Error: ", err)
}

PlayGroundで試す

もう少し詳しく

対処できないケースがあります

以下のケースでは、DomainHeightがコピーされません。
ポイントは、変更してはいけない構造体間で同じ名前なのにcopierでは自動的に変換できないケースです。
特に記載はしませんが一応二つ中間構造体を用意し、Copyを二回行えばできなくはないです。

package main

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

// 変更してはいけない START

type Domain struct {
    Name string
    Age int
    Weight float32
    Height float32

}

type OutSider struct {
    Name string
    Age int64
    BodyWeight float64
    Height *OutSiderHeight
}


type OutSiderHeight struct {
    Height float64
    BodyHeight float64
}

// 変更してはいけない END


type Middle struct {
    Name string
    Age int
    Weight float32 `copier:"BodyWeight"`
    Height float32 `copier:"Height"`
}


func main() {
    var err error
    outSider := &OutSider{
        Name: "sasakuna",
        Age: 30,
        BodyWeight: 23.95,
        Height: &OutSiderHeight{
            Height: 168.9,
            BodyHeight: 99.999,
        },
    }
    domain1 := Domain{}
    domain2 := Domain{}

    err = copier.Copy(&domain1, outSider)
    fmt.Println("Domain: ", domain1)
    fmt.Println("Error: ", err)

    err = copier.Copy((*Middle)(&domain2), outSider)
    fmt.Println("Domain: ", domain2)
    fmt.Println("Error: ", err)
}

PlayGroundで試す

さらに詳しく

内容自体は以上となっていますが、なぜこのようなことをしようとしたかについて説明していきたいと思います。

自動生成コードと依存関係問題の板挟み

依存方向を統一したコードを書こうとすると、必ずデータのパース処理というものが発生してくるかと思います。

その場合、まず変換を行うような層であれば、ドメインを変換することは基本的に許されないかと思います。またこれを許可した場合であっても、今回の例でいうところのOutSiderにあたる外部の構造体は複数存在することが予測され柔軟に対応することはできません。

ならばOutSiderをと思っても、これらは大抵外部のライブラリで変更することはかないません。(今回はGrpcを使ってみていたのですが、これも自動生成なので変更は容易ではありません。)

そこで、今回はGoの名前の違う構造体であっても、名前と型が一致していれば直接変換可能な特性を生かして可能な限り簡単に変換できる方法を提案してみました。

実際のところ

とはいえ今回の手法は、構造体のネストがあまり深くないからこそ実現できたものです。
結局、ネストが深いと手間がかかることは避けられなそうなのですが、そのあたりの調査はまた別の機会に行いたいと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0