# MarshalVT vs MarshalToVT vs MarshalToSizedBufferVT in protobuf-go-lite

> Explore MarshalVT, MarshalToVT, and MarshalToSizedBufferVT in protobuf-go-lite. Understand how each method marshals protobuf data with efficient buffer management.

- Repository: [Aperture Robotics/protobuf-go-lite](https://github.com/aperturerobotics/protobuf-go-lite)
- Tags: deep-dive
- Published: 2026-02-25

---

**`MarshalVT` allocates and returns a new byte slice, `MarshalToVT` writes into a caller-provided buffer that must be at least large enough, and `MarshalToSizedBufferVT` performs the actual reverse-order serialization into a pre-sized buffer.**

The `aperturerobotics/protobuf-go-lite` repository generates three distinct marshaling methods for every protobuf message to support different performance and memory management strategies. Understanding the difference between `MarshalVT`, `MarshalToVT`, and `MarshalToSizedBufferVT` is essential for optimizing serialization throughput and minimizing allocations in Go applications.

## The Three VT Marshaling Methods

All three methods belong to the *VT* (Version-tolerant) marshaling family and share the same low-level serialization logic, but they differ in **who allocates the output buffer** and **how that buffer is populated**:

| Method | Buffer Allocation | What It Does | Return Value | Typical Use Case |
|--------|-------------------|--------------|--------------|------------------|
| `MarshalVT()` | **Allocates** new slice of exact required size | Calls `MarshalToSizedBufferVT` and returns written portion | `([]byte, error)` | General convenience when you need the serialized bytes |
| `MarshalToVT(buf []byte)` | **Caller provides** slice at least as large as message | Computes size, slices buffer to exact length, forwards to `MarshalToSizedBufferVT` | `(int, error)` – bytes written | Reusing buffers from pools to avoid allocations |
| `MarshalToSizedBufferVT(buf []byte)` | **Caller must provide** exactly-sized slice | Writes fields **backwards** from end of slice toward beginning | `(int, error)` – bytes written | Zero-copy scenarios where size is pre-calculated via `SizeVT()` |

## How the Methods Differ

### MarshalVT: The Convenience Method

`MarshalVT` is the highest-level API designed for simplicity. It automatically calculates the required buffer size using `SizeVT()`, allocates a new byte slice of that exact length, and delegates the actual serialization to `MarshalToSizedBufferVT`.

In [`types/pluginpb/plugin.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/pluginpb/plugin.pb.go), the implementation follows this pattern:

```go
func (m *Version) MarshalVT() (dAtA []byte, err error) {
    if m == nil {
        return nil, nil
    }
    size := m.SizeVT()
    dAtA = make([]byte, size)
    n, err := m.MarshalToSizedBufferVT(dAtA[:size])
    if err != nil {
        return nil, err
    }
    return dAtA[:n], nil
}

```

This method is ideal when you simply need the serialized bytes and do not want to manage buffer lifecycle or pooling.

### MarshalToVT: The Reusable Buffer Approach

`MarshalToVT` strikes a balance between convenience and performance. The caller provides a buffer that must be at least as large as the serialized message, but does not need to be exactly sized. The method computes the exact size, slices the provided buffer to that length, and forwards to `MarshalToSizedBufferVT`.

From [`types/pluginpb/plugin.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/pluginpb/plugin.pb.go):

```go
func (m *Version) MarshalToVT(dAtA []byte) (int, error) {
    size := m.SizeVT()
    return m.MarshalToSizedBufferVT(dAtA[:size])
}

```

This approach enables buffer reuse strategies, such as using `sync.Pool` to reduce garbage collection pressure in high-throughput servers.

### MarshalToSizedBufferVT: The Low-Level Core

`MarshalToSizedBufferVT` is the foundational implementation that performs the actual protobuf wire encoding. It requires the caller to provide a buffer already sized to the exact message length (typically determined by `SizeVT()`). The method writes fields in reverse order, starting from the end of the buffer and moving toward the beginning.

The implementation in [`types/pluginpb/plugin.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/pluginpb/plugin.pb.go) demonstrates this reverse-write pattern:

```go
func (m *Version) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
    if m == nil {
        return 0, nil
    }
    i := len(dAtA)
    // ... write fields backward ...
    // example: encode fields from end toward start
    return len(dAtA) - i, nil
}

```

This reverse-order serialization is a performance optimization that allows the method to calculate varint lengths without pre-scanning or temporary buffers.

## Implementation Details

The three methods form a clear hierarchy in the generated code. According to the source in `aperturerobotics/protobuf-go-lite`, `MarshalVT` and `MarshalToVT` are thin wrappers around `MarshalToSizedBufferVT`, which contains the actual encoding logic using helpers like `EncodeVarint` defined in [`protobuf-go-lite.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/protobuf-go-lite.go).

The "VT" suffix indicates these methods belong to the version-tolerant marshaling family, as noted in [`internal/weakdeps/weakdeps.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/internal/weakdeps/weakdeps.go). Each message type generated in the repository—including wrapper types like `DoubleValue` in [`types/known/wrapperspb/wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/wrapperspb/wrappers.pb.go)—implements this three-method pattern.

## Practical Code Examples

### Simple Marshaling with Allocation

Use `MarshalVT` when you need the serialized bytes without managing buffer pools:

```go
msg := &pluginpb.Version{
    Major: proto.Int32(1),
    Minor: proto.Int32(2),
}
data, err := msg.MarshalVT()
if err != nil {
    log.Fatalf("marshal error: %v", err)
}
fmt.Printf("Encoded %d bytes\n", len(data))

```

### Reusing Buffers to Avoid Allocations

Use `MarshalToVT` with a pre-allocated buffer to reduce GC pressure:

```go
msg := &wrapperspb.DoubleValue{Value: 3.14}
size := msg.SizeVT()
buf := make([]byte, size)  // Could come from sync.Pool
n, err := msg.MarshalToVT(buf)
if err != nil {
    log.Fatalf("marshal error: %v", err)
}
encoded := buf[:n]

```

### Zero-Copy Marshaling with Exact Sizing

Use `MarshalToSizedBufferVT` when you have already calculated the size and want maximum performance:

```go
msg := &pluginpb.CodeGeneratorRequest{}
size := msg.SizeVT()
buf := make([]byte, size) // Must be exactly sized
n, err := msg.MarshalToSizedBufferVT(buf)
if err != nil {
    log.Fatalf("marshal error: %v", err)
}
// n == size, written backward from end of buffer

```

## Key Source Files

| File | Purpose |
|------|---------|
| [`types/pluginpb/plugin.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/pluginpb/plugin.pb.go) | Generated implementations of `Version`, `CodeGeneratorRequest`, and other plugin types showing the three-method pattern. |
| [`types/known/wrapperspb/wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/wrapperspb/wrappers.pb.go) | Wrapper types like `DoubleValue` demonstrating the same marshaling hierarchy. |
| [`protobuf-go-lite.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/protobuf-go-lite.go) | Core utilities including `SizeVT` calculation and `EncodeVarint` used by the sized buffer writer. |
| [`internal/weakdeps/weakdeps.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/internal/weakdeps/weakdeps.go) | Documentation of the VT (version-tolerant) method family and conditional compilation. |

## Summary

- **`MarshalVT`** allocates a correctly-sized buffer internally and returns the serialized bytes, offering the simplest API for general use.
- **`MarshalToVT`** accepts a caller-provided buffer that must be at least large enough, slices it to the exact size, and marshals into it—ideal for buffer pooling strategies.
- **`MarshalToSizedBufferVT`** is the core implementation that requires an exactly-sized buffer and writes fields in reverse order, providing maximum performance when the message size is pre-calculated via `SizeVT`.

## Frequently Asked Questions

### When should I use MarshalVT versus MarshalToVT?

Use **`MarshalVT`** when you need a simple, allocation-per-call approach and do not manage buffer pools. Use **`MarshalToVT`** when optimizing for high-throughput scenarios where you can reuse buffers from a `sync.Pool` or other allocation strategy to reduce garbage collection pressure.

### What happens if I pass an undersized buffer to MarshalToSizedBufferVT?

**`MarshalToSizedBufferVT`** assumes the provided buffer has exactly the length returned by `SizeVT()`. Passing an undersized buffer will cause the method to write beyond the slice bounds, resulting in a runtime panic. Always verify buffer length matches `SizeVT()` before calling this method.

### Are there strict versions of these marshaling methods?

Yes. The repository generates **`MarshalVTStrict`**, **`MarshalToVTStrict`**, and **`MarshalToSizedBufferVTStrict`** for every message. These methods follow the identical buffer management contracts but enforce stricter protobuf validation rules during serialization, such as checking for required fields or valid UTF-8 strings.

### Why does MarshalToSizedBufferVT write fields backwards?

Writing fields in **reverse order** (from the end of the buffer toward the beginning) is a performance optimization that eliminates the need for temporary buffers or pre-scanning to calculate variable-length integer (varint) sizes. This approach allows the marshaller to encode length-delimited fields directly into their final position without shifting data afterward.