Telego Predicate-Based Routing: A Complete Guide to Telegram Bot Update Handling
Telego routes incoming Telegram updates to handlers using boolean predicate functions that inspect update contents, enabling composable filtering through logical operators like And, Or, and Not instead of manual conditional statements.
Telego is a comprehensive Go library for building Telegram bots. At the heart of its telegohandler package lies a predicate-based routing system that decouples update filtering from business logic. This architecture allows developers to construct complex routing trees using simple, reusable boolean functions rather than writing nested if statements.
Core Concepts of Telego Predicate-Based Routing
The routing mechanism rests on three fundamental structures defined in telegohandler/handler_group.go and telegohandler/predicates.go.
The Predicate Type
A Predicate is a first-class function that evaluates whether an update matches specific criteria:
// telegohandler/predicates.go
type Predicate func(context.Context, telego.Update) bool
Predicates receive the update context and the update itself, returning true only if the update should be routed to the associated handler.
Route Structure
The unexported route struct encapsulates a single routing entry:
// telegohandler/handler_group.go#L18-L31
type route struct {
predicates []Predicate
handler Handler
group *HandlerGroup
}
Each route maintains a slice of predicates that must all evaluate to true for the route to match. Routes can terminate in a handler function, a middleware chain, or a nested sub-group.
HandlerGroup Tree
HandlerGroup forms a hierarchical tree structure:
// telegohandler/handler_group.go
type HandlerGroup struct {
parent *HandlerGroup
routes []route
// ... other fields
}
The root group receives all updates and traverses its routes recursively. Child groups created via Group() inherit the parent's context but maintain their own predicate logic.
How Route Matching Works
When BotHandler receives an update, it delegates to HandlerGroup.HandleUpdate, which creates a Context and invokes Context.Next.
The Routing Algorithm
The Next method implements a depth-first traversal with backtracking:
// telegohandler/context.go#L10-L41
func (c *Context) Next(update telego.Update) error {
// iterate over the current group's routes
for i := c.stack[len(c.stack)-1] + 1; i < len(c.group.routes); i++ {
r := c.group.routes[i]
if r.match(c.ctx, update) { // ← predicate check
c.stack[len(c.stack)-1] = i
if r.handler != nil { // handler or middleware
return r.handler(c, update)
}
// sub-group: descend recursively
c.group = r.group
c.stack = append(c.stack, -1)
return c.Next(update)
}
}
// back-track to parent groups when nothing matches
if c.group.parent != nil && c.group != c.finalGroup {
c.group = c.group.parent
c.stack = c.stack[:len(c.stack)-1]
return c.Next(update)
}
return nil
}
Predicate Evaluation
The route.match method evaluates predicates against a cloned update to prevent mutation:
// telegohandler/handler_group.go#L18-L31
func (r route) match(ctx context.Context, update telego.Update) bool {
if len(r.predicates) == 0 {
return true // no predicates → always match
}
// work on a copy so predicates cannot mutate the original update
update = update.Clone()
for _, p := range r.predicates {
if !p(ctx, update) {
return false
}
}
return true
}
If all predicates return true, the route matches and the handler executes.
Built-in Predicates
The telegohandler/predicates.go file provides over 70 ready-to-use predicates covering every Telegram update field:
| Predicate | Description | Signature Reference |
|---|---|---|
Any() |
Always returns true |
predicates.go#L11-L15 |
None() |
Always returns false |
predicates.go#L18-L22 |
And(p1, p2, …) |
Logical AND of predicates | predicates.go#L25-L34 |
Or(p1, p2, …) |
Logical OR of predicates | predicates.go#L37-L46 |
Not(p) |
Logical NOT of a predicate | predicates.go#L49-L54 |
AnyMessage() |
Update contains a non-nil Message |
predicates.go#L60-L64 |
AnyMessageWithText() |
Message present with non-empty Text |
predicates.go#L71-L75 |
CommandEqual("start") |
Message is command /start (case-insensitive) |
predicates.go#L25-L40 |
TextContains("hello") |
Message text contains substring | predicates.go#L44-L48 |
CallbackDataPrefix("pay_") |
Callback query data starts with prefix | predicates.go#L30-L34 |
All predicates compose naturally with And, Or, and Not, enabling complex filtering without nested conditionals.
Registering Routes and Middleware
The HandlerGroup API in telegohandler/handler_group.go provides three primary registration methods.
Handle
Registers a handler function with optional predicates:
// telegohandler/handler_group.go#L82-L100
func (h *HandlerGroup) Handle(handler Handler, predicates ...Predicate) {
h.routes = append(h.routes, route{
predicates: predicates,
handler: handler,
})
}
Group
Creates a nested sub-group that inherits predicates:
// telegohandler/handler_group.go#L101-L121
func (h *HandlerGroup) Group(predicates ...Predicate) *HandlerGroup {
group := &HandlerGroup{parent: h}
h.routes = append(h.routes, route{
predicates: predicates,
group: group,
})
return group
}
Use
Registers middleware that executes before handlers:
// telegohandler/handler_group.go#L123-L141
func (h *HandlerGroup) Use(middlewares ...Handler) {
for _, m := range middlewares {
h.routes = append(h.routes, route{handler: m})
}
}
Practical Example: Command Echo Bot
This complete example demonstrates Telego predicate-based routing with middleware, grouped routes, and command filtering:
package main
import (
"context"
"log"
"github.com/mymmrac/telego"
"github.com/mymmrac/telego/telegohandler"
)
func main() {
// Initialise bot (token omitted for security)
bot, err := telego.NewBot("YOUR_BOT_TOKEN")
if err != nil {
log.Fatal(err)
}
// Create a handler group
h := telegohandler.NewBotHandler(bot)
// Middleware that logs every incoming update
h.Use(func(ctx *telegohandler.Context, upd telego.Update) error {
log.Printf("update %d from chat %d", upd.UpdateID, upd.Message.Chat.ID)
return ctx.Next(upd) // continue routing
})
// Echo only text messages that start with "/say"
h.Group(
telegohandler.CommandPrefix("/say"), // matches /say, /sayHello, etc.
telegohandler.AnyMessageWithText(), // ensure we have text
).HandleMessage(func(ctx *telegohandler.Context, msg telego.Message) error {
// Echo the text after the command
reply := telego.NewMessage(msg.Chat.ID, msg.Text[len("/say"):])
_, err := bot.SendMessage(reply)
return err
})
// Start long-polling
err = bot.StartLongPolling(context.Background())
if err != nil {
log.Fatal(err)
}
}
Key implementation details:
CommandPrefix("/say")andAnyMessageWithText()combine implicitly through theGroupmethod, creating a predicate slice that requires both conditions.- The middleware calls
ctx.Next(upd)to pass control to the next matching route, enabling the middleware chain pattern. HandleMessageregisters a final handler that executes only when all grouped predicates returntrue.
Creating Custom Predicates
When built-in predicates don't cover specific business logic, implement the Predicate type directly:
func FromUser(username string) telegohandler.Predicate {
return func(_ context.Context, upd telego.Update) bool {
return upd.Message != nil && upd.Message.From != nil &&
strings.EqualFold(upd.Message.From.Username, username)
}
}
Usage follows the same pattern as library predicates:
h.Group(telegohandler.AnyMessage(), FromUser("alice")).HandleMessage(myHandler)
This composability enables sophisticated routing trees—such as admin-only commands or feature-flagged responses—without modifying the core handler logic.
Summary
- Predicate-based routing in Telego uses boolean functions (
func(context.Context, telego.Update) bool) to filter incoming updates, defined intelegohandler/predicates.go. - The
routestruct intelegohandler/handler_group.goaggregates predicates and associates them with handlers, middleware, or sub-groups. Context.Nextintelegohandler/context.goimplements a depth-first traversal with backtracking, evaluating all predicates in a route viaroute.matchbefore executing handlers.- Built-in predicates cover message types, commands, text content, callback queries, and logical operations (
And,Or,Not), all composable through theHandlerGroupAPI. - Custom predicates implement the same function signature, enabling domain-specific routing logic while maintaining type safety.
Frequently Asked Questions
How does Telego handle multiple predicates in a single route?
When multiple predicates are passed to Handle() or Group(), Telego evaluates them as a logical AND operation. In telegohandler/handler_group.go, the route.match method iterates through all predicates in the slice and returns false immediately if any predicate returns false. The update is cloned before evaluation to prevent mutation by individual predicates.
What is the difference between Handle and Group in Telego?
Handle registers a terminal handler function that executes when all predicates match, storing it in a route struct with the handler field set. Group creates a nested HandlerGroup that can contain its own routes, middleware, and sub-groups, enabling hierarchical routing trees. Groups are useful for applying common predicates (like admin checks) to multiple handlers without repetition.
How does Telego predicate-based routing compare to regex-based routing?
Telego predicates are type-safe boolean functions that inspect struct fields directly, offering compile-time safety and better performance than string parsing. Regex-based routing typically matches against raw update text or JSON strings, which is flexible but error-prone and slower for complex conditions. Telego's approach allows logical composition (And, Or, Not) and direct access to Telegram API types like Message, CallbackQuery, and Chat.
Can predicates modify the update during routing?
No, predicates receive a clone of the update, not the original. In telegohandler/handler_group.go, the route.match method calls update.Clone() before iterating through predicates. This design prevents side effects where one predicate might mutate the update (e.g., trimming text) and affect subsequent predicates or the final handler. The original update remains immutable during the routing phase.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →