Go SDK

The official NoLag SDK for Go. Idiomatic Go client with goroutines and channels support.

Installation

go get github.com/NoLagApp/go-sdk

Quick Start

package main

import (
    "fmt"
    "time"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    // Create client with your actor token
    client := nolag.New("your-actor-token")

    // Connect to NoLag
    if err := client.Connect(); err != nil {
        panic(err)
    }
    defer client.Close()

    // Subscribe to a topic
    err := client.Subscribe("my-topic", func(data any, meta nolag.MessageMeta) {
        fmt.Printf("Received: %v\n", data)
    })
    if err != nil {
        panic(err)
    }

    // Publish a message
    if err := client.Emit("my-topic", map[string]any{"hello": "world"}); err != nil {
        fmt.Printf("Emit failed: %v\n", err)
    }

    // Get actor ID assigned by server
    fmt.Println("Actor ID:", client.ActorID())

    // Keep running
    time.Sleep(60 * time.Second)
}

Configuration

import (
    "time"
    nolag "github.com/NoLagApp/go-sdk"
)

options := nolag.Options{
    URL:                  "wss://broker.nolag.app/ws", // Custom broker URL
    Reconnect:            true,                         // Auto-reconnect (default: true)
    ReconnectInterval:    5 * time.Second,              // Reconnect interval (default: 5s)
    MaxReconnectAttempts: 10,                           // Max attempts, 0 = infinite (default: 10)
    HeartbeatInterval:    30 * time.Second,             // Heartbeat interval, 0 to disable (default: 30s)
    LoadBalance:          true,                         // Enable load balancing (default: false)
    LoadBalanceGroup:     "workers",                    // Load balance group name
    ActorTokenID:         "custom-id",                  // Optional actor token identifier
    Debug:                true,                         // Enable debug logging (default: false)
}

client := nolag.New("your-actor-token", options)

Subscribing to Topics

Subscribe, Unsubscribe, and all filter methods return error. Always check the returned error.

// Basic subscription. Subscribe returns an error
err := client.Subscribe("chat/messages", func(data any, meta nolag.MessageMeta) {
    fmt.Printf("Message from %s: %v\n", meta.Sender, data)
})
if err != nil {
    fmt.Printf("Subscribe failed: %v\n", err)
}

// With options (load balancing + filters)
loadBalance := true
err = client.Subscribe("tasks", handler, nolag.SubscribeOptions{
    LoadBalance:      &loadBalance,
    LoadBalanceGroup: "workers",
    Filters:          []any{"priority:high", "region:us"},
})
if err != nil {
    fmt.Printf("Subscribe failed: %v\n", err)
}

// Unsubscribe (also returns an error)
if err := client.Unsubscribe("chat/messages"); err != nil {
    fmt.Printf("Unsubscribe failed: %v\n", err)
}

Publishing Messages

Emit returns an error. Use EmitOptions to set retain, echo, and filter targeting.

// Publish any data (maps, structs, strings, etc.). Emit returns an error
if err := client.Emit("chat/messages", map[string]any{"text": "Hello!"}); err != nil {
    fmt.Printf("Emit failed: %v\n", err)
}

// With options
echo := false
err := client.Emit("status", map[string]any{"online": true}, nolag.EmitOptions{
    Retain: true,     // Retain last message for new subscribers
    Echo:   &echo,    // Don't receive this message back (default: true)
    Filter: "room-1", // Target specific filter subscribers
})
if err != nil {
    fmt.Printf("Emit failed: %v\n", err)
}

Fluent API (SetApp / SetRoom)

The fluent API scopes all operations to an app/room pair. Topics are automatically prefixed, so room.Emit("messages", ...) publishes to "chat/general/messages".

// The fluent API scopes operations to an app/room.
// Topics are automatically prefixed with "app/room/".
room := client.SetApp("chat").SetRoom("general")

// Subscribe: topic becomes "chat/general/messages"
err := room.Subscribe("messages", func(data any, meta nolag.MessageMeta) {
    fmt.Printf("Message: %v\n", data)
})
if err != nil {
    fmt.Printf("Subscribe failed: %v\n", err)
}

// Emit: topic becomes "chat/general/messages"
if err := room.Emit("messages", map[string]any{"text": "Hello!"}); err != nil {
    fmt.Printf("Emit failed: %v\n", err)
}

// Unsubscribe
room.Unsubscribe("messages")

// Filter management on a room
room.SetFilters("messages", []any{"priority:high"})
room.AddFilters("messages", []string{"priority:medium"})
room.RemoveFilters("messages", []string{"priority:high"})

// Event handlers on scoped topics
room.On("messages", func(args ...any) {
    fmt.Println("Custom event on chat/general/messages")
})
room.Off("messages")

// Get the full topic prefix
fmt.Println(room.Prefix()) // "chat/general"

Filter Management

Filters narrow which messages a subscriber receives. You can set filters at subscribe time or manage them dynamically with SetFilters, AddFilters, and RemoveFilters. On the publish side, use EmitOptions.Filter to target specific subscribers.

// Subscribe with initial filters
err := client.Subscribe("orders", handler, nolag.SubscribeOptions{
    Filters: []any{"region:us", "status:pending"},
})

// Replace all filters for a topic (empty slice = receive all messages)
err = client.SetFilters("orders", []any{"region:eu", "status:shipped"})

// Add filters to existing set (deduplicates automatically)
err = client.AddFilters("orders", []string{"status:delivered"})

// Remove specific filters
err = client.RemoveFilters("orders", []string{"status:shipped"})

// Emit with a filter value. Only subscribers with matching filter receive it
err = client.Emit("orders", orderData, nolag.EmitOptions{
    Filter: "region:us",
})

Connection Events

// Listen for connection events
client.On("connected", func(args ...any) {
    fmt.Println("Connected!")
})

client.On("disconnected", func(args ...any) {
    fmt.Println("Disconnected")
})

client.On("reconnecting", func(args ...any) {
    attempt := args[0].(int)
    fmt.Printf("Reconnecting... attempt %d\n", attempt)
})

// Prefer OnError for broker errors: it delivers a typed *nolag.ServerError
// with the error code, topic, and remediation hint. See Error Handling below.
client.OnError(func(err *nolag.ServerError) {
    fmt.Printf("Error: %v\n", err)
})

client.On("presence", func(args ...any) {
    topic := args[0].(string)
    data := args[1]
    fmt.Printf("Presence update on %s: %v\n", topic, data)
})

// Remove all handlers for an event
client.Off("error")

// Check connection status
if client.Status() == nolag.StatusConnected {
    fmt.Println("We're connected!")
}

// Get the actor ID assigned by the server after authentication
fmt.Println("Actor ID:", client.ActorID())

Presence

// Set your presence data
if err := client.SetPresence(map[string]any{
    "status": "online",
    "typing": false,
}); err != nil {
    fmt.Printf("SetPresence failed: %v\n", err)
}

// Get presence of all actors in a topic
presenceList, err := client.GetPresence("chat/room-1")
if err == nil {
    for _, actor := range presenceList {
        fmt.Printf("%s (%s): %v (joined %s)\n",
            actor.ActorTokenID,
            actor.ActorType,
            actor.Presence,
            actor.JoinedAt,
        )
    }
}

// Listen for presence changes
client.On("presence", func(args ...any) {
    topic := args[0].(string)
    presence := args[1]
    fmt.Printf("Presence update in %s: %v\n", topic, presence)
})

Error Handling

Errors reach you through two separate channels, and confusing them is the most common reason a Go client appears to do nothing:

  • Returned errors come from local, synchronous problems: not connected, encode failures, timeouts. Every operation returns one.
  • Broker errors are asynchronous. Subscribe and Emit are fire-and-forget, so a rejected subscription or an unwritable topic is reported later on the error event, not as a return value. Subscribe returning nil means the frame was sent, not that the broker accepted it.

Register OnError before calling Connect so nothing is missed during the handshake.

Broker Errors

client := nolag.New("your-actor-token")

client.OnError(func(err *nolag.ServerError) {
    // err also satisfies the error interface
    log.Printf("nolag: %v", err)

    switch err.Name {
    case "unknown_topic":
        // The room has not been created. Provision it via the rooms API.
        log.Printf("missing room for topic %s: %s", err.Topic, err.Hint)
    case "not_authorized":
        log.Printf("actor lacks access to %s", err.Topic)
    }
})

if err := client.Connect(); err != nil {
    log.Fatal(err)
}

ServerError carries the full frame:

FieldTypeDescription
CodeintNumeric error code, for example 42940. Zero if the broker sent none
NamestringMachine-readable name, for example unknown_topic. Always set
TopicstringThe topic the error refers to, when topic-scoped
HintstringRemediation hint from the broker, when provided
MsgRefstringThe publish this error responds to, when applicable

Code and Hint require protocol version 2. The SDK requests v2 automatically and the negotiated result is available from client.ProtocolVersion(). Against an older broker this returns 1 and only Name is populated.

Returned Errors

package main

import (
    "errors"
    "fmt"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    client := nolag.New("your-actor-token")

    // Connect with error handling
    if err := client.Connect(); err != nil {
        fmt.Printf("Connection failed: %v\n", err)
        return
    }
    defer client.Close()

    // All operations return errors
    if err := client.Subscribe("topic", handler); err != nil {
        // Check for specific error types
        if errors.Is(err, nolag.ErrNotConnected) {
            fmt.Println("Not connected!")
        }
        fmt.Printf("Subscribe failed: %v\n", err)
    }

    if err := client.Emit("topic", "data"); err != nil {
        fmt.Printf("Emit failed: %v\n", err)
    }

    if err := client.Unsubscribe("topic"); err != nil {
        fmt.Printf("Unsubscribe failed: %v\n", err)
    }

    if err := client.SetFilters("topic", []any{"filter1"}); err != nil {
        fmt.Printf("SetFilters failed: %v\n", err)
    }
}

// Sentinel errors available:
// nolag.ErrNotConnected - operation attempted while disconnected
// nolag.ErrAuthFailed   - authentication failed
// nolag.ErrTimeout      - operation timed out

REST API Client

The SDK also includes a REST API client for managing apps, rooms, actors, and scopes:

package main

import (
    "context"
    "fmt"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    ctx := context.Background()

    // Create API client with project-scoped API key
    api := nolag.NewAPI("nlg_live_xxx.secret")

    // List all apps in your project
    apps, err := api.Apps.List(ctx, nil)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Found %d apps\n", len(apps.Data))

    // Create a new app
    app, err := api.Apps.Create(ctx, nolag.AppCreate{
        Name:        "my-chat-app",
        Description: "A real-time chat application",
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("Created app: %s\n", app.AppID)

    // Create a room in the app
    room, err := api.Rooms.Create(ctx, app.AppID, nolag.RoomCreate{
        Name: "general",
        Slug: "general",
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("Created room: %s\n", room.RoomID)

    // Create an actor (save the access token!)
    actor, err := api.Actors.Create(ctx, nolag.ActorCreate{
        Name:      "web-client",
        ActorType: nolag.ActorDevice,
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("Actor token: %s\n", actor.AccessToken)
}

Access Scopes

Manage access scopes for tenant isolation:

import nolag "github.com/NoLagApp/go-sdk"

api := nolag.NewAPI("nlg_live_xxx.secret")
ctx := context.Background()

// List scopes
scopes, err := api.Scopes.List(ctx)
if err != nil {
    panic(err)
}
fmt.Printf("Found %d scopes\n", len(scopes))

// Create a scope
scope, err := api.Scopes.Create(ctx, nolag.ScopeCreate{
    Slug: "tenant-acme",
    Name: "Acme Corp",
})
if err != nil {
    panic(err)
}
fmt.Printf("Created scope: %s\n", scope.AccessScopeID)

// Assign an actor to the scope
scopeID := scope.AccessScopeID
_, err = api.Actors.Update(ctx, actorId, nolag.ActorUpdate{
    AccessScopeID: &scopeID,
})

// List actors in a scope
actors, err := api.Scopes.ListActors(ctx, scope.AccessScopeID)

// Update a scope
newName := "Acme Corporation"
_, err = api.Scopes.Update(ctx, scope.AccessScopeID, nolag.ScopeUpdate{
    Name: &newName,
})

// Delete a scope (must unscope actors first)
err = api.Scopes.Delete(ctx, scope.AccessScopeID)

Load Balancing

Distribute messages across multiple subscribers:

// Enable load balancing per-subscription
loadBalance := true
err := client.Subscribe("tasks", processTask, nolag.SubscribeOptions{
    LoadBalance:      &loadBalance,
    LoadBalanceGroup: "task-workers",
})
if err != nil {
    fmt.Printf("Subscribe failed: %v\n", err)
}

// Or enable load balancing globally via connection options
client := nolag.New("your-actor-token", nolag.Options{
    LoadBalance:      true,
    LoadBalanceGroup: "task-workers",
})

Type Definitions

import nolag "github.com/NoLagApp/go-sdk"

// WebSocket Client
nolag.Client           // The real-time messaging client
nolag.Options          // Connection options (URL, Reconnect, LoadBalance, etc.)
nolag.SubscribeOptions // Subscription options (LoadBalance, Filters, etc.)
nolag.EmitOptions      // Publish options (Retain, Echo, Filter)
nolag.App              // Intermediate context from SetApp()
nolag.Room             // Scoped pub/sub context from SetApp().SetRoom()

// Enums / Constants
nolag.ConnectionStatus // StatusDisconnected, StatusConnecting, StatusConnected, StatusReconnecting
nolag.ActorType        // ActorDevice, ActorUser, ActorService, ActorSession,
                       // ActorAgent, ActorOrchestrator, ActorObserver
nolag.QoS              // QoSAtMostOnce, QoSAtLeastOnce, QoSExactlyOnce

// Sentinel Errors
nolag.ErrNotConnected  // Not connected to broker
nolag.ErrAuthFailed    // Authentication failed
nolag.ErrTimeout       // Operation timed out

// Data types
nolag.MessageMeta      // Message metadata (Sender, Timestamp, IsReplay, MsgID, Filter)
nolag.ActorPresence    // Presence info (ActorTokenID, ActorType, Presence, JoinedAt)
nolag.MessageHandler   // func(data any, meta MessageMeta)
nolag.EventHandler     // func(args ...any)

// REST API Client
nolag.API              // REST API client
nolag.APIOptions       // API client options
nolag.NoLagAPIError    // API error type
nolag.APIError         // Raw API error details

// Resources
nolag.AppResource, nolag.AppCreate, nolag.AppUpdate
nolag.RoomResource, nolag.RoomCreate, nolag.RoomUpdate
nolag.ActorResource, nolag.ActorWithToken, nolag.ActorCreate, nolag.ActorUpdate
nolag.ScopeResource, nolag.ScopeCreate, nolag.ScopeUpdate
nolag.PaginatedApps, nolag.ListOptions

Requirements

  • Go 1.21+
  • github.com/gorilla/websocket v1.5.1
  • github.com/vmihailenco/msgpack/v5 v5.4.1

Next Steps