# How MarshalVT and UnmarshalVT Methods Work Without Reflection in protobuf-go-lite

> Discover how MarshalVT and UnmarshalVT in protobuf-go-lite use direct field access and low-level binary operations to achieve reflection-free, compile-time serialization for faster protobuf decoding.

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

---

**The `protobuf-go-lite` generator produces static, compile-time determined serialization routines that encode and decode protobuf messages using direct field access and low-level binary operations, completely eliminating Go's reflection machinery.**

The `aperturerobotics/protobuf-go-lite` repository provides a lightweight alternative to the standard Go protobuf runtime. Unlike generic serialization libraries that rely on `reflect.TypeOf` or `reflect.Value`, the generated `MarshalVT` and `UnmarshalVT` methods use static code generation to achieve high-performance, reflection-free protobuf encoding.

## Static Code Generation Strategy

The `protobuf-go-lite` tool generates explicit marshalling and unmarshalling routines for every protobuf message. These routines are **completely static**—they know the exact memory layout of each message at compile time and therefore never invoke Go’s reflection package.

The generated code follows the same architectural pattern used by the official `google.golang.org/protobuf` generator, but it is trimmed down to the essentials needed by the “lite” runtime. This approach allows the compiler to inline hot paths and optimize the serialization logic as if it were hand-written.

## Step-by-Step MarshalVT Implementation

### Size Calculation with SizeVT()

Before allocating memory, `MarshalVT()` determines the exact serialized size by calling `SizeVT()`. This generated method walks through every field in the message and accumulates the wire-encoded size of each element, including varints, fixed-size fields, and length-delimited payloads.

In [`types/known/wrapperspb/wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/wrapperspb/wrappers.pb.go), the `SizeVT()` method for `DoubleValue` calculates the size of the `Value` field plus any unknown fields:

```go
func (m *DoubleValue) SizeVT() (n int) {
    if m == nil {
        return 0
    }
    var l int
    _ = l
    if m.Value != 0 {
        n += 9 // 1 byte tag + 8 bytes fixed64
    }
    l = len(m.unknownFields)
    n += l
    return n
}

```

### Buffer Allocation and Reverse Encoding

`MarshalVT()` allocates a byte slice of the exact length returned by `SizeVT()` and delegates to `MarshalToSizedBufferVT`. This method implements **reverse-order encoding**: it starts at the end of the buffer and writes fields backwards toward the beginning.

This technique eliminates the need for temporary buffers or extra copies. Because the encoder writes from the end, it can immediately return `len(dAtA) - i` as the number of bytes written, where `i` is the final position index.

In [`wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/wrappers.pb.go), the `MarshalToSizedBufferVT` method for `DoubleValue` demonstrates this pattern:

```go
func (m *DoubleValue) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
    i := len(dAtA)
    _ = i
    var l int
    _ = l
    if len(m.unknownFields) > 0 {
        i -= len(m.unknownFields)
        copy(dAtA[i:], m.unknownFields)
    }
    if m.Value != 0 {
        i -= 8
        binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(m.Value)))
        i--
        dAtA[i] = 0x9 // tag (field 1, wire type 1)
    }
    return len(dAtA) - i, nil
}

```

### Low-Level Encoding Primitives

The generated code relies on a small set of optimized helper functions from the lite runtime rather than reflection:

- **Fixed-size fields** (`float64`, `float32`, `uint64`, etc.) use `binary.LittleEndian.PutUint64` or `PutUint32` for direct byte conversion.
- **Varint fields** use `EncodeVarint` from [`protobuf-go-lite.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/protobuf-go-lite.go) to write variable-length integers.
- **Length-delimited fields** encode the length as a varint followed by the raw payload bytes.
- **Unknown fields** are appended unchanged to preserve forward compatibility.

The `EncodeVarint` implementation in [`protobuf-go-lite.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/protobuf-go-lite.go) handles the variable-length encoding without any type introspection:

```go
func EncodeVarint(x uint64) []byte {
    var buf [10]byte
    n := binary.PutUvarint(buf[:], x)
    return buf[:n]
}

```

## UnmarshalVT Implementation Details

### Forward Parsing Without Reflection

`UnmarshalVT` walks the input byte slice sequentially, decoding wire-format tags and values. For each field number encountered, the generated code contains a direct switch case that writes the decoded value into the corresponding struct field using simple assignment.

In [`wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/wrappers.pb.go), the `UnmarshalVT` method for `DoubleValue` parses the wire format directly:

```go
func (m *DoubleValue) UnmarshalVT(dAtA []byte) error {
    l := len(dAtA)
    iNdEx := 0
    for iNdEx < l {
        preIndex := iNdEx
        var wire uint64
        for shift := uint(0); ; shift += 7 {
            if shift >= 64 {
                return ErrIntOverflow
            }
            if iNdEx >= l {
                return io.ErrUnexpectedEOF
            }
            b := dAtA[iNdEx]
            iNdEx++
            wire |= uint64(b&0x7F) << shift
            if b < 0x80 {
                break
            }
        }
        fieldNum := int32(wire >> 3)
        wireType := int(wire & 0x7)
        if wireType == 4 {
            return fmt.Errorf("proto: DoubleValue: wiretype end group for non-group")
        }
        if fieldNum <= 0 {
            return fmt.Errorf("proto: DoubleValue: illegal tag %d (wire type %d)", fieldNum, wireType)
        }
        switch fieldNum {
        case 1:
            if wireType != 1 {
                return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
            }
            if iNdEx+8 > l {
                return io.ErrUnexpectedEOF
            }
            v := uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:]))
            m.Value = math.Float64frombits(v)
            iNdEx += 8
        default:
            iNdEx = preIndex
            skippy, err := skip(dAtA[iNdEx:])
            if err != nil {
                return err
            }
            if (skippy < 0) || (iNdEx+skippy) < 0 {
                return ErrInvalidLength
            }
            if (iNdEx + skippy) > l {
                return io.ErrUnexpectedEOF
            }
            m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
            iNdEx += skippy
        }
    }
    if iNdEx > l {
        return io.ErrUnexpectedEOF
    }
    return nil
}

```

### Handling Unknown Fields

When `UnmarshalVT` encounters a field number not recognized in the switch statement, it captures the raw bytes into the `unknownFields` slice. This preserves forward compatibility without requiring reflection to determine the field type.

### Unsafe Variant for Performance

For scenarios where the caller guarantees a well-formed payload, `UnmarshalVTUnsafe` provides a variant that skips bounds checking and nil checks. This method uses the same parsing logic but removes safety overhead for maximum throughput.

## Complete Working Example

The following example demonstrates round-trip serialization using the generated `MarshalVT` and `UnmarshalVT` methods without any reflection:

```go
package main

import (
	"fmt"
	"log"

	// import a generated message type
	"github.com/aperturerobotics/protobuf-go-lite/types/known/wrapperspb"
)

func main() {
	// Build a message
	m := &wrapperspb.Int64Value{Value: 42}

	// ---------- Marshal ----------
	// Serialize without reflection
	b, err := m.MarshalVT()
	if err != nil {
		log.Fatalf("marshal error: %v", err)
	}
	fmt.Printf("binary: %x\n", b)

	// ---------- Unmarshal ----------
	// Decode back into a fresh struct
	var decoded wrapperspb.Int64Value
	if err := decoded.UnmarshalVT(b); err != nil {
		log.Fatalf("unmarshal error: %v", err)
	}
	fmt.Printf("decoded: %+v\n", decoded)
}

```

Running this program outputs the varint-encoded protobuf payload (`08 2a`) and confirms the original value round-trips correctly, all without invoking Go's reflection machinery.

## Key Source Files and Architecture

The reflection-free serialization layer is implemented across these critical files:

- **[`protobuf-go-lite.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/protobuf-go-lite.go)** — Core runtime helpers including `EncodeVarint`, `ConsumeVarint`, and `DecodeVarint` functions that handle wire-format encoding without type introspection.

- **[`types/known/wrapperspb/wrappers.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/wrapperspb/wrappers.pb.go)** — Example generated code demonstrating `MarshalVT`, `MarshalToSizedBufferVT`, `UnmarshalVT`, `UnmarshalVTUnsafe`, and `SizeVT` implementations for wrapper types.

- **[`generator/generator.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/generator/generator.go)** — The code generator that emits the `MarshalVT` and `UnmarshalVT` methods into [`.pb.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/.pb.go) files during the protobuf compilation phase.

- **[`features/marshal/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/features/marshal/marshal.go)** — Registers the marshal feature with the generator and integrates the generated `MarshalVT` methods into the public API surface.

Together, these components form a fully static serialization layer that mimics the full protobuf runtime while minimizing binary size and eliminating reflection overhead.

## Summary

- **Static Generation**: `protobuf-go-lite` generates explicit marshalling code for each message at compile time, eliminating runtime reflection.
- **Reverse Encoding**: `MarshalToSizedBufferVT` writes fields backwards from the end of a pre-allocated buffer, avoiding temporary copies.
- **Direct Field Access**: `UnmarshalVT` uses switch statements on field numbers to write directly into struct fields without type introspection.
- **Zero Reflection**: No usage of `reflect.TypeOf`, `reflect.Value`, or `protoimpl.X` helpers occurs in the generated code or lite runtime.

## Frequently Asked Questions

### Why does protobuf-go-lite avoid reflection in MarshalVT and UnmarshalVT?

Reflection in Go requires runtime type inspection through `reflect.Value` and `reflect.Type`, which adds significant CPU overhead and prevents compiler inlining optimizations. By generating static code that knows the exact field layout at compile time, `protobuf-go-lite` achieves near hand-written performance while maintaining a minimal runtime footprint.

### How does reverse-order encoding improve performance in MarshalVT?

`MarshalToSizedBufferVT` writes fields starting from the end of the pre-allocated byte slice and moving backwards toward the beginning. This technique eliminates the need for intermediate buffers or memory copies when handling length-delimited fields, and allows the function to return the number of bytes written by simply calculating `len(dAtA) - i` where `i` is the final position index.

### What is the difference between UnmarshalVT and UnmarshalVTUnsafe?

`UnmarshalVT` includes bounds checking, nil pointer checks, and validation of wire types to ensure safe parsing of arbitrary input. `UnmarshalVTUnsafe` provides the same parsing logic but removes these safety checks, offering maximum throughput when the caller can guarantee that the input buffer is well-formed and properly bounded.

### Can I use MarshalVT and UnmarshalVT with standard protobuf messages?

These methods are specifically generated by the `protobuf-go-lite` compiler for messages defined in that ecosystem. While the wire format is compatible with standard Protocol Buffers, the generated `MarshalVT` and `UnmarshalVT` methods themselves are only available on message types generated by the `protobuf-go-lite` tool, not on messages generated by the standard `protoc-gen-go`.