binder / Docs / Comparison

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.

Read the method before the tables. Two of the figures here changed materially once a measurement fault was found, and one of those faults had been flattering a competitor. A benchmark you cannot reproduce is an advertisement.

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=10

What 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

Query string, 5 fields hand-written stdlib: 363 hand-written stdlib 363 binder: 520 binder 520 Echo: 874 Echo 874 Gin: 1104 Gin 1,104 gorilla/schema: 1934 gorilla/schema 1,934 JSON body, 5 fields binder: 599 binder 599 hand-written stdlib: 743 hand-written stdlib 743 Echo: 820 Echo 820 Gin: 864 Gin 864 Every source at once binder: 606 binder 606 Echo: 1336 Echo 1,336 Gin: 1474 Gin 1,474
Nanoseconds per bind — lower is better. Shared scale across the three panels, so the bars are comparable between them.

Allocations per bind

Query string, 5 fields binder: 1 binder 1 hand-written stdlib: 7 hand-written stdlib 7 Echo: 8 Echo 8 Gin: 9 Gin 9 gorilla/schema: 46 gorilla/schema 46 JSON body, 5 fields binder: 8 binder 8 hand-written stdlib: 8 hand-written stdlib 8 Echo: 8 Echo 8 Gin: 8 Gin 8 Every source at once binder: 6 binder 6 Echo: 15 Echo 15 Gin: 21 Gin 21
Allocations per bind — lower is better. Shared scale across the three panels, so the bars are comparable between them.

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.

Implementationns/opB/opallocs/op
hand-written stdlib3634807
binder520641
Echo8745448
Gin1,1046089
gorilla/schema1,9341,36746

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.

Implementationns/opB/opallocs/op
binder5992568
hand-written stdlib7436818
Echo8206818
Gin8646818

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.

Implementationns/opB/opallocs/op
binder — one call6062486
Echo — two calls plus manual reads1,3361,20215
Gin — three calls plus manual reads1,4741,64421

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.

Measured on one machine, one Go version and one shape of struct, against the library versions named above. Results move with all four. Run go test -bench . in benchmarks/ on your own hardware before drawing a conclusion that matters to you.