Guide list | Previous: Python | Next: Rust
Go Binding Guide (zlink.systems/zlink)¶
Contract-owning document for this chapter — the Go bindings spec covers it. This chapter shows that contract as working sample code.
Explains how to use zlink in Go through working sample code. The deep explanation of messaging concepts is owned by the core guide; this guide focuses on the Go API surface.
Installation¶
Ships as the zlink.systems/zlink module. The native core is bundled
per platform.
- Go 1.25 or later.
- No separate native install needed — per-RID
.so/.dllfiles load automatically.
5-Minute Example — PING/ACK¶
A minimal example with a Pair socket where one side sends PING and the
other replies with ACK. The server binds, the client connects.
// Server
ctx, _ := zlink.NewContext()
defer ctx.Close()
server, _ := ctx.PairSocket()
defer server.Close()
server.Bind("tcp://127.0.0.1:5555")
var received zlink.Received
if _, err := server.Recv(&received, zlink.RecvFlagsNone); err != nil { ... }
defer received.Close()
part, _ := received.SinglePartOrError()
fmt.Println(string(part.Data())) // PING
reply, _ := zlink.NewMessage([]byte("ACK"))
server.Send().Message(reply).Submit(nil)
// Client
ctx, _ := zlink.NewContext()
defer ctx.Close()
client, _ := ctx.PairSocket()
defer client.Close()
client.Connect("tcp://127.0.0.1:5555")
ping, _ := zlink.NewMessage([]byte("PING"))
client.Send().Message(ping).Submit(nil)
var received zlink.Received
if _, err := client.Recv(&received, zlink.RecvFlagsNone); err != nil { ... }
defer received.Close()
part, _ := received.SinglePartOrError()
fmt.Println(string(part.Data())) // ACK
In real code always check the error. The example above uses _ just to keep
the flow readable.
Core Types¶
The 4 fundamental types every feature shares.
1. Context¶
The runtime entry point for a process. Usually you create one and build every socket/service from it.
ctx, err := zlink.NewContext()
if err != nil { ... }
defer ctx.Close() // closing the context shuts down every child socket
Use Options() to adjust the I/O thread count:
It's recommended to close sockets explicitly before the context closes. Closing the context interrupts blocking operations on any socket still open. (see thread safety)
2. Message¶
Owns a single payload frame. Sending transfers ownership, so Close() doesn't
need to be called separately. If the send fails, ownership is retained, so
retry or close it explicitly.
// build a copy from a byte slice
msg, err := zlink.NewMessage([]byte("payload"))
// allocate a sized empty frame, fill it directly
msg, err := zlink.NewMessageWithSize(256)
copy(msg.Data(), myData)
// build a copy from a string
msg, err := zlink.NewMessageString("payload")
// discard without sending
defer msg.Close()
Use Data() to read a received payload. The returned slice is tied to the
message's lifetime, so copy it if you need to keep it:
data := msg.Data() // valid only while the message is alive
snapshot := msg.Bytes() // an independent copy
text := msg.Text() // UTF-8 string conversion
The terminal for HWM-managed sends is Submit(ctx). It uses the completion-
backed DONTWAIT path without blocking the goroutine in Core; the Context bounds
the caller wait. Go send has no separate callback or synchronous terminal.
Request also has one idiomatic terminal:
request.Message(...).Timeout(...).Submit(ctx) returns the reply parts and
error directly after its socket completion is drained. The reply is not DATA
received separately. Send and request do not expose callback terminals.
Core owns retry after accepting a pre-admission operation; do not add a caller
retry queue or resubmit its payload. The native
ZLINK_OPT_PENDING_MAX_MSGS/BYTES caps are shared by pending SEND and REQUEST,
with no send-only pending names. Completion means local admission, not peer
delivery or an application acknowledgement.
Canceling the context.Context can stop a pre-submit call or the Go waiter. If
Core already accepted the payload, admission or request processing may continue
and the socket owner still drains the late completion. STREAM must call
SetReceiveMode(StreamReceiveRaw) or SetReceiveMode(StreamReceivePacket)
before bind/connect, then use Recv or RecvPacket respectively.
If a public poller owns PollCompletion for a socket, keep another goroutine
calling Wait() while Submit(ctx) is outstanding. Wait() drains native
completions and settles or cleans Go state; a blocking submit between waits on
the same goroutine can stall completion.
3. Received — the receive envelope¶
Holds a received message envelope. Carries a routing ID, part list, and an
optional reply context. Can be declared once and reused across multiple
receives. Parts are released by calling Close().
var received zlink.Received // stack-declared, no heap allocation
_, err = socket.Recv(&received, zlink.RecvFlagsNone)
defer received.Close() // releases the received parts
// single-part access
part, err := received.SinglePartOrError()
payload := part.Data()
// multipart access
for _, part := range received.Parts() {
_ = part.Data()
}
// routing ID (present on ROUTER/SPOT receive)
rid := received.RoutingID() // *RoutingID, nil if absent
4. RoutingID¶
An immutable value of 1-255 bytes identifying a peer or spot. Build it directly or pull it from a receive envelope.
rid := zlink.NewRoutingID([]byte("server-01"))
rid := zlink.NewRoutingIDString("server-01")
rid := zlink.NewRoutingIDUint32(1) // 4 bytes, big-endian
rid := zlink.NewRoutingIDUUIDBytes(uuid) // 16-byte UUID
rid, err := zlink.NewRoutingIDFromHex("0102...") // parsed from hex
fmt.Println(rid.String()) // human-readable form
Ownership And Lifetime¶
The Go binding's ownership rules are simple.
| Situation | Rule |
|---|---|
Submit succeeds |
ownership of the added *Message transfers to the send stack. No Close() needed |
Submit fails |
ownership returns to the caller. Close() required |
Recv succeeds |
the caller owns the Received. defer received.Close() required |
Request.SubmitAsync completes |
ownership of the reply parts ([]*Message) comes to the caller. Close() each part |
Context.Close() |
interrupts every blocking operation under the context |
// pattern: safe even on error
msg, _ := zlink.NewMessage([]byte("data"))
if err := socket.Send().Message(msg).Submit(context.Background()); err != nil {
defer msg.Close() // only closed on send failure
}
// on send success msg is already consumed, no Close() needed
Error Handling¶
The Go binding returns the standard error interface. If you need the result
code, check it with a type assertion.
err := socket.Send().Message(msg).Submit(context.Background())
if err != nil {
var submitErr *zlink.SubmitError
if errors.As(err, &submitErr) {
switch submitErr.Result {
case zlink.SubmitBackpressured:
// back-pressure — retry shortly
case zlink.SubmitNotConnected:
// no connected peer
default:
return err
}
}
return err
}
Error types:
| Type | Description | Result field |
|---|---|---|
*SubmitError |
send/publish failure | SubmitResult |
*RequestError |
request failure | RequestResult |
*RecvError |
receive failure | RecvResult |
*BindError |
bind failure | BindResult |
*ConnectError |
connect failure | ConnectResult |
*ConfigError |
option-set failure | ConfigResult |
*CloseError |
close failure | CloseResult |
*HandlerError |
handler registration failure | HandlerResult |
No message on a non-blocking receive isn't an error:
ok, err := socket.Recv(&received, zlink.RecvFlagsDontWait)
if err != nil { /* a real error */ }
if !ok { /* no message */ }
C API Mapping¶
| C API | Go API |
|---|---|
zlink_ctx_new() |
zlink.NewContext() |
zlink_ctx_term() |
ctx.Close() |
zlink_socket(ctx, type) |
ctx.PairSocket(), etc. |
zlink_close(socket) |
socket.Close() |
zlink_bind(socket, ep) |
socket.Bind(ep) |
zlink_connect(socket, ep) |
socket.Connect(ep) |
zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) |
socket.Send().Message(m).Flags(flag).Submit(ctx) |
zlink_recv(..., parts_out, capacity, count_out, ...) |
socket.Recv(&received, flags) |
zlink_msg_data(msg) |
msg.Data() |
zlink_msg_size(msg) |
msg.Size() |
zlink_msg_close(msg) |
msg.Close() |
zlink_routing_id_t |
zlink.RoutingID |
zlink_socket_monitor_open(...) |
zlink.OpenSocketMonitor(socket, ...) |
zlink_poller_new() |
zlink.NewPoller() |
zlink_timer_new() |
zlink.NewTimer() |
Native Library / Deployment¶
The Go binding embeds a per-platform .so (Linux) or .dylib (macOS). No
separate install — just go get.
Checking the native version in use:
Checking whether a feature is supported:
Threading: Context can be shared across goroutines, but sockets must be used
from a single goroutine only. (see thread safety)
A default send without DontWait stops its calling goroutine while waiting for
HWM admission. Other goroutines continue to run, so this is safe in this
execution environment. Use Flags(zlink.SendFlagsDontWait).Submit(ctx) when
immediate back-pressure is required.
Samples¶
Verified sample code lives under bindings/go/samples/.
| Sample | Description |
|---|---|
pair_recv_sample |
PAIR socket send/receive |
dealer_router_recv_sample |
DEALER/ROUTER send/receive |
request_reply_async_sample |
Async request/reply |
pubsub_recv_sample |
XPUB/SUB publish/subscribe |
stream_recv_sample |
STREAM raw TCP |
stream_packet_callback_sample |
STREAM PACKET pull (legacy directory name) |
monitor_recv_sample |
Monitor event receive |
SPOT/Actor examples are covered by the framework samples, not the core binding. Go doesn't have a framework binding yet.
Running the samples:
See Also¶
- Socket patterns: overview — PAIR · PUB/SUB · DEALER · ROUTER · STREAM · Proxy
- Operations: Socket options · TLS · Monitoring · Thread safety · Message API · Routing ID