Adds test/benchmarks/ as a separate Go module so competitor deps
(go-telegram-bot-api/v5, telebot.v3, go-telegram/bot, telego,
echotron/v3) stay out of the root go.mod.
Hot paths covered:
- Webhook decode (small Update -> typed Update struct)
- Large unmarshal (Update with entities + reply markup + photo array)
- API round-trip (sendMessage against httptest.Server)
- Dispatch route (20 handlers, last-registered matches)
Results on Apple M4 Max / go1.26.2: ours wins 3 of 4 paths and is
2nd of 5 in the round-trip path. Full report at
docs/benchmarks/2026-05-10-comparison.md, raw output committed under
test/benchmarks/results/.
Caveats called out in the report:
- codec asymmetry (we ship goccy/go-json; competitors mostly stdlib)
- echotron call bench skipped — built-in rate limiter not externally
configurable; would measure throttling, not the library
- dispatch bench limited to libs with a public sync entry point
(ours, telebot, gobot); gotba has no dispatcher, telego/echotron
use channel/per-chat paradigms not directly comparable
Also gitignores docs/superpowers/ (local brainstorm/spec scratch)
and regenerates docs/reference/dispatch.md after the new
Router.Process method.
29 KiB
dispatch
import "github.com/lukaszraczylo/go-telegram/dispatch"
Package dispatch provides a typed router for Telegram updates. It consumes any transport.Updater and dispatches updates to handlers registered by command, regex, or update-payload kind.
Index
- Variables
- type Context
- type Filter
- type Handler
- type Middleware
- type NamedHandlers
- type Router
- func New(b *client.Bot, opts ...RouterOption) *Router
- func (r *Router) Group(group int) *RouterScope
- func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
- func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
- func (r *Router) OnCallbackFilter(f Filter[*api.CallbackQuery], h Handler[*api.CallbackQuery])
- func (r *Router) OnChannelPost(h Handler[*api.Message])
- func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
- func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
- func (r *Router) OnChatJoinRequestFilter(f Filter[*api.ChatJoinRequest], h Handler[*api.ChatJoinRequest])
- func (r *Router) OnChatMember(h Handler[*api.ChatMemberUpdated])
- func (r *Router) OnChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
- func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
- func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
- func (r *Router) OnEditedChannelPost(h Handler[*api.Message])
- func (r *Router) OnEditedMessage(h Handler[*api.Message])
- func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
- func (r *Router) OnInlineQueryFilter(f Filter[*api.InlineQuery], h Handler[*api.InlineQuery])
- func (r *Router) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
- func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
- func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdated])
- func (r *Router) OnMyChatMember(h Handler[*api.ChatMemberUpdated])
- func (r *Router) OnMyChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
- func (r *Router) OnPoll(h Handler[*api.Poll])
- func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
- func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
- func (r *Router) OnPreCheckoutQueryFilter(f Filter[*api.PreCheckoutQuery], h Handler[*api.PreCheckoutQuery])
- func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased])
- func (r *Router) OnRemovedChatBoost(h Handler[*api.ChatBoostRemoved])
- func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
- func (r *Router) OnText(pattern string, h Handler[*api.Message])
- func (r *Router) Process(ctx context.Context, u *api.Update) error
- func (r *Router) Run(ctx context.Context, u transport.Updater) error
- func (r *Router) Use(mw Middleware[*api.Update])
- type RouterOption
- type RouterScope
Variables
ErrContinueGroups signals that this group's handler should be treated as not-matching when returned by a handler: dispatch moves on to the next handler in the same group, then to subsequent groups.
Without ErrContinueGroups, a non-error return from a matched handler stops dispatch (default first-match-wins semantics).
var ErrContinueGroups = errors.New("dispatch: continue groups")
ErrEndGroups stops dispatch from running any further handlers in any group for this update when returned by a handler. Use it to indicate the update has been definitively handled.
errors.Is(err, ErrEndGroups) is the canonical check, though dispatch itself recognises it by exact identity.
var ErrEndGroups = errors.New("dispatch: end groups")
type Context
Context bundles the per-update state every handler receives.
Ctx is the request context propagated from Router.Run; cancelling the run cancels every handler.
Bot is the API client. Handlers reply by calling api.SendMessage(c.Ctx, c.Bot, ...) etc.
Update is the raw update; payload-typed handlers also receive a narrowed pointer to one of its sub-fields.
Command, CommandArgs and RegexMatch are populated by the router for the matching route kind; they replace the previous "command", "command_args" and "regex_match" entries in Values, which were the only conventional keys. Values remains for user-defined custom keys.
Command is the matched bot command (e.g. "/start"); empty when the route is not a command match.
CommandArgs is everything after the command; empty when no command matched or the command had no trailing text.
RegexMatch is the regex sub-matches when an OnText/OnCallback regex route matched; nil otherwise.
Values is lazily allocated for user-defined keys. Handlers that don't write pay no allocation. Reads against a nil map return the zero value. Writers must use Set instead of indexing the map directly.
type Context struct {
Ctx context.Context
Bot *client.Bot
Update *api.Update
Command string
CommandArgs string
RegexMatch []string
Values map[string]any
}
func NewContext
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context
NewContext constructs a Context. Used by Router internally; exposed for custom test harnesses.
func (*Context) Set
func (c *Context) Set(key string, val any)
Set writes key/val into Values, allocating the map on first use. Use this instead of `c.Values[k] = v` so the no-write path stays allocation-free.
type Filter
Filter is a predicate over a typed payload (e.g. *api.Message). Filters compose via And/Or/Not for multi-condition matching.
Example:
f := message.HasPhoto().And(message.InChat(-100123456789))
type Filter[T any] func(payload T) bool
func All
func All[T any](filters ...Filter[T]) Filter[T]
All combines filters with AND. Returns a Filter that matches when all match. Returns a filter that always matches when filters is empty.
func Any
func Any[T any](filters ...Filter[T]) Filter[T]
Any combines filters with OR. Returns a Filter that matches when at least one matches. Returns a filter that never matches when filters is empty.
func (Filter[T]) And
func (f Filter[T]) And(others ...Filter[T]) Filter[T]
And returns a Filter that matches iff f and every one of others matches.
func (Filter[T]) Not
func (f Filter[T]) Not() Filter[T]
Not returns a Filter that inverts f.
func (Filter[T]) Or
func (f Filter[T]) Or(others ...Filter[T]) Filter[T]
Or returns a Filter that matches iff f matches OR any of others matches.
type Handler
Handler is a generic handler over update payload type T. T is typically *api.Message, *api.CallbackQuery, *api.InlineQuery, or *api.Update for global middleware.
type Handler[T any] func(ctx *Context, payload T) error
type Middleware
Middleware wraps a Handler[T] with cross-cutting behaviour (logging, recovery, auth). Middleware composition is left-to-right: Use(a,b,c) runs as a(b(c(handler))).
type Middleware[T any] func(Handler[T]) Handler[T]
func Chain
func Chain[T any](mws ...Middleware[T]) Middleware[T]
Chain composes a slice of middleware into a single Middleware[T].
func Recovery
func Recovery() Middleware[*api.Update]
Recovery returns middleware that recovers from panics in downstream handlers, converting them into a returned error and logging via the bot's configured logger. Registered automatically by NewRouter.
type NamedHandlers
NamedHandlers manages handlers by string name, allowing runtime registration, replacement, and removal. This complements the Router's registration methods: each registration via Named*() also gets a name for later lookup.
Use case: a plugin system that loads/unloads command handlers without restarting the bot.
type NamedHandlers[T any] struct {
// contains filtered or unexported fields
}
func NewNamedHandlers
func NewNamedHandlers[T any]() *NamedHandlers[T]
NewNamedHandlers returns a new, empty NamedHandlers[T].
func (*NamedHandlers[T]) Handler
func (n *NamedHandlers[T]) Handler() Handler[T]
Handler returns a single Handler[T] that runs each registered handler in registration order, first non-nil error stops the chain. Use this to wire NamedHandlers into a Router.OnXxx call:
names := dispatch.NewNamedHandlers[*api.Message]()
names.Set("logger", loggingHandler)
names.Set("audit", auditHandler)
router.OnCommand("/admin", names.Handler())
Subsequent Set/Remove calls take effect on the next dispatch.
func (*NamedHandlers[T]) Has
func (n *NamedHandlers[T]) Has(name string) bool
Has reports whether name is registered.
func (*NamedHandlers[T]) Names
func (n *NamedHandlers[T]) Names() []string
Names returns the registered names in registration order.
func (*NamedHandlers[T]) Remove
func (n *NamedHandlers[T]) Remove(name string) bool
Remove unregisters the handler under name. Returns true if it existed.
func (*NamedHandlers[T]) Set
func (n *NamedHandlers[T]) Set(name string, h Handler[T])
Set registers or replaces the handler under name. If name is new, it is appended to the end of the registration order.
type Router
Router dispatches updates from any Updater to typed handlers.
Matchers run in registration order; first match wins. A panic-recovery middleware is attached automatically and runs around every dispatch.
type Router struct {
// contains filtered or unexported fields
}
func New
func New(b *client.Bot, opts ...RouterOption) *Router
New constructs a Router. Recovery middleware is added by default; users can disable it by passing WithoutRecovery (not implemented here, but the hook is in place via Use).
func (*Router) Group
func (r *Router) Group(group int) *RouterScope
Group returns a RouterScope that registers handlers in the given group. Group 0 (the default) runs first, then group 1, etc. Within a group, handlers run in registration order; the first non-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
func (*Router) OnBusinessConnection
func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
OnBusinessConnection registers a handler for business connection updates.
func (*Router) OnCallback
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
OnCallback registers a handler for callback queries whose Data matches the regex.
Panics at registration time if pattern is not a valid regular expression.
func (*Router) OnCallbackFilter
func (r *Router) OnCallbackFilter(f Filter[*api.CallbackQuery], h Handler[*api.CallbackQuery])
OnCallbackFilter registers a typed callback-query handler gated by filter f. Filter routes are checked after pattern-based OnCallback routes; first match wins.
func (*Router) OnChannelPost
func (r *Router) OnChannelPost(h Handler[*api.Message])
OnChannelPost registers a handler for channel post updates.
func (*Router) OnChatBoost
func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
OnChatBoost registers a handler for chat boost updates.
func (*Router) OnChatJoinRequest
func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
OnChatJoinRequest registers a handler for chat join requests.
func (*Router) OnChatJoinRequestFilter
func (r *Router) OnChatJoinRequestFilter(f Filter[*api.ChatJoinRequest], h Handler[*api.ChatJoinRequest])
OnChatJoinRequestFilter registers a filtered handler for chat join requests.
func (*Router) OnChatMember
func (r *Router) OnChatMember(h Handler[*api.ChatMemberUpdated])
OnChatMember registers a handler for chat member status changes.
func (*Router) OnChatMemberFilter
func (r *Router) OnChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
OnChatMemberFilter registers a filtered handler for chat member status changes.
func (*Router) OnChosenInlineResult
func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
OnChosenInlineResult registers a handler for chosen inline results.
func (*Router) OnCommand
func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
OnCommand registers a handler for a slash command. The command string includes the leading slash (e.g. "/start"). Matching strips an optional "@BotName" suffix.
func (*Router) OnEditedChannelPost
func (r *Router) OnEditedChannelPost(h Handler[*api.Message])
OnEditedChannelPost registers a handler for edited channel post updates.
func (*Router) OnEditedMessage
func (r *Router) OnEditedMessage(h Handler[*api.Message])
OnEditedMessage registers a handler for edited message updates.
func (*Router) OnInlineQuery
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
OnInlineQuery registers a handler for inline queries (one matcher only; inline queries are not partitioned by content here).
func (*Router) OnInlineQueryFilter
func (r *Router) OnInlineQueryFilter(f Filter[*api.InlineQuery], h Handler[*api.InlineQuery])
OnInlineQueryFilter registers an inline-query handler gated by filter f. Filter routes are checked after bare OnInlineQuery handlers; first match wins.
func (*Router) OnMessageFilter
func (r *Router) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
OnMessageFilter registers a typed message handler gated by filter f. Filter routes are checked after command and text routes; first match wins.
func (*Router) OnMessageReaction
func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
OnMessageReaction registers a handler for message reaction updates.
func (*Router) OnMessageReactionCount
func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdated])
OnMessageReactionCount registers a handler for anonymous message reaction count updates.
func (*Router) OnMyChatMember
func (r *Router) OnMyChatMember(h Handler[*api.ChatMemberUpdated])
OnMyChatMember registers a handler for bot's own chat member status changes.
func (*Router) OnMyChatMemberFilter
func (r *Router) OnMyChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
OnMyChatMemberFilter registers a filtered handler for bot's own chat member status changes.
func (*Router) OnPoll
func (r *Router) OnPoll(h Handler[*api.Poll])
OnPoll registers a handler for poll state updates.
func (*Router) OnPollAnswer
func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
OnPollAnswer registers a handler for poll answer updates.
func (*Router) OnPreCheckoutQuery
func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
OnPreCheckoutQuery registers a handler for pre-checkout queries.
func (*Router) OnPreCheckoutQueryFilter
func (r *Router) OnPreCheckoutQueryFilter(f Filter[*api.PreCheckoutQuery], h Handler[*api.PreCheckoutQuery])
OnPreCheckoutQueryFilter registers a filtered handler for pre-checkout queries.
func (*Router) OnPurchasedPaidMedia
func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased])
OnPurchasedPaidMedia registers a handler for purchased paid media updates.
func (*Router) OnRemovedChatBoost
func (r *Router) OnRemovedChatBoost(h Handler[*api.ChatBoostRemoved])
OnRemovedChatBoost registers a handler for removed chat boost updates.
func (*Router) OnShippingQuery
func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
OnShippingQuery registers a handler for shipping queries.
func (*Router) OnText
func (r *Router) OnText(pattern string, h Handler[*api.Message])
OnText registers a handler for messages whose Text matches the regex.
Panics at registration time if pattern is not a valid regular expression.
func (*Router) Process
func (r *Router) Process(ctx context.Context, u *api.Update) error
Run consumes the Updater and dispatches each update. It blocks until the Updater's channel is closed or ctx is cancelled.
By default updates are processed concurrently (up to WithMaxConcurrency(50) goroutines). Handlers for different updates may therefore run simultaneously; shared state must be protected. Pass WithMaxConcurrency(0) to New to restore serial (legacy) behaviour.
Run waits for all in-flight handlers to finish before returning. Process runs a single update through the router's middleware and handler chain synchronously. Entry point for callers sourcing updates outside the standard transport.Updater flow — custom webhook frameworks, message-bus consumers, or tests driving the router without spinning up Run.
Honours the router's global middleware (Use) but bypasses the concurrency semaphore wired up by Run; the caller controls parallelism.
func (*Router) Run
func (r *Router) Run(ctx context.Context, u transport.Updater) error
func (*Router) Use
func (r *Router) Use(mw Middleware[*api.Update])
Use registers a global middleware applied to every Update dispatch.
type RouterOption
RouterOption configures a Router at construction time.
type RouterOption func(*Router)
func WithMaxConcurrency
func WithMaxConcurrency(n int) RouterOption
WithMaxConcurrency sets the maximum number of updates processed in parallel. Default is 50. Pass 0 to dispatch serially (one update at a time, in the calling goroutine — the legacy behaviour before v1.1.0).
Note: concurrent dispatch means handlers for different updates may run simultaneously. Handlers that mutate shared state must be safe for concurrent access.
type RouterScope
RouterScope registers handlers into a specific priority group on its parent Router. Group 0 runs first, then group 1, etc. Within a group, handlers run in registration order; the first non-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
type RouterScope struct {
// contains filtered or unexported fields
}
func (*RouterScope) OnCommand
func (s *RouterScope) OnCommand(cmd string, h Handler[*api.Message])
OnCommand registers a command handler in this group.
func (*RouterScope) OnMessageFilter
func (s *RouterScope) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
OnMessageFilter registers a filter-based message handler in this group.
func (*RouterScope) OnText
func (s *RouterScope) OnText(pattern string, h Handler[*api.Message])
OnText registers a regex text handler in this group. Panics at registration time if pattern is not a valid regular expression.
Generated by gomarkdoc