Go
ZAP Go SDK — the canonical zero-copy wire runtime, capability layer, and zapgen RPC codegen.
Go Binding
github.com/zap-proto/go is the canonical
Go runtime for the ZAP wire format. It is pure standard library — zero external
dependencies — and is the reference every other language runtime is checked
against: a buffer built here parses unchanged through the TypeScript and Python
runtimes, and vice versa (pinned by a shared golden vector).
The module is the read side (Parse / Message / Object / List), the write
side (Builder / ObjectBuilder / ListBuilder), the capability runtime
(cap), the RPC call envelope (rpc), and the schema compiler (cmd/zapgen).
It is not a network library — listeners, transports, and handshakes live
downstream; the rpc package defines the call envelope (the bytes), and the
generated client takes a channel you supply.
Installation
go get github.com/zap-proto/[email protected]The schema compiler installs as a separate binary:
go install github.com/zap-proto/go/cmd/[email protected]Round-trip
Build a message, then parse it back zero-copy. StartObject lays out a struct's
fixed section; Parse validates the header and Root() returns an Object that
reads fields straight out of the buffer — no decode step, no allocation.
package main
import (
"fmt"
zap "github.com/zap-proto/go"
)
func main() {
// Write: a struct with a uint32 at offset 0 and a text field at offset 8.
b := zap.NewBuilder(256)
ob := b.StartObject(16) // 16-byte fixed section
ob.SetUint32(0, 0xDEADBEEF)
ob.SetText(8, "zap")
ob.FinishAsRoot()
buf := b.Finish() // []byte — the ZAP wire message
// Read: zero-copy. The Message aliases buf; nothing is deserialized.
msg, err := zap.Parse(buf)
if err != nil {
panic(err)
}
root := msg.Root()
fmt.Printf("%#x %q\n", root.Uint32(0), root.Text(8)) // 0xdeadbeef "zap"
}Field offsets are explicit here to show the runtime, but in real code you don't
write them by hand — zapgen generates a typed View + Builder per struct from a
.zap schema (see Codegen below). The header is a fixed 16 bytes
(ZAP\x00 magic, version, flags, root offset, size); NewBuilder emits
version 1, and Parse accepts version 1 and 2.
Codegen
zapgen reads a .zap schema and emits, per struct, a zero-copy View +
Input/New builder, and per interface, a typed RPC client, an abstract
ordinal-dispatch server, and a 1-based method-ordinal table over the rpc
envelope. Both schema forms — brace and whitespace-significant — go through one
parser.
zapgen schema.zap # emit one <struct>_zap.go per struct, next to the input
zapgen -out ./gen schema.zap # emit into the given directory
zapgen -single schema.zap # emit one combined <schema>_zap.goDrop a directive at the top of the consuming package and run go generate ./...:
//go:generate zapgen schema.zapFor this schema:
struct Ping
Seq u64
struct Pong
Seq u64
interface Echo
ping(req: Ping) returns (resp: Pong)
notify(req: Ping)
health() returns (resp: Pong)
shutdown()zapgen emits NewPing(PingInput{Seq: 41}) (build → []byte) and
WrapPing(b) (parse → typed Ping view with a Seq() getter), plus the Echo
client/server described under RPC. Method ordinals auto-assign
1, 2, 3, … in declaration order, so appending a method never renumbers the
existing ones — wire compatibility is preserved.
RPC
An interface becomes a typed client and an abstract server, carried over the
wire by the fixed call envelope in package rpc. The envelope is
transport-agnostic: the generated EchoClient takes an EchoChannel you supply
(your read/write loop), and the generated DispatchEcho routes a decoded
envelope to your handler. Sockets and framing stay downstream.
Server
Implement the generated handler interface; DispatchEcho does the envelope
decode and ordinal routing.
type echoHandler struct{}
func (h *echoHandler) Ping(req []byte) ([]byte, error) {
p, err := WrapPing(req)
if err != nil {
return nil, err
}
return NewPong(PongInput{Seq: p.Seq() + 1}), nil
}
func (h *echoHandler) Notify(req []byte) error { return nil }
func (h *echoHandler) Health() ([]byte, error) { return NewPong(PongInput{Seq: 1}), nil }
func (h *echoHandler) Shutdown() error { return nil }
// In your read loop, for each inbound envelope:
// respBytes, err := DispatchEcho(handler, envelope)
// An unknown ordinal yields a StatusNotFound response; a handler error yields
// StatusInternal — both as valid response envelopes, not transport errors.Client
NewEchoClient takes the channel and an optional capability token attached to
every request. Each method builds a request envelope, sends it over the channel,
and checks the response status. A request method returns three values: an
rpc.Promise (for pipelining), the response body, and an error.
client := NewEchoClient(channel, capToken) // capToken may be nil
_, body, err := client.Ping(NewPing(PingInput{Seq: 41}))
if err != nil {
return err
}
pong, err := WrapPong(body)
if err != nil {
return err
}
fmt.Println(pong.Seq()) // 42The leading rpc.Promise lets you pipeline a dependent call before the first
result returns: each method has an On variant (PingOn(promise)) that targets
a prior call's promise, collapsing two round trips into one.
EchoChannel is the one interface you implement to bind a transport:
type EchoChannel interface {
Call(envelope []byte) (rpc.Response, error)
}Driving the envelope directly
Generated clients are the normal path, but rpc is a public API. Build a
request, ship the bytes over any transport, decode the response:
import "github.com/zap-proto/go/rpc"
reqBytes := rpc.BuildRequest(rpc.Call{
Method: 1, // the interface method ordinal
Target: rpc.NoTarget, // not pipelining off an earlier promise
Cap: capToken, // capability bytes, or nil
Payload: req, // the request struct's bytes
})
// ... send reqBytes, receive respBytes ...
resp, err := rpc.ParseResponse(respBytes)
if err != nil {
return err
}
if resp.Status == rpc.StatusOK {
pong, _ := WrapPong(resp.Body) // the response struct, zero-copy
_ = pong
}Status codes are StatusOK (200), StatusBadRequest (400),
StatusUnauthorized (401), StatusForbidden (403), StatusNotFound (404), and
StatusInternal (500).
Capabilities
Package cap is the ZAP capability runtime. A Cap is a signed, attenuable
token of authority over a Target, granted to a Holder by an Issuer. Caps
form a chain via the Parent field; VerifyChain walks back to a root checking
each signature, expiry, revocation, target invariance, and
monotonically-narrowing permissions.
import "github.com/zap-proto/go/cap"
// Mint a root cap. Ed25519Signer is the built-in mandatory scheme; wire a
// SchemeVerify hook + matching Signer for ML-DSA-65 / hybrid / secp256k1.
signer, _ := cap.NewEd25519Signer()
var target, holder [32]byte // 32-byte content hashes of the resource + holder
c, err := cap.Issue(cap.Issuance{
Kind: uint32(cap.KindIAMSession),
Target: target,
Holder: holder,
Permissions: cap.PermAttenuate, // bits the holder may exercise / delegate
ExpiresAt: 2_000_000_000, // unix seconds; 0 = never
}, signer)
if err != nil {
return err
}
// Verify a single cap. The Verifier holds the policy: revocation lookup,
// issuer-key resolution, and (optionally) a PQ signature hook.
v := cap.Verifier{
IssuerKey: func(issuer [32]byte) ([]byte, error) {
if issuer == signer.Public() {
return signer.PublicKey(), nil
}
return nil, cap.ErrIssuerUnknown
},
}
if err := v.Verify(c, 1_700_000_000); err != nil { // now, in unix seconds
return err // ErrExpired, ErrRevoked, ErrSigMismatch, ErrIssuerUnknown, …
}Derive a narrower child with Attenuate — the signer must hold the parent's
holder key, permissions intersect with the parent's, expiry can only shrink, and
the parent must carry PermAttenuate (or be a KindDelegate cap):
child, err := cap.Attenuate(c, childHolder, cap.PermAudit, nil, 0, signer)Signature scope is the SPEC §3 canonical bytes (cap.CanonicalBytes), and
CapID = SHA-256(CanonicalBytes ‖ Sig) (Cap.ID), so cap identifiers are
byte-identical across every language runtime. Revoke / VerifyRevocation
handle revocation; VerifyChain(leaf, chain, op, target, holder, now) validates
a full chain including the op-against-mask and delegation gate.
Concurrency
Parse aliases the input buffer and Object is a value type holding a pointer
to the immutable message, so reads are safe to share across goroutines. A
Builder is single-owner — build per goroutine, not shared.
Build & test
go build ./...
go test ./...Examples
- examples/echo — a generated Echo RPC service with an in-memory client/server round-trip
- cap/cap_test.go — issue, attenuate, chain walk, revocation, caveats