How to Use the CloneVT Method for Deep Copying Protobuf Messages in Go

The protobuf-go-lite library automatically generates a CloneVT() method for every message type that creates a complete deep copy, recursively cloning all nested messages, slices, maps, and byte arrays while leaving the original untouched.

The aperturerobotics/protobuf-go-lite repository provides a lightweight Protocol Buffers implementation for Go that generates efficient CloneVT methods for deep copying. Unlike standard library approaches that might share underlying arrays or maps, the generated CloneVT method ensures complete independence between the original and copied messages by recursively cloning all reference types.

What Is the CloneVT Method?

CloneVT is a code-generated method created by the "clone" feature in protobuf-go-lite. When you compile your .proto files using this library, every message struct receives two related methods:

  • CloneVT() *Message – Returns a concrete pointer type with a complete deep copy of all fields.
  • CloneMessageVT() protobuf_go_lite.CloneMessage – Returns the generic CloneMessage interface, useful for polymorphic code that handles multiple message types.

The method handles scalar fields (integers, floats, strings, bools) with direct assignment, while reference fields (nested messages, slices, maps, bytes, and oneof fields) are recursively cloned to prevent shared mutable state.

How CloneVT Works Under the Hood

The generator logic resides in features/clone/clone.go, specifically in the generateCloneMethodsForMessage function (lines 34-40) and the core cloning routine in the body function (lines 45-103).

The generated code follows a consistent pattern for every message type:

  1. Nil safety checkif m == nil { return (*Message)(nil) } ensures safe handling of nil receivers.
  2. Allocationr := new(Message) creates a fresh struct instance.
  3. Scalar copying – Direct assignment (r.Field = m.Field) for primitive types.
  4. Reference cloning – Recursive calls to CloneVT() for nested messages, slices.Clone() for repeated scalar fields, and manual loops with element cloning for repeated messages or maps.
  5. Unknown fields preservationslices.Clone(m.unknownFields) ensures wire-format data is retained.
  6. Returnreturn r provides the independent copy.

Field Handling Categories

The generator categorizes fields into three types for optimal cloning performance:

  • Singular message fields – Calls rhs.CloneVT() recursively.
  • Repeated scalar fields – Uses slices.Clone(rhs) since scalar elements are immutable values.
  • Repeated message or map fields – Iterates with a loop, cloning each element individually (lines 101-108 of clone.go).

Practical Examples

Basic Deep Copy with Timestamp

The Timestamp message in types/known/timestamppb/timestamp.pb.go (lines 43-53) demonstrates a simple implementation:

import (
    "fmt"
    "github.com/aperturerobotics/protobuf-go-lite/types/known/timestamppb"
)

func exampleDeepCopy() {
    // Original message
    orig := &timestamppb.Timestamp{
        Seconds: 1620000000,
        Nanos:   123456789,
    }

    // Deep copy – a completely independent instance
    copy := orig.CloneVT()

    // Modifying the copy does not affect the original
    copy.Nanos = 0
    fmt.Println(orig.Nanos) // 123456789
    fmt.Println(copy.Nanos) // 0
}

Cloning Nested Messages and Maps

For complex messages like Struct in types/known/structpb/struct.pb.go (lines 477-495 for Struct, lines 498-514 for Value), the method handles recursive cloning:

import (
    "fmt"
    pb "github.com/aperturerobotics/protobuf-go-lite/types/known/structpb"
)

func cloneComplex() {
    // Build a nested structure
    orig := &pb.Struct{
        Fields: map[string]*pb.Value{
            "name": {Kind: &pb.Value_StringValue{StringValue: "Alice"}},
            "age":  {Kind: &pb.Value_NumberValue{NumberValue: 30}},
        },
    }

    // Deep copy
    dup := orig.CloneVT()

    // Change a nested value – original stays unchanged
    dup.Fields["name"].Kind = &pb.Value_StringValue{StringValue: "Bob"}
    fmt.Println(orig.Fields["name"].GetStringValue()) // Alice
    fmt.Println(dup.Fields["name"].GetStringValue()) // Bob
}

Using the Generic CloneMessageVT Interface

For polymorphic code, use CloneMessageVT() which returns the protobuf_go_lite.CloneMessage interface. This is implemented in types/known/anypb/any.pb.go (around line 56):

import (
    "fmt"
    "github.com/aperturerobotics/protobuf-go-lite"
    anypb "github.com/aperturerobotics/protobuf-go-lite/types/known/anypb"
)

func genericClone(msg protobuf_go_lite.Message) protobuf_go_lite.CloneMessage {
    // All generated messages implement CloneMessageVT()
    return msg.CloneMessageVT()
}

func demo() {
    src := &anypb.Any{TypeUrl: "type.googleapis.com/example.Foo", Value: []byte{0x01, 0x02}}
    cloned := genericClone(src).(*anypb.Any) // type-assert back to concrete type
    fmt.Printf("%+v\n", cloned)
}

Summary

  • CloneVT() generates a complete deep copy of any protobuf message, recursively cloning nested structures, maps, slices, and byte arrays.
  • The generator code lives in features/clone/clone.go, creating both CloneVT() (concrete return type) and CloneMessageVT() (generic interface) methods.
  • Scalar fields copy by value, while reference types use slices.Clone, maps.Clone, or recursive CloneVT calls to ensure independence.
  • The method safely handles nil receivers and preserves unknown fields during cloning.

Frequently Asked Questions

What is the difference between CloneVT and proto.Clone?

CloneVT is a statically generated method specific to protobuf-go-lite that returns a concrete type (*MyMessage), enabling compile-time type safety and avoiding reflection. Standard proto.Clone from google.golang.org/protobuf/proto uses reflection and returns the generic proto.Message interface, which requires type assertions and incurs runtime overhead.

Does CloneVT handle nil receivers safely?

Yes. Every generated CloneVT implementation begins with a nil check: if m == nil { return (*Message)(nil) }. This ensures that calling CloneVT() on a nil pointer returns nil rather than causing a panic, making the method safe to use in chains or conditional logic.

Are unknown fields preserved during cloning?

Yes. The generator explicitly clones the unknownFields byte slice using slices.Clone(m.unknownFields) when the slice is non-empty. This preserves any unrecognized protobuf wire-format data encountered during deserialization, ensuring that round-tripping through CloneVT does not lose information.

Can I use CloneVT with generic code?

Yes, through the CloneMessageVT() method. While CloneVT() returns a concrete pointer type, CloneMessageVT() returns the protobuf_go_lite.CloneMessage interface. This allows you to write generic functions that can clone any message type without knowing the concrete type at compile time, though you may need to type-assert the result to access specific fields.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →