Deep Copy in Go: Reflection, JSON, and GOB Techniques Explained

58 views

Performing a deep copy in Go involves creating a new instance of a data structure and copying all data from the original instance to the new one. This ensures that changes to the new instance do not affect the original instance and vice versa. Below is an example demonstrating how to achieve deep copying using reflection, manual copying, and serialization/deserialization methods.

Using Reflection (Generic Deep Copy)

To perform a deep copy using reflection, you can traverse each field of the struct and copy the values recursively:

package main

import (
	"fmt"
	"reflect"
)

// DeepCopy performs a deep copy of the provided object.
func DeepCopy(src interface{}) interface{} {
	if src == nil {
		return nil
	}

	dst := reflect.New(reflect.TypeOf(src)).Elem()
	dst.Set(reflect.ValueOf(src))
	return deepCopyValue(dst).Interface()
}

func deepCopyValue(src reflect.Value) reflect.Value {
	if !src.IsValid() {
		return src
	}

	switch src.Kind() {
	case reflect.Ptr:
		if src.IsNil() {
			return reflect.Zero(src.Type())
		}
		dst := reflect.New(src.Elem().Type())
		dst.Elem().Set(deepCopyValue(src.Elem()))
		return dst
	case reflect.Interface:
		if src.IsNil() {
			return reflect.Zero(src.Type())
		}
		dst := reflect.New(src.Elem().Type()).Elem()
		dst.Set(deepCopyValue(src.Elem()))
		return dst
	case reflect.Struct:
		dst := reflect.New(src.Type()).Elem()
		for i := 0; i < src.NumField(); i++ {
			dst.Field(i).Set(deepCopyValue(src.Field(i)))
		}
		return dst
	case reflect.Slice:
		if src.IsNil() {
			return reflect.Zero(src.Type())
		}
		dst := reflect.MakeSlice(src.Type(), src.Len(), src.Cap())
		for i := 0; i < src.Len(); i++ {
			dst.Index(i).Set(deepCopyValue(src.Index(i)))
		}
		return dst
	case reflect.Map:
		if src.IsNil() {
			return reflect.Zero(src.Type())
		}
		dst := reflect.MakeMapWithSize(src.Type(), src.Len())
		for _, key := range src.MapKeys() {
			dst.SetMapIndex(key, deepCopyValue(src.MapIndex(key)))
		}
		return dst
	default:
		return src
	}
}

type Inner struct {
	Value int
}

type Outer struct {
	Name   string
	Scores []int
	Inner  Inner
}

func main() {
	original := Outer{
		Name:   "Alice",
		Scores: []int{10, 20, 30},
		Inner:  Inner{Value: 42},
	}

	copy := DeepCopy(original).(Outer)

	// Modify the copy
	copy.Name = "Bob"
	copy.Scores[0] = 100
	copy.Inner.Value = 99

	fmt.Println("Original:", original)
	fmt.Println("Copy:    ", copy)
}

Using Serialization/Deserialization (JSON)

Another method is to serialize the object into a format like JSON and then deserialize it back, which ensures a deep copy:

package main

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"fmt"
)

// Using JSON
func DeepCopyJSON(src, dst interface{}) error {
	data, err := json.Marshal(src)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, dst)
}

// Using GOB
func DeepCopyGOB(src, dst interface{}) error {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	dec := gob.NewDecoder(&buf)

	if err := enc.Encode(src); err != nil {
		return err
	}
	return dec.Decode(dst)
}

type Inner struct {
	Value int
}

type Outer struct {
	Name   string
	Scores []int
	Inner  Inner
}

func main() {
	original := Outer{
		Name:   "Alice",
		Scores: []int{10, 20, 30},
		Inner:  Inner{Value: 42},
	}

	// Using JSON
	var copyJSON Outer
	err := DeepCopyJSON(original, &copyJSON)
	if err != nil {
		fmt.Println("Error in JSON DeepCopy:", err)
		return
	}

	// Modify the JSON copy
	copyJSON.Name = "Bob"
	copyJSON.Scores[0] = 100
	copyJSON.Inner.Value = 99

	fmt.Println("Original (JSON):", original)
	fmt.Println("Copy (JSON):    ", copyJSON)

	// Using GOB
	var copyGOB Outer
	err = DeepCopyGOB(original, &copyGOB)
	if err != nil {
		fmt.Println("Error in GOB DeepCopy:", err)
		return
	}

	// Modify the GOB copy
	copyGOB.Name = "Charlie"
	copyGOB.Scores[0] = 200
	copyGOB.Inner.Value = 88

	fmt.Println("Original (GOB):", original)
	fmt.Println("Copy (GOB):    ", copyGOB)
}

Explanation

  1. Reflection Method: The DeepCopy and deepCopyValue functions traverse the object using reflection to duplicate each field appropriately. This method is more complex but provides a generic way to deep copy different types of structures.

  2. Serialization/Deserialization:

    • JSON: DeepCopyJSON uses JSON serialization and deserialization to create a deep copy.
    • GOB: DeepCopyGOB uses the GOB encoder and decoder for binary serialization, which can be more efficient than JSON but may require more setup.

Each method has its pros and cons. Reflection is flexible and works for many types, while JSON and GOB are simpler to use but may have performance overhead. Choose the method that best fits your needs.