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=300ms -count=6,
median of six 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 . -benchmemWhat 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 871 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 binder reads
path, query and body. Gin needs a call per source. 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 leads on the query string and on filling a struct from every source, and is level with Echo on a JSON body. It allocates more than the framework binders on that JSON body, some of which is the cost of putting the body back for middleware to read — a guarantee the others do not make.
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 | 661 | 672 | 14 |
| Echo | 871 | 544 | 8 |
| Gin | 1,104 | 608 | 9 |
| gorilla/schema | 1,944 | 1,367 | 46 |
Reading fields by name with strconv is the floor, and it is
1.8× 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.
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 |
|---|---|---|---|
| hand-written stdlib | 741 | 681 | 8 |
| binder | 799 | 1,240 | 23 |
| Echo | 816 | 681 | 8 |
| Gin | 856 | 681 | 8 |
Binder is the quickest of the three by a small margin, and within eight
per cent of decoding straight into the struct by hand. It also
allocates roughly three times as much. Echo and Gin hand
the body to encoding/json and let it fill the struct;
binder walks the body itself so that a member can be converted by its own
rules — a JSON string into an integer field, a
TextUnmarshaler, a value bound alongside a path parameter.
Values that do not take the direct path still pass through an interface,
and that is where the extra allocations are.
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 three plus two manual reads, Gin takes four. The comparison is of the whole job.
| Implementation | ns/op | B/op | allocs/op |
|---|---|---|---|
| binder — one call | 1,131 | 1,928 | 27 |
| Echo — three calls plus manual reads | 1,327 | 1,202 | 15 |
| Gin — four calls plus manual reads | 1,464 | 1,644 | 21 |
This is the case binder exists for, and the margin is still only about fifteen per cent. The stronger argument is the one the table cannot show: one call and one error, against 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 it allocates more than Echo or Gin on a JSON body. Both are true, and on any endpoint that touches a database neither matters: the whole spread here is under a microsecond.
If you already use Echo or Gin, their binders are built in and near
enough in cost that switching for speed 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.