As usual, when a new blog post comes out on blog.golang.org, I’m all eager to read it as soon as possible. The most recent one, Contributors Summit, is a nice write-up on the issues that the Go contributors have been talking about. While reading it, I stumbled upon a sentence that made me write this post. Here is is:
For instance, it would be nice if io.Reader accepted a context so that blocking read operations could be canceled.
This gave me chills. This is what io.Reader
would look like with a context.
type Reader interface {
Read(ctx context.Context, p []byte) (n int, err error)
}
I did some research and found that some people already proposed this change for Go 2. Thankfully it received a decent amount of thumbs down, so it’s likely not making it.
This post is about all of the things that are wrong with the "context"
package, why it is useful
despite that, and that Go 2 should do something about it. So, grab some popcorn and let’s get
started!
Go is a general purpose language
First things first, let’s establish some ground. Go is a good language for writing servers, but Go is not a language for writing servers. Go is a general purpose programming language, just like C, C++, Java or Python. For example, I’ve been using Go for about 2 years and I’ve never written a single server in it.
For this reason, when designing the Go language and it’s standard library, we need to approach it from a general purpose language perspective. Now, I’m not trying to say that context is only useful for server people. But mostly, it is.
Context is like a virus
This is the first and most important problem with context: it spreads! As mentioned in this blog post about the context package:
At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests.
Every such function also needs to propagate the context down it’s call path, or else it wouldn’t be fully cancelable. This means that all the potentially slow functions from other libraries that are being called from a function accepting a context should also accept a context.
In short, if you’re writing a library that has function which can take some significant amount of time and your library is potentially going to be used by a server application, you have to accept a context in those functions.
That’s how context spreads like a virus. What’s bad about that? Let’s recap:
- Go is a general purpose language.
- If a library is potentially going to be used by a server, it should accept a context.
- Now, everyone has to deal with the context, even the ones who don’t need it.
Of course, I can just pass context.TODO()
everywhere, but that’s just gross, it hurts readability,
makes my code look ugly and simply removes a part of fun I have with Go.
If the Go language ever comes to the point where I’d have to write this
n, err := r.Read(context.TODO(), p)
put a bullet in my head, please.
You might argue: A library can provide two version of each function, one with a context and one
without a context. Sure, just take a look at the
"database/sql"
package. Although it does solve the problem
partially, it smells quite bad.
Also, imagine teaching Go to a student. You start explaining the context-equipped io.Reader
interface (or anything else which occasionally requires a context) to them and they ask: What is
that ctx context.Context
thingy there? And the answer would probably just be: Don’t worry about
that, just pass context.TODO()
there for now. Sounds a lot like public static void
to me.
The message is: Context spreads like a virus and I (alongside almost everyone who doesn’t write servers in Go) don’t want to deal with it when I don’t have to.
The "context"
package itself is not that good
The first thing is a personal opinion, but for me, the context.Context
interface has too many
methods. Now, the more serious problems.
If you use ctx.Value
in my (non-existent) company, you’re fired
I’m not sure who came up with this idea that context should carry a map of meaningless objects to meaningless objects. There are just so many things that are wrong with it. Let’s list a few:
- An obvious one, it’s not statically typed at all.
- It requires documenting which values (keys and their types) a certain function supports and uses. As we all know, documentation is mostly a code that never runs.
- It’s very similar to thread-local storage. We know how bad of an idea thread-local storage is. Non-flexible, complicates usage, composition, testing.
- This probably doesn’t happen often, but it’s prone to name collisions.
- It’s just magic. An error-prone magic.
I know that ctx.Value
makes some things easier. But, I believe that designing your APIs without
ctx.Value
in mind at all makes it always possible to come up with alternatives.
Context is mostly an inefficient linked list
The way WithCancel
, WithDeadline
, etc. constructors from the "context"
package work is they
create a linked list. Among other things, this sometimes requires creating a
goroutine for WithCancel
, which propagates
cancelation signals from the previous context to the new one. Of course, if the context is never
canceled, this goroutine is leaked.
The WithValue
constructor takes a context and returns a context which propagates the previous
context but also contains a value under the specified key. This is, obviously, achieved by creating
another node in the linked list, the purpose of which is to return the correct value for that key
and propagate the previous context otherwise. So, ctx.Value
is not only a map of meaningless
objects to meaningless objects, it’s also a terribly slow map of meaningless objects to meaningless
objects.
And lastly
ctx context.Context
is a lot like
Foo foo = new Foo();
One of the things Go was created to avoid.
What does the "context"
package actually solve?
Despite all of the bad things described above, the "context"
package is genuinely useful, because
it solves one thing that is kinda hard to do in Go: cancelation. That’s the only problem the
"context"
package really solves (or attempts to solve).
Let’s face it, cancelation in Go is hard. There is a whole talk called ‘Advanced Go Concurrency
Patterns’, which discusses this problem in depth. This
talk happened before the "context"
package was introduced into the Go standard library and thus it
discusses solutions to the cancelation problem which only involve simple channels.
There are number of reasons why solutions proposed in the talk don’t scale very well. Here are a few of them:
- The cancelation channels are usually not accepted by other libraries and functions and thus cancelation is only possible ‘in-between’ the slow operations.
- Considering a ‘tree of goroutines’ (where children goroutines are the ones spawned by the parent goroutines), it’s easy to cancel the whole tree (just close the cancelation channel), but it’s harder to cancel a sub-tree (you need to introduce another channel for that, or some other solution).
The "context"
package does solve these problems. Inefficiently and with numerous problems, but
solves them better than anything else out there. In Go, we need to be able to solve the
cancelation problem. Solving it is usually necessary anytime a decent usage of goroutines is
involved.
Go 2 should explicitly address the cancelation problem
I think it’s a weakness of the Go programming language that we needed to introduce a package like
"context"
. Go makes it very easy to create goroutines and communicate between them. However, the
"context"
package is a proof that Go makes it hard enough to arrange goroutines to finish. I
believe this problem should be solved directly in the language. The language should provide a
solution, which is:
- Simple and elegant.
- Optional, non-intrusive and non-infectious.
- Robust and efficient.
- Only solves the cancelation problem. Values can be omitted. Timeouts can also be implemented on top of a very simple cancelation.
You might argue: I like context, it’s an elegant solution to the problem without changing or complicating the language. I disagree. For all the reasons described above it’s not an elegant solution and although it’s not an integral part of the language, it is and is becoming more and more an integral part of the libraries. In the end, it makes the language harder to use.
I have a few solutions in mind, but I’ll leave them for another post or a proposal, or I’ll leave them for myself if someone comes up with a better solution. The purpose of this post is just to point out the problem.
Conclusion
This post was trying to point out a problem in the Go language. In short, cancelation is a problem
in Go and the "context"
package does not solve this problem very well. I can’t think of any other
solution that would solve this problem good except for a language change. That is up for Go 2.
Thanks for reading and I’m looking forward to your feedback and hate comments objections ;).
Michal Štrba
comments powered by Disqus