Files
golib/notify
2026-01-24 20:35:40 +11:00
..
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00
2026-01-24 20:35:40 +11:00

notify

Thread-safe pub/sub notification system for Go applications.

Features

  • Thread-Safe: All operations are safe for concurrent use
  • Configurable Buffering: Set custom buffer sizes per notifier
  • Targeted & Broadcast: Send to specific subscribers or broadcast to all
  • Graceful Shutdown: Built-in Close() for clean resource cleanup
  • Idempotent Operations: Safe to call Unsubscribe() and Close() multiple times
  • Zero Dependencies: Uses only Go standard library
  • Comprehensive Tests: 95%+ code coverage with race detector clean

Installation

go get git.haelnorr.com/h/golib/notify

Quick Start

package main

import (
    "fmt"
    "git.haelnorr.com/h/golib/notify"
)

func main() {
    // Create a notifier with 50-notification buffer per subscriber
    n := notify.NewNotifier(50)
    defer n.Close()

    // Subscribe to receive notifications
    sub, err := n.Subscribe()
    if err != nil {
        panic(err)
    }
    defer sub.Unsubscribe()

    // Listen for notifications
    go func() {
        for notification := range sub.Listen() {
            fmt.Printf("[%s] %s: %s\n", 
                notification.Level, 
                notification.Title, 
                notification.Message)
        }
        fmt.Println("Listener exited")
    }()

    // Send a notification
    n.Notify(notify.Notification{
        Target:  sub.ID,
        Level:   notify.LevelSuccess,
        Title:   "Welcome",
        Message: "You're now subscribed!",
    })

    // Broadcast to all subscribers
    n.NotifyAll(notify.Notification{
        Level:   notify.LevelInfo,
        Title:   "System Status",
        Message: "All systems operational",
    })
}

Usage

Creating a Notifier

The buffer size determines how many notifications can be queued per subscriber:

// Unbuffered - sends block until received
n := notify.NewNotifier(0)

// Small buffer - low memory, may drop if slow readers
n := notify.NewNotifier(25)

// Recommended - balanced approach
n := notify.NewNotifier(50)

// Large buffer - handles bursts well
n := notify.NewNotifier(500)

Subscribing

Each subscriber receives a unique ID and a buffered notification channel:

sub, err := n.Subscribe()
if err != nil {
    // Handle error (e.g., notifier is closed)
    log.Fatal(err)
}

fmt.Println("Subscriber ID:", sub.ID)

Listening for Notifications

Use a for-range loop to process notifications:

for notification := range sub.Listen() {
    switch notification.Level {
    case notify.LevelSuccess:
        fmt.Println("✓", notification.Message)
    case notify.LevelError:
        fmt.Println("✗", notification.Message)
    default:
        fmt.Println("", notification.Message)
    }
}

Sending Targeted Notifications

Send to a specific subscriber:

n.Notify(notify.Notification{
    Target:  sub.ID,
    Level:   notify.LevelWarn,
    Title:   "Account Warning",
    Message: "Password expires in 3 days",
    Details: "Please update your password",
})

Broadcasting to All Subscribers

Send to all current subscribers:

n.NotifyAll(notify.Notification{
    Level:   notify.LevelInfo,
    Title:   "Maintenance",
    Message: "System will restart in 5 minutes",
})

Unsubscribing

Clean up when done (safe to call multiple times):

sub.Unsubscribe()

Graceful Shutdown

Close the notifier to unsubscribe all and prevent new subscriptions:

n.Close()
// After Close():
// - All subscribers are removed
// - All notification channels are closed
// - Future Subscribe() calls return error
// - Notify/NotifyAll are no-ops

Notification Levels

Four predefined levels are available:

Level Constant Use Case
Success notify.LevelSuccess Successful operations
Info notify.LevelInfo General information
Warning notify.LevelWarn Non-critical warnings
Error notify.LevelError Errors requiring attention

Advanced Usage

Custom Action Data

The Action field can hold any data type:

type UserAction struct {
    URL    string
    Method string
}

n.Notify(notify.Notification{
    Target:  sub.ID,
    Level:   notify.LevelInfo,
    Message: "New update available",
    Action: UserAction{
        URL:    "/updates/download",
        Method: "GET",
    },
})

// In listener:
for notif := range sub.Listen() {
    if action, ok := notif.Action.(UserAction); ok {
        fmt.Printf("Action: %s %s\n", action.Method, action.URL)
    }
}

Multiple Subscribers

Create a notification hub for multiple clients:

n := notify.NewNotifier(100)
defer n.Close()

// Create 10 subscribers
subscribers := make([]*notify.Subscriber, 10)
for i := 0; i < 10; i++ {
    sub, _ := n.Subscribe()
    subscribers[i] = sub
    
    // Start listener for each
    go func(id int, s *notify.Subscriber) {
        for notif := range s.Listen() {
            log.Printf("Sub %d: %s", id, notif.Message)
        }
    }(i, sub)
}

// Broadcast to all
n.NotifyAll(notify.Notification{
    Level:   notify.LevelSuccess,
    Message: "All subscribers active",
})

Concurrent-Safe Operations

All operations are thread-safe:

n := notify.NewNotifier(50)

// Safe to subscribe from multiple goroutines
for i := 0; i < 100; i++ {
    go func() {
        sub, _ := n.Subscribe()
        defer sub.Unsubscribe()
        // ...
    }()
}

// Safe to notify from multiple goroutines
for i := 0; i < 100; i++ {
    go func() {
        n.NotifyAll(notify.Notification{
            Level:   notify.LevelInfo,
            Message: "Concurrent notification",
        })
    }()
}

Best Practices

1. Use defer for Cleanup

n := notify.NewNotifier(50)
defer n.Close()

sub, _ := n.Subscribe()
defer sub.Unsubscribe()

2. Check Errors

sub, err := n.Subscribe()
if err != nil {
    log.Printf("Subscribe failed: %v", err)
    return
}

3. Buffer Size Recommendations

Scenario Buffer Size
Real-time chat 10-25
General app notifications 50-100
High-throughput logging 200-500
Testing/debugging 0 (unbuffered)

4. Listener Goroutines

Always use goroutines for listeners to prevent blocking:

// Good ✓
go func() {
    for notif := range sub.Listen() {
        process(notif)
    }
}()

// Bad ✗ - blocks main goroutine
for notif := range sub.Listen() {
    process(notif)
}

5. Detect Channel Closure

for notification := range sub.Listen() {
    // Process notifications
}
// When this loop exits, the channel is closed
// Either subscriber unsubscribed or notifier closed
fmt.Println("No more notifications")

Performance

  • Subscribe: O(1) average case (random ID generation)
  • Notify: O(1) lookup + O(1) channel send (non-blocking)
  • NotifyAll: O(n) where n is number of subscribers
  • Unsubscribe: O(1) map deletion + O(1) channel close
  • Close: O(n) where n is number of subscribers

Benchmarks

Typical performance on modern hardware:

  • Subscribe: ~5-10µs per operation
  • Notify: ~1-2µs per operation
  • NotifyAll (10 subs): ~10-20µs
  • Buffer full handling: ~100ns (TryLock drop)

Thread Safety

All public methods are thread-safe:

  • NewNotifier() - Safe
  • Subscribe() - Safe, concurrent calls allowed
  • Unsubscribe() - Safe, idempotent
  • Notify() - Safe, concurrent calls allowed
  • NotifyAll() - Safe, concurrent calls allowed
  • Close() - Safe, idempotent
  • Listen() - Safe, returns read-only channel

Testing

Run tests:

# Run all tests
go test

# With race detector
go test -race

# With coverage
go test -cover

# Verbose output
go test -v

Current test coverage: 95.1%

Documentation

Full API documentation available at:

  • pkg.go.dev
  • Or run: go doc -all git.haelnorr.com/h/golib/notify

License

MIT License - see repository root for details

Contributing

See CONTRIBUTING.md in the repository root

Other modules in the golib collection:

  • cookies - HTTP cookie utilities
  • env - Environment variable helpers
  • ezconf - Configuration loader
  • hlog - Logging with zerolog
  • hws - HTTP web server
  • jwt - JWT token utilities