Copying Structs in Go: Value Copy and Deep Copy Implementation

Dec 06, 2025 · Programming · 14 views · 7.8

Keywords: Go | struct copying | deep copy

Abstract: This article delves into the copying mechanisms of structs in Go, explaining the fundamentals of value copy for structs containing only primitive types. Through concrete code examples, it demonstrates how shallow copying is achieved via simple assignment and analyzes why manual deep copy implementation is necessary when structs include reference types (e.g., slices, pointers) to avoid shared references. The discussion also addresses potential semantic confusion from testing libraries and provides practical recommendations for managing memory addresses and data independence effectively.

Fundamentals of Struct Copying in Go

In Go, the copying behavior of a struct depends on the types of its fields. For structs containing only primitive types, Go employs a value copy mechanism. This means that through a simple assignment operation, a new memory instance is created, and all field values from the original struct are copied individually to the new instance, ensuring that two variables point to different memory addresses while initially having identical field values.

Practical Example of Value Copy

The following code illustrates a typical value copy scenario:

type Person struct {
    Name string
    Age  int
}

alice1 := Person{"Alice", 30}
alice2 := alice1
fmt.Println(alice1 == alice2)   // Outputs true, field values are equal
fmt.Println(&alice1 == &alice2) // Outputs false, memory addresses differ

alice2.Age += 10
fmt.Println(alice1 == alice2)   // Outputs false, field values differ after modification

In this example, alice2 := alice1 performs a complete value copy. Initially, alice1 and alice2 have identical field values, but since they are stored at different memory locations, address comparison yields false. Subsequent modification of alice2.Age affects only the new instance, not alice1, verifying the independence of the copy.

Challenges with Reference-Type Fields and Deep Copy Necessity

When a struct includes reference-type fields (e.g., slices, maps, pointers, or arrays), a simple assignment copies only the references (i.e., memory addresses) of these fields, not the underlying data. This leads to multiple struct instances sharing the same data source, where modifications to any instance may affect others, compromising data isolation.

For example, consider this struct:

type Employee struct {
    ID   int
    Tags []string  // Slice field
    Info *Details  // Pointer field
}

If emp2 := emp1 is executed, emp2.Tags and emp2.Info will point to the same underlying slice and pointer objects as those in emp1. Consequently, modifications to emp2.Tags (e.g., appending elements) will be reflected in emp1.Tags, as both reference the same slice header.

Strategies for Implementing Deep Copy

Go's standard library does not provide built-in deep copy functionality, so developers must implement it manually based on specific contexts. Common approaches include:

  1. Custom Copy Functions: Write dedicated deep copy methods for each struct, explicitly copying each field and recursively creating new instances for reference-type fields. For example:
    func (e *Employee) DeepCopy() Employee {
        newTags := make([]string, len(e.Tags))
        copy(newTags, e.Tags)
        newInfo := &Details{*e.Info} // Assuming Details is copyable
        return Employee{e.ID, newTags, newInfo}
    }
  2. Leveraging Serialization and Deserialization: Convert the struct to a byte stream via JSON or gob encoding, then decode it into a new instance. This method is simple and generic but may impact performance and requires all fields to be serializable.
    import "encoding/json"
    
    func DeepCopyViaJSON(src, dst interface{}) error {
        data, err := json.Marshal(src)
        if err != nil {
            return err
        }
        return json.Unmarshal(data, dst)
    }
  3. Using Third-Party Libraries: Community-provided tools (e.g., copier or deepcopy) often automate nested struct handling through reflection, simplifying development workflows.

Common Pitfalls in Testing and Solutions

In the original problem, test failure might stem from semantic confusion in testing libraries. Certain assertion functions (e.g., assert.NotEqual) may default to value comparison rather than address comparison when evaluating structs. To correctly verify memory independence, use Go's built-in address operator directly:

aa := a
if &a == &aa {
    t.Error("Copied items should not be the same object.")
}

This approach explicitly checks pointer equality, avoiding ambiguities that library functions might introduce.

Summary and Best Practices

The key to understanding struct copying in Go lies in distinguishing between value-type and reference-type behaviors. For structs with pure value-type fields, simple assignment suffices for safe copying; once reference-type fields are involved, deep copy must be considered to prevent unintended data sharing. In practice, it is recommended to:

By mastering these mechanisms, developers can manage memory and data flow more effectively, building robust and maintainable Go applications.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.