Go's standard library has always been top-notch and a great base to build whatever userland library or app you want. Tons of batteries included, feels like a micro framework out of the box. You need Oauth2 with any service? Cool, just use https://pkg.go.dev/golang.org/x/oauth2 and you get support for any server that follows the Oauth2 specs. You need to work with TCP/UDP, it is there by default. You need production grade SMTP server? crypto ? a testing framework? All there by default.

But over the last few releases, the Go team has started getting into the userland space releasing libraries that directly compete and exists to replace community maintained software. While this can be considered a great thing, the official versions from the Go team have not been the greatest thing to use. Here are a few examples:

HTTP routing

One of the things that drew me to Go in 2017 was the simplicity around HTTP in the core library ( here , here and here). One thing that the standard router has always lacked was a good router. The STD router was only good when either of the following is true:

  • No dynamic route(s). All static routes.
  • No shared group of middlewares across multiple routes

Both items above are pretty much standard in any non-trivial application - even a beginners' todo app. And as such this is a problem that hasn't plagued the community as there have been multiple and great alternatives to the standard router like chi ( my favorite ), gorilla/mux e.t.c but in 1.22, Go is shipping with a better and improved router. But is it actually any better?

  mux := http.NewServeMux()
  // pretty normal route
  mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "This is the path response\n")

  // Now we have dynamic routes
  mux.HandleFunc("GET /path/{uuid}/", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("uuid")
    fmt.Fprintf(w, "Checking out the path with uuid=%v\n", id)

Looks great but you will need a bunch of string magic, concatenation going on in the path declaration. Given the chance at making a second design or pass at a standard HTTP router, I find this very weird. It looks like learning some form of magical DSL. I just want mux.Get("/path"). It is just simply unergonomic and a poor API. There are concerns about breaking backwards compatibility but it is not uncommon for the go team to release experimental packages in the golang.org/x/* repo where a fresh start can be made. Or provide some form of new public facade method/API that wraps this string magic.

HandleFunc by default allows any HTTP verbs on a given route, how soon before people ask to support specific HTTP verbs for one route and we have to do this:

  // Support only GET/PUT/DELETE on this dynamic route
  mux.HandleFunc("GET PUT DELETE /path/{uuid}/", func(w http.ResponseWriter, r *http.Request) {

There is also no simple way to chain middlewares and share them across a group of HTTP routes/handlers without resorting to some form of voodoo.

Or better yet, why are we actively redesigning the HTTP router when this is a solved problem in userland. No one is out there complaining about HTTP routing or performance

Yes, I know it is easy to write some form of wrapper around this. I prefer type safety, I absolutely also don't want to copy/paste the same block of code into every project. Neither do i want the Go ecosystem to turn to NPM land where packages and wrappers that ideally shouldn't exist are just a neccesity

And oh if you define /path/{uuid}, an external http call to /path/12345/not/supposed/to/be/part/of/route would/should work. A new $ ( yet another magic string ) now exists that you have to add to your path definition to make sure /path/12345 is the only thing regarded as valid


Logging is a critical part of any standard library. Putting content to stdout/stderr is a pretty standard way of debugging software - even though it is considered a lame's man version. And as expected, the Go std library comes default with a log package that allows you write to any place essentially as long as it implements the io.Writer interface.

In reality, no one uses the aforementioned package in production. Structured logging is the way to go and this is mostly because:

  • they are easily human and machine readable + parseable.
  • High cardinality
  • lots of toolings exists to read/search structured logs.

As this is an extremely important part of any production system, logging has been longed solved. Logrus is a brillant library. The only issues I know of are performance/allocations related depending on what you are building. But there are other options like zap ( from Uber ) and Zerolog if reducing allocations is extremely important to your software. Just like vgo, I woke up to a very detailed proposal on adding a structured logging library to the Go's standard library. There isn't a shortage of production proof logging libraries.

Slog for me has the worst API compared to zerolog or logrus. I will share an example below:

package main

import (

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	logger.Info("an error occurred",
		"err", sql.ErrNoRows,
		"request_id", "12345-abc")

You get this:

  "time": "2009-11-10T23:00:00Z",
  "level": "INFO",
  "msg": "an error occurred",
  "err": "sql: no rows in result set",
  "request_id": "12345-abc"

Compare the code above to the equivalent in Logrus which produces the same result:

package main

import (

	log "github.com/sirupsen/logrus"

func main() {

	log.WithField("request_id", "12345-abc").
		WithField("err", sql.ErrNoRows).
		Info("an error occurred")

A few things are quite visible here. It is hard to parse what slog is doing without at first. Which is the msg bit? Which is the structured log bit? And why do i have to have an API like that? WithField is simple and removes any potential for errors.

Asides the APIs, I strongly believe it doesn't solve any ( existing ) pain points around structured logging, metadatas and contexts. Maybe it is a great to have in the std. Maybe not.


I started writing Go in 2017. I was writing PHP and a bit of Ruby prior to Go and one thing that was an absolute joy to use in PHP ( hate to admit this ) was composer and Semver. Bundle in Ruby had this too. Go was really shocking, you just go get a repo and you get the latest commit. If a breaking changes was introduced in the library's head, good luck deploying to prod that way and spending time to resolve/fix the changes.

The solution by the Go team to this was to vendor your dependencies and check it into your repo 🫨🫨🫨🫨. Even JS and PHP for all the bad blood they get were not doing this in 2017.

Enter dep

Dep was a community maintained semver implementation of dependency management for Go. It was an absolute delight to use, you could pin versions of your libraries and sleep well at night.

I might be wrong, but dep was the most popular dependency management tooling for Go at some point. Until one day I saw somewhere the main Dep maintainer was working directly with the Go team to release an official package manager. In fact, dep was migrated and lived under the golang organisation for a period of time so it really felt like an awesome piece of software would be added to the toolchain.

Then a few weeks later, the Go team releases an experimental tool called Vgo - that allowed you import multiple versions of the same library. The author of Dep had a bit to say. In general, I think most people didn't like the design behind Vgo and stuck with dep and vendoring.

Vgo was deprecated shortly after and go mod was introduced. I personally have reservations about go mod but it works but the very long journey to go mod would have been relatively shorter if the Go team built on top of dep. We could have also avoided the many many pitfalls that came with the first few iterations of modules.

Final thoughts

Just give us enums, kill goto, give us pattern matching and a few other things :) And a big thank you for fixing for loop to have per iteration scope!