The pebbe/zmq4 Go binding for ZeroMQ doesn’t just let you send messages; it actively manages the lifecycle of ephemeral network sockets for you, often in ways that surprise people expecting traditional network programming.

Let’s watch a simple request-reply pattern unfold.

package main

import (
	"fmt"
	"time"

	zmq "github.com/pebbe/zmq4"
)

func main() {
	// Create a ZeroMQ context
	context, _ := zmq.NewContext()
	defer context.Close()

	// Create a socket for the server
	server, _ := context.Socket(zmq.REP) // Reply socket
	defer server.Close()
	server.Bind("tcp://*:5555") // Bind to a TCP address

	fmt.Println("Server started, waiting for requests...")

	// Create a socket for the client
	client, _ := context.Socket(zmq.REQ) // Request socket
	defer client.Close()
	client.Connect("tcp://localhost:5555") // Connect to the server

	// Send a request from the client
	fmt.Println("Client sending request...")
	client.Send("Hello", 0)

	// Receive the request on the server
	msg, _ := server.Recv(0)
	fmt.Printf("Server received: %s\n", msg)

	// Send a reply from the server
	server.Send("World", 0)

	// Receive the reply on the client
	reply, _ := client.Recv(0)
	fmt.Printf("Client received: %s\n", reply)

	// Give it a moment to ensure everything is flushed
	time.Sleep(100 * time.Millisecond)
}

When you run this, you’ll see:

Server started, waiting for requests...
Client sending request...
Server received: Hello
Client received: World

The magic here isn’t just sending bytes. Notice server.Bind and client.Connect. These aren’t just opening TCP ports. ZeroMQ sockets are typed (zmq.REP, zmq.REQ, zmq.PUB, zmq.SUB, etc.), and this type dictates their behavior within a larger messaging pattern. A REP socket must receive a message before it can send one, and a REQ socket must send a message before it can receive one. This enforced sequence is the core of the request-reply pattern. The pebbe/zmq4 binding makes these typed sockets and their state machines accessible from Go.

The zmq.Context is the global factory for all sockets. You create it once and then use it to Socket() new sockets. Crucially, you must Close() the context when you’re done. Sockets themselves also need to be Close()d. The defer keyword is your best friend here for ensuring cleanup.

The Bind operation on the server side is like listen in traditional networking, but it also establishes the endpoint for ZeroMQ’s internal routing. Connect on the client is like connect, but ZeroMQ will automatically retry connections if they fail initially. This is a key difference: ZeroMQ sockets are designed for unreliable networks and transient endpoints.

When client.Send("Hello", 0) is called, the REQ socket enters a "sent" state. It won’t accept another Send until it Recvs. Similarly, when server.Recv(0) is called, the REP socket waits. Once it receives "Hello", it transitions to a "received" state, and it can then Send("World"). After sending, it goes back to the "waiting to receive" state.

The 0 in Send and Recv refers to flags. The most common flag you’ll see is zmq.DONTWAIT (or zmq.DONTWAIT | zmq.SNDMORE for sending multiple parts). Using zmq.DONTWAIT makes the operation non-blocking; if the message can’t be sent or received immediately, it returns an error (typically zmq.ErrWouldBlock) instead of blocking the goroutine. This is essential for building responsive applications.

The pebbe/zmq4 binding, like the underlying ZeroMQ library, abstracts away the low-level socket options. You don’t typically deal with SO_REUSEADDR or raw packet framing. Instead, you configure ZeroMQ socket options using methods like server.SetSockOptString(zmq.IDENTITY, "my_server") or server.SetInt32(zmq.RCVTIMEO, 1000). These options control ZeroMQ’s internal behavior, like setting a socket identity for routing, or a receive timeout in milliseconds.

What trips up most Go developers when they first use pebbe/zmq4 is the implicit state management dictated by the socket type. They try to Send twice on a REQ socket without an intervening Recv, or Recv twice on a REP socket without an intervening Send. ZeroMQ enforces its messaging patterns rigorously, and the pebbe/zmq4 binding exposes this directly. The error you’ll most commonly see in such cases is zmq.ErrInvalidState or a zmq.ErrAgain if you’re using zmq.DONTWAIT.

The next hurdle is understanding how to manage multiple sockets and their states concurrently, often involving select statements on channel reads that are fed by Recv operations in separate goroutines.

Want structured learning?

Take the full Zeromq course →