parco
// binary serialization for Go

You already know the schema.
Stop paying for it on the wire.

Parco is a binary serializer built on generics: no reflection, no codegen, no struct tags, zero core dependencies. You describe the layout with a builder; parco reads and writes exactly those bytes.

The honest part: it's schema-driven, like Protobuf and unlike JSON. Both ends must agree on the layout beforehand. That trade is where the speed comes from — not benchmark tricks.

$ go get github.com/sonirico/parco
type Player struct { Name string // SmallVarchar Score uint32 // little-endian } p := Player{Name: "Ana", Score: 7}
└─ compiler.Compile(p, w) ─→ 8 bytes
03 41 6E 61 07 00 00 00
length header (uint8) payload — "Ana" + uint32 LE
Same value as JSON: 24 bytes. As parco: 8.
0x00 features

What you get

No reflection

Fields are described with a fluent builder and generics. The compiler knows every type at build time; the runtime never inspects one.

Composable types

Primitives, slices, maps, arrays, nested structs, optionals. Build a type once, reuse it anywhere — standalone or inside another builder.

Multi-model streams

Register models under a type ID and parse or compile mixed messages from one reader — or use the global registry.

You control the layout

Choose byte order per field and header width per collection: a UInt8Header costs one byte for up to 255 elements.

Allocation-conscious

Pooled buffers on the hot path, hardened against lying length headers: memory grows with data that actually arrives.

Zero core dependencies

The core API is stdlib only. No runtime library, no generated files to keep in sync, nothing to version but your own code.

0x10 quick start

A model in one expression

type Point struct { X, Y int32 }

factory := parco.ObjectFactory[Point]()
parser, compiler := parco.Builder[Point](factory).
    Int32(binary.LittleEndian,
        func(p *Point) int32 { return p.X },
        func(p *Point, x int32) { p.X = x },
    ).
    Int32(binary.LittleEndian,
        func(p *Point) int32 { return p.Y },
        func(p *Point, y int32) { p.Y = y },
    ).
    Parco()

var buf bytes.Buffer
err := compiler.Compile(Point{X: 1, Y: 2}, &buf)  // 8 bytes on the wire
point, err := parser.Parse(&buf)                  // {1 2}

Getter compiles, setter parses. The full tour — slices, maps, optionals, nested structs, the model registry — lives in the README.

0x20 etymology

The name, decoded

parser + compiler = parco

Two halves, same builder: every model you describe yields its parser and its compiler from one definition.

0x30 trade-offs

The trade, stated plainly

Reach for parco when

  • You control both ends of the wire (shared schema)
  • Bandwidth and latency are budget lines, not footnotes
  • You want compile-time safety instead of struct tags
  • IoT, game servers, tight microservice links
  • Predictable memory beats convenience

Use something else when

  • Strangers consume your API — give them JSON
  • The schema changes faster than you can version it
  • You need cross-language support — that's Protobuf
  • You're debugging by reading payloads with your eyes

Parco is Go-only and will stay that way. If Protobuf already works for you, keep it.

0x40 benchmarks

Measured, not promised

Headline numbers from the reference suite (see PERFORMANCE.md for methodology and honest caveats):

vsspeedpayload sizeallocations
encoding/json2–3× faster~70% smaller~10× fewer
msgpackcomparable~40% smallerfewer

Continuous results land on the benchmark dashboard after each push to main.