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 genericCloneMessageinterface, 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:
- Nil safety check –
if m == nil { return (*Message)(nil) }ensures safe handling of nil receivers. - Allocation –
r := new(Message)creates a fresh struct instance. - Scalar copying – Direct assignment (
r.Field = m.Field) for primitive types. - 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. - Unknown fields preservation –
slices.Clone(m.unknownFields)ensures wire-format data is retained. - Return –
return rprovides 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 := ×tamppb.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 bothCloneVT()(concrete return type) andCloneMessageVT()(generic interface) methods. - Scalar fields copy by value, while reference types use
slices.Clone,maps.Clone, or recursiveCloneVTcalls to ensure independence. - The method safely handles
nilreceivers 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →