12 KiB
Section 2 — Understand a Go MQTT 5 connection
Prerequisite: Section 1's broker and backend credentials work. This section contains a complete small program. Type it in stages, compile after each meaningful change, and explain the code before running the failure exercises.
Version baseline: Paho v0.23.0, verified from upstream release tags on 2026-10-07. It requires Go 1.24 or newer; Go 1.25 is compatible. The matching local reference is references/paho.golang-v0.23.0/ (commit 1aa03969c5932464359b3e4ed752d9ac61abccc9).
Working directory and file placement
Run every shell command in this lesson from the learning project root:
cd /home/ryan/Media/Code/Code/robot-market/mqtt-5-learning
Paths such as cmd/backend/main.go are relative to this directory. Markdown files under lessons/ explain the work; create the application files at the paths shown, not inside lessons/ or references/. Code blocks labeled Go, TOML, YAML, or conf are file contents; shell blocks are terminal commands. Create parent directories before saving files.
1. Build the connection program
Create the directory first:
mkdir -p cmd/backend
Create cmd/backend/main.go and paste the complete program below into it. If the file already exists, replace its contents rather than appending a second program:
package main
import (
"context"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/eclipse/paho.golang/autopaho"
"github.com/eclipse/paho.golang/paho"
)
func main() {
if err := run(); err != nil {
log.Print(err)
os.Exit(1)
}
}
func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
user := os.Getenv("TEST_MQTT5_MQTT_USERNAME")
password := os.Getenv("TEST_MQTT5_MQTT_PASSWORD")
if user == "" || password == "" {
return fmt.Errorf("set the lab username and password environment variables")
}
broker, err := url.Parse("mqtt://127.0.0.1:18883")
if err != nil {
return fmt.Errorf("parse broker URL: %w", err)
}
cfg := autopaho.ClientConfig{
ServerUrls: []*url.URL{broker},
KeepAlive: 20,
CleanStartOnInitialConnection: true,
SessionExpiryInterval: 0,
ConnectUsername: user,
ConnectPassword: []byte(password),
OnConnectionUp: func(_ *autopaho.ConnectionManager, ack *paho.Connack) {
log.Printf("connected: session_present=%t reason=0x%02x", ack.SessionPresent, ack.ReasonCode)
},
OnConnectionDown: func() bool {
log.Print("connection down; AutoPaho will retry")
return true
},
OnConnectError: func(err error) {
// Keep errors free of credentials; never log cfg or broker userinfo.
log.Printf("connect attempt failed: %v", err)
},
ClientConfig: paho.ClientConfig{
ClientID: "lab-backend",
OnClientError: func(err error) { log.Printf("client error: %v", err) },
OnServerDisconnect: func(packet *paho.Disconnect) {
log.Printf("server disconnect: reason=0x%02x", packet.ReasonCode)
},
},
}
manager, err := autopaho.NewConnection(ctx, cfg)
if err != nil {
return fmt.Errorf("create connection manager: %w", err)
}
defer func() {
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := manager.Disconnect(shutdown); err != nil {
log.Printf("shutdown did not finish within deadline: %v", err)
}
}()
startup, cancel := context.WithTimeout(ctx, 10*time.Second)
err = manager.AwaitConnection(startup)
cancel()
if err != nil {
return fmt.Errorf("initial connection: %w", err)
}
log.Print("connection ready; press Ctrl+C to stop")
<-ctx.Done()
return nil
}
2. Understand the connection lifecycle
Before reading the full program, predict what evidence you need at each stage:
Go process → AutoPaho retry loop → TCP connection → CONNECT → CONNACK
→ subscriptions (Lesson 4)
AutoPaho owns reconnect attempts and creates the underlying Paho client for a connection. Paho implements MQTT packets and protocol exchanges. Your application owns readiness, authorization policy, command state, and business retries. A running goroutine or open TCP socket is too early to declare MQTT ready: the broker can still reject CONNECT.
| Setting or call | MQTT meaning and reason for this choice |
|---|---|
ServerUrls |
Where to dial; the lab host endpoint is different from the Compose service endpoint |
ClientID |
Broker session identity; two simultaneous processes with this ID compete for one connection |
ConnectUsername, ConnectPassword |
CONNECT authentication inputs; independent of Client ID and payload identity |
KeepAlive: 20 |
Maximum idle interval between client control packets, in seconds; Paho handles PINGREQ/PINGRESP when needed |
CleanStartOnInitialConnection: true |
Discard a previous broker session on the manager's first successful connection |
SessionExpiryInterval: 0 |
Do not retain this broker session after the connection closes |
NewConnection |
Start connection management; a nil error does not establish a successful CONNACK |
AwaitConnection |
Wait for connection availability; it does not wait for SUBACK or a device heartbeat |
OnConnectionUp |
Observe a successful connection, including reconnection; inspect CONNACK evidence here |
OnConnectError |
Observe failed attempts, including refused connections or unsuccessful CONNACK |
OnConnectionDown |
Decide whether reconnect attempts should continue; returning false ends that retry lifecycle |
OnServerDisconnect |
Inspect a DISCONNECT actually sent by the broker; a severed cable cannot send this packet |
Keep Alive checks the MQTT connection, not the health of the vending application. With an effective Keep Alive of 20 seconds, the broker's protocol inactivity limit is 30 seconds without a client control packet; an already-detected closed socket can be handled earlier. A CONNACK Server Keep Alive can override the requested value. Your three-second heartbeat in Lesson 4 carries application evidence and has a separate purpose.
There are three different contexts here. The signal context controls the program lifetime. startup limits the initial wait to ten seconds. shutdown is a fresh context because the signal context is already canceled during shutdown. The manager receives the long-lived context, not the startup deadline; passing the startup context to NewConnection would terminate your service when that deadline expires.
CleanStartOnInitialConnection: true and SessionExpiryInterval: 0 choose a disposable early-lab session. This is convenient for first experiments and intentionally offers no offline broker session after disconnect. Later lessons change these settings and test recovery. Do not copy them as a durable production policy.
Paho v0.23.0's Disconnect(ctx) cancels the manager and waits for manager shutdown with the provided deadline. Its Done() documentation notes that underlying MQTT cleanup may still be completing; neither API is proof that application data was persisted or device work was finished.
3. Run it
In zsh, read your backend password without echoing it:
export TEST_MQTT5_MQTT_USERNAME=backend
read -rs 'TEST_MQTT5_MQTT_PASSWORD?Backend lab password: '
export TEST_MQTT5_MQTT_PASSWORD
go run ./cmd/backend
export NAME=value puts a variable into this terminal's environment so child processes such as go run inherit it. The read line uses zsh syntax: -r preserves backslashes, -s hides typed characters, and NAME?prompt selects the variable and prompt. Enter the password you created in Lesson 1; this does not create a broker user. The second export exposes that newly read variable to the Go process. In bash the prompt syntax differs; do not copy this zsh-specific line unchanged.
go run ./cmd/backend compiles the package and runs it in the foreground. ./ means relative to the project root. Ctrl+C sends an interrupt to the foreground program; signal.NotifyContext turns it into context cancellation, allowing your cleanup code to run. After the process exits, unset TEST_MQTT5_MQTT_PASSWORD removes the credential from this shell's environment; it does not rotate the broker credential.
Environment variables avoid password literals in this command but are not a secrets vault. Use only lab credentials. Expect connected and connection ready; callback log order can vary. Press Ctrl+C and verify exit within the shutdown deadline.
Run syntax checks after creating the file:
gofmt -w cmd/backend/main.go
go build ./cmd/backend
go vet ./cmd/backend
gofmt -w rewrites the named source file using Go formatting. go build checks compilation and linking; go vet checks suspicious code patterns without running the application. A successful vet run is normally quiet.
go build emits a binary in the current directory; remove or ignore it as appropriate. A build verifies syntax/types, not connection behavior.
4. Observe the protocol and failures
CONNECT carries your client ID, authentication fields, keepalive, and session properties. A successful CONNACK is connection evidence. Bad credentials produce an unsuccessful connection attempt; the rejected CONNACK is handled by the connection-error path rather than your success callback.
- Stop the broker before running the program. Expect retry diagnostics and initial-wait failure near ten seconds, followed by cleanup.
- Start the broker; run the program; stop and restart the broker. Expect connection-down and connection-up events. You have not restored subscriptions yet because this program has none.
- Supply a wrong password. Explain why retrying cannot repair an incorrect credential.
- Run two copies using the same client ID. Observe connection takeover/reconnect behavior, then stop both. Unique IDs are necessary for concurrently running clients.
- Press Ctrl+C while the broker is unavailable. Cancellation should interrupt the wait; do not wait for the full startup deadline.
Cautions and troubleshooting
- AutoPaho handles connection management; it does not invent topic authorization, business retries, or durable command storage.
- Do not call
log.Fatalfrom callbacks. It terminates the process without running deferred cleanup. - Callbacks should be short. Never add an indefinite channel send, database operation, or sleep to
OnConnectionDown. - Keep client IDs stable when you later want session resumption, but distinct across different simultaneously running clients.
- Logging entire config structs or URL userinfo can reveal passwords. These examples keep credentials separate from the URL.
- A connection-up event is not subscription readiness, and neither is device readiness.
Completion: Explain every field in cfg, distinguish the three contexts, and demonstrate successful startup, failed startup, reconnect, and cancellation.
Questions to answer before Section 3
- Why can
NewConnectionreturn before a usable broker connection exists? - What would happen if you passed
startuptoNewConnection? - Why does shutdown use
context.Background()as its parent? - What does returning
falsefromOnConnectionDownchange?
Check your reasoning after answering
NewConnection starts a background lifecycle; CONNACK is later evidence. A startup context passed as the manager lifetime would stop the manager when that context expires or is canceled. Shutdown needs a fresh bounded context because the signal context is already canceled. Returning false from OnConnectionDown stops further automatic reconnect attempts for that manager; it does not repair the connection or preserve application state.
Sources
Next: Section 3.