Files
go-telegram/client/client.go
T
lukaszraczylo 75c7ce3119 perf(client): pool req-body buffer + manual http.Request with cached URL
Two changes that together cut allocs/call from 15 to 13 (client-internal
bench) and per-call CPU from 600ns to 455ns (-24%) on the no-HTTP path:

1. Codec gets an optional BodyEncoder extension (MarshalTo io.Writer).
   When present, encodeJSONBody stream-encodes the request directly into
   a pooled *bytes.Buffer instead of allocating a [2-step] Marshal+Reader
   pair. DefaultCodec implements it via goccy/go-json.NewEncoder.
2. *Bot caches the parsed base URL on construction. buildRequest skips
   net/http.NewRequestWithContext for the common case and constructs
   *http.Request manually — clones the URL by value, sets the method
   path, and populates ContentLength + GetBody from the body's concrete
   type so RetryDoer's body-replay across attempts still works.

Cross-library bench (sendMessage round-trip vs httptest.Server): -2
allocs/call (104 -> 102), bytes -1.2%, time within noise (real HTTP
plumbing dominates). The CPU win is visible on synthetic stub-doer
benches and translates to lower GC pressure on sustained-throughput
workloads.

Slow-path fallback preserved for codecs that don't implement BodyEncoder
and for *Bot instances where url.Parse on the configured base failed —
they take the original NewRequestWithContext path.
2026-05-10 22:36:57 +01:00

68 lines
2.1 KiB
Go

package client
import (
"net/url"
)
const defaultBaseURL = "https://api.telegram.org"
// Bot is the Telegram Bot API client. Construct via New. All API methods
// (declared in package api) hang off *Bot via thin wrappers around call.
type Bot struct {
token string
base string
http HTTPDoer
codec Codec
logger Logger
// baseURL is the parsed form of base, lazily populated on first Call.
// Caching it avoids running url.Parse on every API request.
baseURL *url.URL
// pathPrefix is "/bot<token>/" built once so per-call URL assembly
// is a single string concatenation with the method name.
pathPrefix string
}
// Token returns the bot token. Exposed for advanced use cases (custom
// transports, manual URL building); ordinary code does not need it.
func (b *Bot) Token() string { return b.token }
// BaseURL returns the configured Telegram API base URL.
func (b *Bot) BaseURL() string { return b.base }
// HTTP returns the underlying HTTPDoer. Exposed for adapters that need
// to share connection pools or for diagnostic checks.
func (b *Bot) HTTP() HTTPDoer { return b.http }
// Codec returns the configured Codec.
func (b *Bot) Codec() Codec { return b.codec }
// Logger returns the configured Logger.
func (b *Bot) Logger() Logger { return b.logger }
// New constructs a Bot with the given token and optional configuration.
// The default HTTP client is tuned for long-poll workloads (see
// NewDefaultHTTPDoer); the default codec wraps encoding/json; the default
// logger discards records.
func New(token string, opts ...Option) *Bot {
b := &Bot{
token: token,
base: defaultBaseURL,
http: NewDefaultHTTPDoer(),
codec: DefaultCodec{},
logger: NoopLogger{},
}
for _, o := range opts {
o(b)
}
// Pre-compute URL pieces. Errors here are unlikely (defaultBaseURL is
// well-formed; user-supplied bases via WithBaseURL are validated by
// url.Parse below) but if parsing fails we leave baseURL nil and fall
// back to the string-concat path on the next Call.
if u, err := url.Parse(b.base); err == nil {
b.baseURL = u
}
b.pathPrefix = "/bot" + b.token + "/"
return b
}