Core (Notifier, Config)
The Notifier type and its Config. Every go-notification app starts here.
notification.New
func New(cfg Config) *NotifierCreates a new notifier and applies defaults to any zero-valued Config fields. The returned *Notifier is safe for concurrent use and should be a long-lived singleton. It starts a background worker pool when Async is enabled (the default), so it must be closed with Close() when no longer needed.
Notifier methods
func (n *Notifier) RegisterChannel(ch Channel)
func (n *Notifier) Channel(name string) Channel
func (n *Notifier) SetRateLimit(channel string, rate int, interval time.Duration, burst int)
func (n *Notifier) Send(ctx context.Context, notifiable Notifiable, notif Notification, opts ...SendOption) error
func (n *Notifier) SendMulti(ctx context.Context, notifiables []Notifiable, notif Notification, opts ...SendOption) error
func (n *Notifier) Close()RegisterChannel
func (n *Notifier) RegisterChannel(ch Channel)Registers a driver. The channel's identifier comes from its own Name() method — there is no name argument. You reference the name from Notification.Via().
Silent overwrite on name collision
Registering a second channel that reports the same Name() silently
replaces the first — no error, no warning. Each family has a default name
("mail", "sms", "chat", "whatsapp", "push", "webhook",
"database"), and Slack/Telegram/Discord/Teams all default to "chat".
When registering two drivers from the same family — or any two chat
providers — set a distinct Config.Name on each. See
Multiple instances of one driver.
Channel
func (n *Notifier) Channel(name string) ChannelReturns the registered channel for name, or nil if none. Useful for reaching channel-specific APIs — e.g. type-asserting the database channel to query stored notifications.
SetRateLimit
func (n *Notifier) SetRateLimit(channel string, rate int, interval time.Duration, burst int)Installs a token-bucket rate limiter for a single channel. See Rate Limiting.
Send
func (n *Notifier) Send(ctx context.Context, notifiable Notifiable, notif Notification, opts ...SendOption) errorDispatches a notification to one recipient on every channel returned by notif.Via(notifiable) (unless overridden with the Via option). In async mode (default) it returns nil immediately after dispatching, and delivery errors surface via Config.OnError. In sync mode it blocks until every channel completes and returns the joined errors. After Close(), it returns ErrClosed.
SendMulti
func (n *Notifier) SendMulti(ctx context.Context, notifiables []Notifiable, notif Notification, opts ...SendOption) errorCalls Send for each notifiable independently; one failure does not stop the others. See Broadcast.
Close
func (n *Notifier) Close()Shuts down the worker pool and waits for in-flight deliveries to complete. Takes no arguments and returns nothing. Idempotent — safe to call more than once. Subsequent Send calls return ErrClosed.
Config
type Config struct {
// Async dispatches each delivery on a worker goroutine when true.
// When false, Send blocks until delivery completes. Default: true.
Async *bool
// WorkerPool is the max concurrent in-flight sends. Default: 10.
WorkerPool int
// QueueSize is the buffered queue feeding the pool.
// Default: WorkerPool * 10.
QueueSize int
// MaxRetries is the number of retries after the first attempt.
// Total attempts = 1 + MaxRetries. Default: 3.
// Footgun: a zero value is treated as the default (3) — to actually
// disable retries, set a NEGATIVE value (e.g. -1).
MaxRetries int
// RetryDelay is the base delay for exponential backoff. Default: 1s.
// The Nth retry waits RetryDelay * 2^(N-1), capped at 60s, with jitter.
RetryDelay time.Duration
// Logger receives structured events. Default: slog.Default().
Logger *slog.Logger
// OnError runs after all retries for a channel are exhausted.
OnError func(ctx context.Context, n Notifiable, channel string, err error)
// OnSuccess runs after a notification is delivered on a channel.
OnSuccess func(ctx context.Context, n Notifiable, channel string)
}Async is a *bool, not a bool, so its zero value (nil) can default to
true. Use the notification.BoolPtr helper to set it explicitly:
Async: notification.BoolPtr(false).
notification.BoolPtr
func BoolPtr(v bool) *boolReturns a pointer to v. Provided so you can set Config.Async inline.
SendOption
Per-call options passed as the trailing opts ...SendOption argument to Send/SendMulti:
func Via(channels ...string) SendOption // override Notification.Via for this call
func Sync() SendOption // force synchronous delivery
func Async() SendOption // force background deliveryn.Send(ctx, user, OrderShipped{...}, notification.Via("mail")) // only mail
n.Send(ctx, user, OTPCode{Code: "123456"}, notification.Sync()) // block until sentErrors
The package exposes sentinel errors for comparison with errors.Is:
var (
ErrNoChannel = errors.New("notification: channel not registered")
ErrInvalidMessage = errors.New("notification: invalid message type for channel")
ErrNoRoute = errors.New("notification: notifiable has no route for channel")
ErrNoFormatter = errors.New("notification: notification has no formatter for channel")
ErrClosed = errors.New("notification: notifier closed")
ErrRateLimited = errors.New("notification: rate limit exceeded")
ErrRetriesExceeded = errors.New("notification: max retries exceeded")
)Recommended singleton pattern
var (
notifier *notification.Notifier
notifierOnce sync.Once
)
func Notifier() *notification.Notifier {
notifierOnce.Do(func() {
notifier = notification.New(notification.Config{
WorkerPool: 20,
MaxRetries: 3,
OnError: func(ctx context.Context, n notification.Notifiable, channel string, err error) {
slog.Error("notification failed", "channel", channel, "id", n.GetID(), "err", err)
},
})
// Register channels once at boot.
notifier.RegisterChannel(mailgun.New(mailgun.Config{ /* ... */ }))
})
return notifier
}Avoid creating notifiers per request — every instance starts its own worker pool.