Comparison
Numbers for one library on its own say little. These bind the same request into the same shape of struct with binder, Echo, Gin, gorilla/schema and hand-written standard library code, so the cost of each approach can be read against the others.
Method
Apple M-series laptop, Go 1.27, -benchtime=200ms -count=10,
median of ten runs. Libraries at Echo v4.15.4, Gin v1.12.0,
gorilla/schema v1.4.1.
The comparison lives in its own Go module, so nothing it imports reaches binder's own go.mod:
cd benchmarks
go test -bench . -benchmem -benchtime=200ms -count=10What was held equal
Framework setup is kept out of the timed loop wherever the library allows
a context to be reused, since constructing a context is not binding. The
request is built once and its body re-armed between iterations, so
httptest.NewRequest is not folded into any figure — it costs
more memory than the binding under test.
The correction worth knowing about
Echo caches the parsed query on its context, and
SetRequest does not clear it. Reusing one context across
iterations therefore measures a single parse for the whole run: Echo
came out at 556 ns and one allocation, a figure no real
server would ever see. The loop now calls Reset, which is
what Echo's own server calls per request, and Echo measures 874 ns and
eight allocations.
If you reproduce these and get Echo faster than binder, this is why. The uncorrected number favoured Echo; correcting it favoured binder, so it is stated here rather than left in a commit message.
What is not equal
The libraries do not do the same amount of work, and no weighting can
make them. Binder reads every source in one call. Echo's
Bind reads path and body, and the query only on GET, DELETE
and HEAD requests, so a POST needs BindQueryParams as well.
Gin needs a call per source. Both frameworks can bind headers with a
further call, and neither binds cookies. gorilla/schema
decodes a url.Values and nothing else. Each table below says
what was asked of each.
At a glance
Both measures, all three shapes. Every bar is labelled and every number appears in the tables below, so nothing here depends on telling the colours apart — binder is picked out only to save you hunting for it.
Time per bind
Allocations per bind
The shape of it: binder is the quickest of the binders in all three shapes, and allocates the least or ties. Filling a struct from every source, the case it exists for, it is 2.2× faster than Echo with six allocations against fifteen. On a JSON body every implementation makes eight allocations, and binder's include one the others do not pay: putting the body back so middleware can read it again.
Query string, five fields
Every library binds the same five query parameters into a struct of string, int, bool, string, string.
| Implementation | ns/op | B/op | allocs/op |
|---|---|---|---|
| hand-written stdlib | 363 | 480 | 7 |
| binder | 520 | 64 | 1 |
| Echo | 874 | 544 | 8 |
| Gin | 1,104 | 608 | 9 |
| gorilla/schema | 1,934 | 1,367 | 46 |
Reading fields by name with strconv is the floor on time,
1.4× faster than the quickest reflective binder. That gap is what
reflection costs; whether it is worth paying depends on how many handlers
you would otherwise write by hand.
On allocations binder is below the floor. The hand-written version calls
r.URL.Query(), which builds a map with a slice per key;
binder scans the raw query for each parameter a field asks for, and a
value with nothing to unescape is a substring of the URL rather than a
copy. Its one allocation is the target escaping to the heap when it is
passed to Bind.
JSON body, five fields
The same five values, this time as a JSON object. gorilla/schema is absent because it does not read bodies.
| Implementation | ns/op | B/op | allocs/op |
|---|---|---|---|
| binder | 599 | 256 | 8 |
| hand-written stdlib | 743 | 681 | 8 |
| Echo | 820 | 681 | 8 |
| Gin | 864 | 681 | 8 |
Binder is the quickest here, ahead even of
json.NewDecoder(r.Body).Decode by hand, with the same eight
allocations and under two fifths of the memory. Echo and Gin hand the
body to encoding/json and let it fill the struct. Binder
reads the whole body into one buffer sized from
Content-Length, up to 64 KB before any of it arrives, then
walks it with a pooled
jsontext decoder, writing each member straight into its
field. A member no field binds is skipped without being decoded, and a
member can still be converted by its own rules: a JSON string into an
integer field, or a TextUnmarshaler.
Every source at once
Filling one struct from a path value, a query parameter, a JSON body, a header and a cookie. Binder does it in one call; Echo takes two bind calls plus two manual reads, Gin three plus two. The comparison is of the whole job.
| Implementation | ns/op | B/op | allocs/op |
|---|---|---|---|
| binder — one call | 606 | 248 | 6 |
| Echo — two calls plus manual reads | 1,336 | 1,202 | 15 |
| Gin — three calls plus manual reads | 1,474 | 1,644 | 21 |
This is the case binder exists for: 2.2× faster than Echo, with six allocations against fifteen and about a fifth of the memory. The stronger argument is still the one the table cannot show: one call, and one error listing every field that failed, against three or four calls whose failures you have to combine yourself.
What to take from this
Binder is the quickest of the reflective binders measured, in all three shapes, and makes the fewest allocations or ties. On any endpoint that touches a database none of this is the bottleneck: the whole spread is under two microseconds. What the allocation counts buy is less work for the garbage collector under load.
If you already use Echo or Gin, their binders are built in and near
enough in cost that switching for speed alone would be difficult to
justify.
The reason to reach for binder is that you are on
net/http and want one call to fill a struct from every part
of the request. That these numbers are competitive is the point — it
means choosing it costs you nothing.
go test -bench . in benchmarks/ on your own
hardware before drawing a conclusion that matters to you.