perf(client): static headers + bool fast-path in decodeResult

Two changes on the Call hot path:

* Replace httpReq.Header.Set("Content-Type", "application/json") (and Accept) with direct map writes against a package-level []string. Both keys are already canonical so the canonicalising path inside Header.Set was pure overhead; saves the per-call []string{val} allocation x2.

* Add a bool fast-path in decodeResult: ~60% of Telegram methods return bool, and the API emits the envelope with no whitespace, so a bytes.Equal check against the two canonical bodies short-circuits the generic Result[bool] Unmarshal entirely. any(true).(Resp) does not box thanks to Go's static bool interface values.

Combined effect on Call_BoolResponse: 18 -> 14 allocs/op, 634ns -> 526ns. DecodeResult_Bool isolation bench: 50ns / 2 allocs -> 2.87ns / 0 allocs.
This commit is contained in:
2026-05-10 02:32:00 +01:00
parent 728b28b0c5
commit da27421521
3 changed files with 43 additions and 6 deletions
+17
View File
@@ -0,0 +1,17 @@
After: static-header-slices + bool-fast-path
goos: darwin
goarch: arm64
pkg: github.com/lukaszraczylo/go-telegram/client
cpu: Apple M4 Max
BenchmarkCall_BoolResponse-16 4597374 526.3 ns/op 1842 B/op 14 allocs/op
BenchmarkCall_StructResponse-16 3740096 679.6 ns/op 1973 B/op 16 allocs/op
BenchmarkEncodeJSONBody-16 41997352 58.35 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 863273641 2.872 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 19988096 100.3 ns/op 144 B/op 2 allocs/op
Deltas vs baseline (allocs/op):
Call_BoolResponse: 18 -> 14 (-4)
Call_StructResponse: 18 -> 16 (-2)
DecodeResult_Bool: 2 -> 0 (-2, also -94% ns)
DecodeResult_Struct: 2 -> 2 (flat)
EncodeJSONBody: 2 -> 2 (flat)
+24 -4
View File
@@ -10,6 +10,12 @@ import (
"reflect"
)
var (
headerJSONValue = []string{"application/json"}
rawOKTrueBody = []byte(`{"ok":true,"result":true}`)
rawOKFalseBody = []byte(`{"ok":true,"result":false}`)
)
// Call is the single point through which every Telegram Bot API method
// invocation flows. It marshals the request, signs the URL with the bot
// token, dispatches via HTTPDoer, decodes the Result envelope, and
@@ -44,8 +50,8 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
if err != nil {
return zero, &NetworkError{Err: err}
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header["Content-Type"] = headerJSONValue
httpReq.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(httpReq)
if err != nil {
@@ -91,8 +97,8 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json
if err != nil {
return nil, &NetworkError{Err: err}
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header["Content-Type"] = headerJSONValue
httpReq.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(httpReq)
if err != nil {
@@ -139,8 +145,22 @@ func encodeJSONBody(codec Codec, req any) (io.Reader, error) {
// decodeResult unmarshals raw into Result[Resp] and translates non-OK
// responses into *APIError.
//
// Bool fast path: ~60% of Telegram methods return bool. The Telegram API
// emits the result envelope with no whitespace, so a byte-equality check
// against the two canonical bodies skips the generic Unmarshal entirely.
// Anything that doesn't match exactly (e.g. responses with extra fields,
// errors) falls through to the slow path.
func decodeResult[Resp any](codec Codec, raw []byte) (Resp, error) {
var zero Resp
if _, isBool := any(zero).(bool); isBool {
switch {
case bytes.Equal(raw, rawOKTrueBody):
return any(true).(Resp), nil
case bytes.Equal(raw, rawOKFalseBody):
return any(false).(Resp), nil
}
}
var env Result[Resp]
if err := codec.Unmarshal(raw, &env); err != nil {
return zero, &ParseError{Err: err, Body: copyBody(raw)}
+2 -2
View File
@@ -69,7 +69,7 @@ func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp mult
return zero, &NetworkError{Err: err}
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(req)
if err != nil {
@@ -125,7 +125,7 @@ func callMultipartRaw(ctx context.Context, b *Bot, method string, mp multipartRe
return nil, &NetworkError{Err: err}
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(req)
if err != nil {