Deep Copying Between Different Struct Types in Go

Deep Copying Between Different Struct Types in Go

While refactoring a system, converting entities between layers made deep copying surprisingly awkward. A product VO in the view layer, an entity in the domain layer, and a PO in the persistence layer can look nearly identical, yet small type differences complicate direct conversion. I used reflection to build a general conversion method, reducing repetitive assembler methods and making the code more maintainable and flexible.

I have been swamped with a system refactor lately. The blog has been gathering dust for a while.

One particularly annoying part of the refactor was converting entities between layers of a layered architecture. Take a product: the view layer may have a product VO, the domain layer a product entity or DO (domain object), and the persistence layer a product PO corresponding to a database entity...

Most of these structures are similar; many are almost or entirely identical. Others have tiny differences, such as a field being a pointer in one struct but not in another, which prevents a direct cast. That means writing lots of assembler methods to convert entities, and complicated structures make this sheer hell. Fundamentally, it is just a deep copy that cannot be handled because the types differ.

I wondered whether reflection could solve this with a reasonably general conversion method for this particular situation. An afternoon later, the following code was born:

The core is copyRecursive. It handles deep copies of structs, slices, and maps, including copies from pointer to non-pointer types and vice versa. The only requirement when copying structs is that every field in the destination struct have a field with the same name and underlying type in the source struct, allowing the recursive deep copy to proceed.

I will not explain the code in detail, but I do have to say:

Reflection is fucking awesome.


20220820 update: Copying into structs whose fields do not all exist under the same names in the source is now supported. When there is no matching source field, the destination field receives its zero value: nil for pointers and an empty struct for structs.

A Project That Failed: Seven Not-So-Happy Days over Chinese New Year
How Java’s this Keyword Can Prevent Compile-Time Constant Propagation