A software article by Efron Licht.
December 2025
In my experience, Go is the best general-purpose programming language for backend development, and a huge part of this comes from the thoughtful design of it’s standard libraries. If you are willing to be a little bit patient, read the documentation, and spend some time getting familiar with their idioms, you have everything you need without needing to go far afield.
Most programmers are not willing to be a little bit patient. They google ‘go web framework’ and then they pick the first web result. More than likely, this is Gin, a kind of insidous fungus masquerading as a software library.
Like many fungi,
Gin is not the only bad library - in fact, it’s not nearly the worst library in common usage - but it is the library that pisses me off the most day to day, and I think it’s emblematic of many of the biggest flaws of software library design writ large.
autogenerated on 2025-12-09
Before we begin:
net/http is somehow “wrong”. I have happily used libraries like gorilla/mux and it’s associated friends and I see nothing wrong with, for example, chi. I am using the standard library as a comparison because it’snet/http and ginOK, let’s get to it. On a surface level, basic HTTP work doesn’t look too different in net/http and Gin:
net/http 1func main() {
2 // route METHOD / PATH to handlers: here, GET /ping.
3 mux := http.NewServeMux()
4 mux.HandleFunc("GET /ping", func(w http.ResponseWriter, r *http.Request) {
5 w.WriteHeader(200)
6 json.NewEncoder(w).Encode(map[string]string {
7 "message": "pong",
8 })
9 })
10 // Create a HTTP server...
11 srv := http.Server {
12 Handler: mux, // that uses our router...
13 Addr: ":8080", // on port 8080
14 }
15 srv.ListenAndServe()
16}
1func main() {
2 // create a default gin router / server / engine
3 r := gin.Default()
4 // route METHOD / PATH to handlers: here, GET /ping.
5 r.GET("/ping", func(c *gin.Context) {
6 c.JSON(http.StatusOK, gin.H{
7 "message": "pong",
8 })
9 })
10 r.Run()
11}
On a surface impression, gin might seem easier- it’s slightly fewer lines of code, and there seems to be less configuration.
But this is all surface.
The proper way to judge a map is by the terrain it covers. In other words, before you choose any software, first you should know the problem you’re trying to solve with it. So before we pick on Gin, let’s review that terrain - HTTP.
Happily, HTTP is not that complicated and we can go over the basics in about ninety seconds and a handful of chalk drawings.
The HyperText Transport Protocol has a client send HTTP Requests, and a server responds with HTTP Responses.
A client sends a HTTP Request to a server. The server parses the request, figures out what the client wants, and sends back a HTTP Response.
This is very quick and dirty. If you want more details on the structure of HTTP, my article series ‘Backend from the Beginning’ builds an entire HTTP library from scratch and goes over all these parts in detail.
HTTP Requests have four main parts, separated by newlines:
I.E, they look like this:


HTTP Responses have a similar structure, with four main parts, separated by newlines


These parts are ordered - you can’t change your mind about the request or status line once you’ve sent them.
Once you’ve sent the body, you (usually) can’t send any more headers.
You don’t have to send the whole body at once on either side - you can stream it.
You now know more about HTTP than many senior web developers. I wish this were not true.
Fundamentally, the structure of our solution - the HTTP library - should mirror the structure of the problem If the solution is significantly larger than the problem, one or more of the following is true:
The go stdlib’s net/http covers all of HTTP in 35 files of pure go and 25,597 lines of code, including the server, client, TLS, proxies, etc.
Gin and it’s dependency chain covers only server-side handling and requires 2,148 files and 1,032,635 lines of code, including 80084 lines of platform-specific GNU style assembly.
This is nuts. You can crack an egg with a 155mm artillery shell. This does not make it a good idea, even if you add rocket boosters and laser guidance.
Some people would argue that the code weight doesn’t matter - what we should be worried about is the API: i.e, the interface we have to keep in our heads. They’d probably sneak in a quote about premature optimization or something. No problem.
The following diagrams illustrate the ‘minimal’ APIs to understand net/http and gin.
net/http servergin sever and kafkaesque nightmare:As hard as it is to believe, this graph omits a ton of details.
If you’re reading this, you’re probably a progammer. Take a moment to think about how the dependencies were chosen for your current project(s). Ask yourself - or better yet, a team-mate - the following questions re: your major dependencies:
For the vast majority of projects, there is no answer to these questions, because no one ever thought about it. They went into google search or chatgpt, typed “best golang web framework reddit” and called it a day. I know this because I have seen it happen at least twenty times at half a dozen software houses. While understandable - software is a busy, stressful, job - this is not acceptable. This is the kind of reasoning you apply to choosing lunch, not critical software dependencies for million or billion-dollar projects.
In anything at all, perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away. ~Antoine De Saint Expry.
Gin is too big. Gin is enormously, staggeringly big. It’s dependency tree is over 55MiB. If we just taking the lines of code in Gin and it’s dependencies - ignoring comments and documentation - we have 877615 lines. This is huge, enormous, elephantine cost must be paid by every single project on every single git clone or go build, and some of that cost leaks into the compiled binary too.
Gin contains, I kid you not, four five at least six different JSON libraries, not counting the one built in to the standard library. (more about this later.)
These include
goccy/go-json (1204K)bytedance/sonic (13 MiB!!!!)quic-go/quic-go/qlogwriter/jsontext (12 KiB - you pass)ugorji/go/codec(3MiB!!!,)./github.com/quic-go/quic-go/qlogwriter/jsontextgabriel-vasile/mimetype/internal/jsonjson-iterator/go (348K)I thought there were only four, but I kept finding more.
The following table compares the code bloat of Gin to other popular and/or historically important programs or written material.
| Program or Library | Description | Files | Code Lines | %target | Size | %size |
|---|---|---|---|---|---|---|
| github.com/gin-gonic/gin | A popular go web framework and OSHA violation | 2189 | 877615 | 100.000% | 55.461 MiB | 100.00% |
| lua | General-purpose scripting langauge a-la Python or Javascript | 105 | 36685 | 4.180% | 14.926 MiB | 26.91% |
| chi | Minimalistic go HTTP framework | 85 | 7781 | 0.887% | 4.746 MiB | 8.56% |
| Command and Conquer: Red Alert | A best-selling real-time strategy game (1996) with it’s own GUI, networking code, custom game engine, etc etc etc. | 1893 | 368288 | 41.965% | 39.957 MiB | 72.05% |
| DOOM | ID software’s revolutionary first-person shooter, including networked play | 152 | 39250 | 4.472% | 2.375 MiB | 4.28% |
| gorilla/mux | Popular go HTTP router. | 19 | 6214 | 0.708% | 1.059 MiB | 1.91% |
| labstack/echo | Popular go web framework | 600 | 326000 | 37.146% | 23.855 MiB | 43.01% |
| golang/go/src | The go programming language, it’s runtime, tooling, and compiler | 9591 | 2244096 | 255.704% | 143.129 MiB | 258.07% |
| MechCommander2-Source/ | 2001 real-time strategy game | 1875 | 858811 | 97.857% | 1.771 GiB | 3269.17% |
| MS-DOS/v1.25/ | Complete operating system, predecessor of microsoft windows | 20 | 12001 | 1.367% | 504.000 KiB | 0.89% |
| MS-DOS/v2.0/ | “ | 116 | 41417 | 4.719% | 2.527 MiB | 4.56% |
| MS-DOS/v4.0 | Final release of MS-DOS with true multitasking support | 1065 | 332117 | 37.843% | 23.203 MiB | 41.84% |
| original-bsd/ | The original berkley systems distribution operating system and hundreds of programs, libraries, and games | 9562 | 1526953 | 173.989% | 185.387 MiB | 334.27% |
| Quake | ID software’s third-person shooter, including 3d graphical engine, GUI, networking code, etc etc | 516 | 170211 | 19.395% | 15.266 MiB | 27.53% |
| Research-Unix-v10/v10 | Original ‘research’ unix before split into BSD and other distributions, including networking, productivity software, and games | 8755 | 1671269 | 190.433% | 137.430 MiB | 247.80% |
| zig/src/ | Systems programming language and tooling, including an entire C compiler for dozens of targets | 175 | 473612 | 53.966% | 24.094 MiB | 43.44% |
| musl | implementation of core C library used by Linux and other operating systems | 1922 | 64837 | 7.388% | 9.199 MiB | 0.16586 |
| Bible (King James Version) | Popular Translation of the Jewish & Christian Core Religious Text | 31104 | — | — | 4.436 Mib | — |
| War and Peace | Tolstoy’s extremely long novel about the Napoleonic wars | 23637 | — | — | 3.212 MiB — |
This is, to be blunt, completely unacceptable. If you picked a sane framework like chi (you don’t need a framework, but for the sake of argument), you could bundle in DOOM, a C compiler to build it with (let’s pick Zig), and an operating system to run it on like MS-DOS 4.0, and throw in War and Peace and the entire Kings James Bible for good measure and you’d still have less bloat than Gin and it’s source tree.
This bloat carries over to the compiled binary, too.
While Go’s compiler is pretty good about eliminating unused code, Gin does it’s best to touch as many different libraries as it can at import time so the compiler can’t do that.
To demonstrate, let’s strip down our examples even further and build the simplest possible Gin programs and an equivalent HTTP servers and see how big the resulting binaries are.
1// simplegin.go
2func main() {
3 e := gin.Default()
4 e.ANY("/", func(c *gin.Context) {
5 c.Writer.WriteHeader(200)
6 })
7 e.Run()
8}
1// simplehttp/main.go
2func main() {
3 http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4 w.WriteHeader(200)
5 }))
6}
Let’s examine the compiled output:
1#!/usr/bin/env bash
2du -H simplehttp simplegin
3
419640K simplegin
57864K simplehttp
Maybe it’s just debug symbols? Let’s strip the binaries and try again:
1#!/usr/bin/env bash
2strip simplehttp
3strip simplegin
4du -H simplehttp simplegin
513572K simplegin
65444K simplehttp
Where’s all this bloat coming from? After all, we don’t use most of Gin’s features… Let’s use GODEBUG=inittrace=1 to see what packages are being initialized to see if we can figure out where all this bloat is coming from.
1GODEBUG=inittrace=1 ./simplegin
1init internal/bytealg @0.005 ms, 0 ms clock, 0 bytes, 0 allocs
2init runtime @0.078 ms, 0.10 ms clock, 0 bytes, 0 allocs
3init crypto/internal/fips140deps/cpu @0.63 ms, 0.003 ms clock, 0 bytes, 0 allocs
4init math @0.67 ms, 0 ms clock, 0 bytes, 0 allocs
5... many, many lines omitted
There’s a lot of noise here, so I’ll summarize a couple highlights:
You pay for toml, gob, yaml, protobuf, xml, and at least two JSON libraries, regardless of whether you use them:
you pay for http/3 (QUIC) even if you aren’t using it
This cost is a direct result of Gin’s horrific ‘everything and the kitchen sink’ API - more about that in a bit.
go build -tags nomsgpackAs it turns out, the
ginteam has been trying to deal with this enormous binary bloat.
You can eliminate the dependency on msgpack by adding the built tagnomsgpack, which shaves ten megabytes off the binary. This should be the default, but still, good job.
Niklaus Wirth, inventor of PASCAL
A quick note on UNIX before we dive into Gin’s API.
UNIX is one of the oldest traditions in software still standing. In this tradition, good APIs have a small surface that exposes deep functionality. The classic example is UNIX’s filesystem API, which made it a long way with only six verbs: OPEN, CLOSE, READ, WRITE, SEEK, and FCTNL - this is enough to handle disk drives, shared network filesystems, terminals, printers, etc.
There’s a good argument to be made that this is not the correct filesystem API anymore - FCTNL is clearly cheating, and it doesn’t handle nonblocking or concurrent IO that well. See the excellent talk What Unix Cost Us by Benno Rice for a discussion of this topic.
For more on UNIX programming, see my Starting Systems Progamming series of article.
Go is firmly part of this tradition, and as such, it’s standard library tries to minimize API surface where possible. The vast, vast majority of interfaces in Go’s standard library have three or fewer methods, usually just one. Even the largest interface in Go, net.Conn tops out at 8 methods. Gin… does not do this.
reflect.Typedoesn’t count: it’s never meant to be implemented by external libraries: all it’s implementors are internal codegen, and reflection is always kind of an exception to every rule. Please don’t @ me.
Let’s take a look at how net/http is designed to see this philosphy in action.
net/http is a beautiful APIServer-side HTTP in go can be summarized in four types and one sentence: The http.Server parses packets into http.Request structs, hands them to a http.Handler, which writes a response via http.ResponseWriter.
Usually, that handler is some kind of router like
http.ServeMuxthat dispatches to different sub-handlers - but it doesn’t have to be.
To give a quick example, here’s a minimal HTTP server using only the Go standard library that responds to POST /greet.
While we use a number of types here, there’s only a handful of interfaces we need to understand this code - http.Handler, http.ResponseWriter, and the omnipresent io.Reader and io.Writer interfaces used by the JSON encoder and decoder.
1// 43 words of interface surface area, not counting comments
2type Handler interface {
3 ServeHTTP(w ResponseWriter, r *Request)
4}
5type ResponseWriter interface {
6 WriteHeader(statusCode int)
7 Header() Header
8 Write([]byte) (int, error)
9}
10type Reader interface {
11 Read(p []byte) (n int, err error)
12}
13type Writer interface {
14 Write(p []byte) (n int, err error)
15}
To summarize Gin’s API in a similar way, the gin.Engine gets http requests, routes them using it’s embedded gin.RouterGroup, and turns them into a *gin.Context, which contains a *http.Request and a gin.ResponseWriter, and hands them to one or more gin.HandlerFuncs, which modify the *gin.Context.
This doesn’t sound too bad - in fact, it sounds almost the same. Let’s take a look at the method summaries of these types to see what we’re dealing with here, starting with gin.Engine
*gin.Engine 1
2 func (engine *Engine) Delims(left, right string) *Engine
3 func (engine *Engine) HandleContext(c *Context)
4 func (engine *Engine) Handler() http.Handler
5 func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string)
6 func (engine *Engine) LoadHTMLFiles(files ...string)
7 func (engine *Engine) LoadHTMLGlob(pattern string)
8 func (engine *Engine) NoMethod(handlers ...HandlerFunc)
9 func (engine *Engine) NoRoute(handlers ...HandlerFunc)
10 func (engine *Engine) Routes() (routes RoutesInfo)
11 func (engine *Engine) Run(addr ...string) (err error)
12 func (engine *Engine) RunFd(fd int) (err error)
13 func (engine *Engine) RunListener(listener net.Listener) (err error)
14 func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error)
15 func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error)
16 func (engine *Engine) RunUnix(file string) (err error)
17 func (engine *Engine) SecureJsonPrefix(prefix string) *Engine
18 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request)
19 func (engine *Engine) SetFuncMap(funcMap template.FuncMap)
20 func (engine *Engine) SetHTMLTemplate(templ *template.Template)
21 func (engine *Engine) SetTrustedProxies(trustedProxies []string) error
22 func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes
23 func (engine *Engine) With(opts ...OptionFunc) *Engine
This is a mess. This seems to cover
delims, NoMethod, NoRoute, Use, Routes())RunTLS, ‘RunQUIC’, With)LoadHTMLGlob, LoadHTMLFS, LoadHTMLFiles), and HTML templating. That is, it combines the concerns of http.Server, http.ServeMux, template/html(https://pkg.go.dev/html/template), not to mention entirely separate HTTP protocols like QUIC.BTW, for the ten thousand configuration options here, **none of them let you select the
http.Serverto use, so good luck if you want to do things like set timeouts or do connection or packet-level configuration. Gin is hardcoded to use the default HTTP server. I think you can do that by calling .Handler() and passing that to a*http.Server, but I’m not sure and it’s not covered by the documentation. Maybe it’s inWith?
But that’s not all - like I mentioned earlier, the gin.Engine embeds a RouterGroup. That means in addition to the previous, it also exposes the following methods:
*gin.RouterGroup 1 func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes
2 func (group *RouterGroup) BasePath() string
3 func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
4 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
5 func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
6 func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes
7 func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
8 func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes
9 func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes
10 func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes
11 func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
12 func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes
13 func (group *RouterGroup) Static(relativePath, root string) IRoutes
14 func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
15 func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes
16 func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes
17 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes
These methods cover routing - for some reason they present every HTTP verb as a separate method - and static file serving in in four different ways. All of these - except Group - return an IRoutes interface, and nearly all take a HandlerFunc.
gin.HandlerFunc and gin.ContextOk, what’s a HandlerFunc?
1type HandlerFunc func(c *gin.Context)
Finally, a small interface. Maybe this is the equivalent of a http.ResponseWriter? Let’s look at the exported interface (fields and methods) of *gin.Context.
1type Context struct {
2 Request *http.Request
3 Writer ResponseWriter // a gin.ResponseWriter, not a http.ResponseWRiter
4 Params Params
5 Keys map[any]any
6 Errors errorMsgs
7 Accepted []string
8 // contains filtered or unexported fields
9}
So it contains the http.Request and a gin.ResponseWriter. What’s a gin.ResponseWriter?
1type ResponseWriter interface {
2 http.ResponseWriter
3 http.Hijacker
4 http.Flusher
5 http.CloseNotifier
6 Status() int
7 Size() int
8 WriteString(string) (int, error)
9 Written() bool
10 WriteHeaderNow()
11 Pusher() http.Pusher
12}
*gin.Context has more methods than Jerry Seinfeld has carsThe bigger the interface, the weaker the abstraction.
Rob Pike, “Go Proverbs”.
Ok, so stopping here, this already contains the entire API surface area of the net/HTTP interface - gin only adds complexity - as well as an extra five public fields and ten methods ON those fields.
That’s not good, but the real horrors are yet to come:gin.Context’s list of methods.
You might want to take a deep breath. You’re not ready for this.
1// 133 functions. Do you have them all memorized? I sure hope so. https://pkg.go.dev/github.com/gin-gonic/gin#Context
2 func (c *Context) Abort()
3 func (c *Context) AbortWithError(code int, err error) *Error
4 func (c *Context) AbortWithStatus(code int)
5 func (c *Context) AbortWithStatusJSON(code int, jsonObj any)
6 func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any)
7 func (c *Context) AddParam(key, value string)
8 func (c *Context) AsciiJSON(code int, obj any)
9 func (c *Context) Bind(obj any) error
10 func (c *Context) BindHeader(obj any) error
11 func (c *Context) BindJSON(obj any) error
12 func (c *Context) BindPlain(obj any) error
13 func (c *Context) BindQuery(obj any) error
14 func (c *Context) BindTOML(obj any) error
15 func (c *Context) BindUri(obj any) error
16 func (c *Context) BindWith(obj any, b binding.Binding) errordeprecated
17 func (c *Context) BindXML(obj any) error
18 func (c *Context) BindYAML(obj any) error
19 func (c *Context) ClientIP() string
20 func (c *Context) ContentType() string
21 func (c *Context) Cookie(name string) (string, error)
22 func (c *Context) Copy() *Context
23 func (c *Context) Data(code int, contentType string, data []byte)
24 func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, ...)
25 func (c *Context) Deadline() (deadline time.Time, ok bool)
26 func (c *Context) DefaultPostForm(key, defaultValue string) string
27 func (c *Context) DefaultQuery(key, defaultValue string) string
28 func (c *Context) Done() <-chan struct{}
29 func (c *Context) Err() error
30 func (c *Context) Error(err error) *Error
31 func (c *Context) File(filepath string)
32 func (c *Context) FileAttachment(filepath, filename string)
33 func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
34 func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
35 func (c *Context) FullPath() string
36 func (c *Context) Get(key any) (value any, exists bool)
37 func (c *Context) GetBool(key any) (b bool)
38 func (c *Context) GetDuration(key any) (d time.Duration)
39 func (c *Context) GetFloat32(key any) (f32 float32)
40 func (c *Context) GetFloat32Slice(key any) (f32s []float32)
41 func (c *Context) GetFloat64(key any) (f64 float64)
42 func (c *Context) GetFloat64Slice(key any) (f64s []float64)
43 func (c *Context) GetHeader(key string) string
44 func (c *Context) GetInt(key any) (i int)
45 func (c *Context) GetInt16(key any) (i16 int16)
46 func (c *Context) GetInt16Slice(key any) (i16s []int16)
47 func (c *Context) GetInt32(key any) (i32 int32)
48 func (c *Context) GetInt32Slice(key any) (i32s []int32)
49 func (c *Context) GetInt64(key any) (i64 int64)
50 func (c *Context) GetInt64Slice(key any) (i64s []int64)
51 func (c *Context) GetInt8(key any) (i8 int8)
52 func (c *Context) GetInt8Slice(key any) (i8s []int8)
53 func (c *Context) GetIntSlice(key any) (is []int)
54 func (c *Context) GetPostForm(key string) (string, bool)
55 func (c *Context) GetPostFormArray(key string) (values []string, ok bool)
56 func (c *Context) GetPostFormMap(key string) (map[string]string, bool)
57 func (c *Context) GetQuery(key string) (string, bool)
58 func (c *Context) GetQueryArray(key string) (values []string, ok bool)
59 func (c *Context) GetQueryMap(key string) (map[string]string, bool)
60 func (c *Context) GetRawData() ([]byte, error)
61 func (c *Context) GetString(key any) (s string)
62 func (c *Context) GetStringMap(key any) (sm map[string]any)
63 func (c *Context) GetStringMapString(key any) (sms map[string]string)
64 func (c *Context) GetStringMapStringSlice(key any) (smss map[string][]string)
65 func (c *Context) GetStringSlice(key any) (ss []string)
66 func (c *Context) GetTime(key any) (t time.Time)
67 func (c *Context) GetUint(key any) (ui uint)
68 func (c *Context) GetUint16(key any) (ui16 uint16)
69 func (c *Context) GetUint16Slice(key any) (ui16s []uint16)
70 func (c *Context) GetUint32(key any) (ui32 uint32)
71 func (c *Context) GetUint32Slice(key any) (ui32s []uint32)
72 func (c *Context) GetUint64(key any) (ui64 uint64)
73 func (c *Context) GetUint64Slice(key any) (ui64s []uint64)
74 func (c *Context) GetUint8(key any) (ui8 uint8)
75 func (c *Context) GetUint8Slice(key any) (ui8s []uint8)
76 func (c *Context) GetUintSlice(key any) (uis []uint)
77 func (c *Context) HTML(code int, name string, obj any)
78 func (c *Context) Handler() HandlerFunc
79 func (c *Context) HandlerName() string
80 func (c *Context) HandlerNames() []string
81 func (c *Context) Header(key, value string)
82 func (c *Context) IndentedJSON(code int, obj any)
83 func (c *Context) IsAborted() bool
84 func (c *Context) IsWebsocket() bool
85 func (c *Context) JSON(code int, obj any)
86 func (c *Context) JSONP(code int, obj any)
87 func (c *Context) MultipartForm() (*multipart.Form, error)
88 func (c *Context) MustBindWith(obj any, b binding.Binding) error
89 func (c *Context) MustGet(key any) any
90 func (c *Context) Negotiate(code int, config Negotiate)
91 func (c *Context) NegotiateFormat(offered ...string) string
92 func (c *Context) Next()
93 func (c *Context) Param(key string) string
94 func (c *Context) PostForm(key string) (value string)
95 func (c *Context) PostFormArray(key string) (values []string)
96 func (c *Context) PostFormMap(key string) (dicts map[string]string)
97 func (c *Context) ProtoBuf(code int, obj any)
98 func (c *Context) PureJSON(code int, obj any)
99 func (c *Context) Query(key string) (value string)
100 func (c *Context) QueryArray(key string) (values []string)
101 func (c *Context) QueryMap(key string) (dicts map[string]string)
102 func (c *Context) Redirect(code int, location string)
103 func (c *Context) RemoteIP() string
104 func (c *Context) Render(code int, r render.Render)
105 func (c *Context) SSEvent(name string, message any)
106 func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error
107 func (c *Context) SecureJSON(code int, obj any)
108 func (c *Context) Set(key any, value any)
109 func (c *Context) SetAccepted(formats ...string)
110 func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
111 func (c *Context) SetCookieData(cookie *http.Cookie)
112 func (c *Context) SetSameSite(samesite http.SameSite)
113 func (c *Context) ShouldBind(obj any) error
114 func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error)
115 func (c *Context) ShouldBindBodyWithJSON(obj any) error
116 func (c *Context) ShouldBindBodyWithPlain(obj any) error
117 func (c *Context) ShouldBindBodyWithTOML(obj any) error
118 func (c *Context) ShouldBindBodyWithXML(obj any) error
119 func (c *Context) ShouldBindBodyWithYAML(obj any) error
120 func (c *Context) ShouldBindHeader(obj any) error
121 func (c *Context) ShouldBindJSON(obj any) error
122 func (c *Context) ShouldBindPlain(obj any) error
123 func (c *Context) ShouldBindQuery(obj any) error
124 func (c *Context) ShouldBindTOML(obj any) error
125 func (c *Context) ShouldBindUri(obj any) error
126 func (c *Context) ShouldBindWith(obj any, b binding.Binding) error
127 func (c *Context) ShouldBindXML(obj any) error
128 func (c *Context) ShouldBindYAML(obj any) error
129 func (c *Context) Status(code int)
130 func (c *Context) Stream(step func(w io.Writer) bool) bool
131 func (c *Context) String(code int, format string, values ...any)
132 func (c *Context) TOML(code int, obj any)
133 func (c *Context) Value(key any) any
134 func (c *Context) XML(code int, obj any)
135 func (c *Context) YAML(code int, obj any)
This is a nightmare. Even a ‘simple’ Gin server that only receives and sends HTTP with JSON bodies over net/HTTP is unavoidably linked to this enormous complexity.
Even if you “just” want to send and receive JSON, there are eleven different ways to do this as methods on gin.Context all of which behave differently depending on build tags and magically invoke multiple layers of struct validation, and some of which depend on the configuration of your gin.Engine, not to mention .Writer.WriteString() and .Writer.Write().
1 func (c *Context) AbortWithStatusJSON(code int, jsonObj any)
2 func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any)
3 func (c *Context) AsciiJSON(code int, obj any)
4 func (c *Context) BindJSON(obj any) error
5 func (c *Context) IndentedJSON(code int, obj any)
6 func (c *Context) JSON(code int, obj any)
7 func (c *Context) JSONP(code int, obj any)
8 func (c *Context) PureJSON(code int, obj any)
9 func (c *Context) SecureJSON(code int, obj any)
10 func (c *Context) ShouldBindBodyWithJSON(obj any) error
11 func (c *Context) ShouldBindJSON(obj any) error
To pick a single example, to know the behavior of SecureJSON at runtime, I need to know, among other things
gin.Engine that’s running this function - one that is not visible in the function signature of a HandlerFunc - set a gin.SecureJSONPrefix?Status Headers are even more complex: there are 24 different ways to set a response header via methods of .Context or it’s fields, including:
Context.Status() (writes a status header)Context.Writer.Status() (READS a previously written status header - sometimes)Context.Writer.WriteHeader() (WRITES a status header, but not in a way where you can always retreive the status header with .Writer.Status(), yes I have run into this and I am salty)As intimidating as these giant lists of methods are, it turns out, the vast majority of these methods are wrappers around the same core functionality. In fact, they’re wrappers around the exact same functionality as net/http.ResponseWriter. Let’s follow the ordinary JSON down the chain and figure out what’s happening.
The .JSON() method calls the exported function WriteJSON, which calls c.Render(). This writes the status - by calling .Status() - which just wraps http.ResponseWriter.WriteHeader,
takes the interface render.Render, which calls the magic method WriteContentType,render.Render() on the magic exported global variable codec/json.API of type json.Core, which happens to be the conditionally-compiled empty struct codec/json.jsonapi then writes the marshaled bytes to the http.ResponseWriter.
The magic exported global variable depends on your build tags. Usually, this is the stdlib’s encoding/JSON.
That is, it’s
1b, _ := json.Marshal(obj)
2w.Write(b)
With a lot of extra steps in between.
Writing the content type header is similarly convoluted.
JSON() calls render.Render.WriteContentType(), which does a vtable lookup to find render.JSON.WriteContentType(), which calls the ordinary function writeContentType(), which does a vtable lookup to find .Header() on the response writer, then sets the header in an ordinary way.
In case that all sounds a bit abstract - and it is - I’ve provided a handy chart for you.
Nothing inside the box labeled ‘gin’ does anything at all useful.
And again, this is just ONE of the ELEVEN different ways to send JSON responses in Gin. Most of them go through similar contortions. All of them have their own structs for some ungodly reason. We haven’t even covered requests! (I meant to, but this article has taken me multiple full workdays already).
This approach is godawful, somehow combining the worst of both runtime lookup (extra indirection and function calls) and conditional compilation. Both you and the compiler have to jump through multiple layers of indirection to figure out what is actually happening at runtime, for no benefit whatsoever. These extra layers serve merely to bloat the binary and confuse the programmer.
In the default case - the case for 99.5% of Gin’s consumers, _you are doing the exact same thing as the standard library, but splitting the responsibility over a half dozen extra interfaces and types and hundreds of lines of code!
If you wanted to use a different JSON library, you could just… use that library!
All gin does is obscure the control flow, inculcating a sense of helplessness in the programmer and causing cache misses at runtime for no benefit whatsoever.
renderrender take a http.ResponseWriter and not just an io.Writer? Is it supposed to do something different from writing the body (e.g, modifying the headers?)WriteContentType take a whole http.ResponseWriter? Is it supposed to modify the body? It should take a *http.Header! Or maybe be the slightly more sane interface { ContentType() string } - or better yet, not exist at all!Let’s keep this section short. Gin’s documentation is sparse at best. An illustrative example is gin.RouterGroup: despite it’s enormous API, it’s documentation is limited to a handful of sentences split between gin.RouterGroup itself and gin.RouterGroup.Handle.
RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix and an array of handlers (middleware).
…
Handle registers a new request handle and middleware with the given path and method. The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. See the example code in GitHub. (Note: no link is provided!)For GET, POST, PUT, PATCH and DELETE requests the respective shortcut functions can be used.
This function is intended for bulk loading and to allow the usage of less frequently used, non-standardized or custom methods (e.g. for internal communication with a proxy).
net/http.ServeMux: example (good) documentationOn the other hand, http.ServeMux’s documentation is nearly a thousand words, not counting in-documentation examples, split into five sections: Patterns, Precedence, Trailing-slash redirection, Request sanitizing, and Compatibility. I encourage you to click on the two above links and take a look for yourself.
None of this is the worst part of Gin. The worst part is this: going from a http.Handler to a gin handler is trivial. You can write an adapter to go FROM the standard library TO gin in a single line.
1func adaptHandler(h http.Handler) func(c *gin.Context) { return func(c *gin.Context) {return h.ServeHTTP(c.ResponseWriter, c.Request)}}
Going from a Gin handler to an ordinary http.Handler is functionally impossible - the only practical to do it is to dig into the code, figure out what it’s actually trying to do, and rip out all of the indirection.
If you’re still early enough in your software project, this is practical - if you’re months or years deep into a legacy codebase, you don’t have a chance in hell.
If a single person on your team gets the bright idea to use Gin, you’re more or less stuck. You can work _around _ it, but it will be lurking at the bottom of your server, a giant chain of dependencies that you can never really get rid of.
This, I think, is the secret to Gin’s success. It’s attractive enough and popular enough to attract the trendhoppers and the naive, and tolerable enough for them to stick with it long enough to get stuck, and, like restaraunts, most people use software because other people are already using it. Worse yet, because it’s so difficult and painful to move away, users of Gin make the wrong conclusion that this is because other libraries are hard, and they sing the praises of their jailers. Maybe flies on the web do the same thing.
Gin is a bad software library and we as developers should stop using things like it. The purpose of this essay is not really to talk about Gin - it’s to use it as an illustrated example of what is bad in software libraries rather than good.
The choice of what library, if any, to use is an engineering decision, not just a matter of opinion. It has concrete effects on the process of writing code and the resulting programs. While taste is part of the decision, it should not be the primary or only one. Gin and libraries like it will make your software worse. Stop using them.
I’ll finish off with some advice on picking dependencies
If you’re not in deep, try and rip it out. If it’s already spread deep into your codebase, the best you can do is probably containment.
net/http handlers instead where possible for any future work, even if there’s still Gin there.<