8.2 KiB
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
Related Projects
Other modules in the golib collection:
cookies- HTTP cookie utilitiesenv- Environment variable helpersezconf- Configuration loaderhlog- Logging with zerologhws- HTTP web serverjwt- JWT token utilities