5 ms·
Go's context package is great, but also hard to reason about. As developers, we often times like to think that just piping a context through a function chain wi
by cfors 6y ago
Go's context package is great, but also hard to reason about. As developers, we often times like to think that just piping a context through a function chain will take care of all of your timeouts concerns for you.
Some fun context gotchas:
- Using `req, err := http.NewRequest(...)` to create an http request, and then calling `client.Do(req)` does not do anything with context, you need to use `http.NewRequestWithContext(...)`. [0]
- Creating a function that accepts a `ctx context.Context` and then making a blocking call that doesn't respect your context will still block for the entirety unless you instrument it yourself.
- If a parent context is canceled, all children (derived) contexts will also be canceled. However, only if they are explicitly checking `context.Done()`.
- And the worst of all is passing things that decide business logic in your context variables! First, you lose all benefits of go's static typing. Second, you will not realize all of the crazy places that a context scoped variable is coming from, have fun debugging that!
[0] https://golang.org/pkg/net/http/#NewRequest https://golang.org/pkg/net/http/#NewRequest
- bigdubs 6y agoThe issue here is contexts were introduced after the API for `http.NewRequest()` was guaranteed to be stable, so there are in effect (2) different versions of the api, one without contexts and one with. IMHO it's a rock and a hard place; either you break compatibility, or you have a slightly more confusing API.
- mook 6y agoAt least having a deprecation warning in the docs would be nice. Doing it compile time doesn't seem like the kind of thing golang would do; that seems like a thing external linters might have in that ecosystem (and likely already does).
- erik_seaberg 6y agoOr you add first-class context handling to the language, because this won't be the last time and continually adding more plumbing to every method signature is unsustainable.
- masklinn 6y agoWhat would "first-class context handling" be though, dynamic scoping?
- kevincox 6y agoProbably. I know dynamic scoping is considered dirty but I'm not convinced that it doesn't have its place. I think the problem in the past is that dynamic scoping often replaced syntactic scoping. But it can be a nice replacement for when you are deciding between a global or a context parameter to every function.
- erik_seaberg 6y agoI'm thinking something like panicking all the goroutines started in a context that's been cancelled, and something like defer to handle cleanup during cancellation. Some expression that can read and write a typed value for a key in the current context (not even the standard library can do this without generics). If contexts have to become part of the calling convention, that's fine, just don't add more noise to every function call in the source.
- earthboundkid 6y agoI think dynamic state makes more sense than package level state for Go. Anything package level need to be either read-only or mutex-locked for thread safety. Removing package level state and having per call dynamic state instead would be better.
- networkimprov 6y agoMost blocking syscalls in Go cannot be terminated by any means. [1] It seems unlikely that the stdlib os API will add dozens of variants taking context.Context. The new FS API proposal makes no mention of deadlines or contexts. [2] [1] https://github.com/golang/go/issues/41054 https://github.com/golang/go/issues/41054 [2] https://github.com/golang/go/issues/5636#issuecomment-661926962 https://github.com/golang/go/issues/5636#issuecomment-661926...
- ohnoesjmr 6y agoAtleast file io used to spawn a thread just for that, so cancellations and deadlines were still respected
- networkimprov 6y agoYou have to be able to direct a signal to the thread blocked on the syscall to be terminated. Go has no way to obtain the Id of that thread. That's what the issue I linked above covers.
- mleonhard 6y agoHere's some Golang code that does it: https://github.com/kawasin73/gointr https://github.com/kawasin73/gointr I think you could use that technique to make blocking syscalls that take a context.Context and return early if the context becomes done.
- morelisp 6y agoThe dual purposes of contexts (cancellation vs. value propagation) is one of the biggest warts in the go stdlib. It really feels like a right hand / left hand situation within Google. They are a pretty good tool for handling cancellation. They are abysmal at carrying values - no type safety, slow, and as the documentation notes require extreme care in key types. The type safety is probably foregone given the constraints of Go's type system, but the other two are direct consequences of marrying the values to the same interface requirements (additive only, mandatory ancestors) as cancellation, which are too weak for a good kv map. We simply don't use the value features unless we're using one of the HTTP routers that forces us to, and then only for the router's parameters.
- zmj 6y agoThe commonality is that both should flow across serialization boundaries, like RPC calls to downstream services.
- kevindong 6y ago> Creating a function that accepts a `ctx context.Context` and then making a blocking call that doesn't respect your context will still block for the entirety unless you instrument it yourself. A Golang service my work team owns has this problem. Virtually every single endpoint the service has contains a context as the first parameter... but the service never actually uses the context in the myriad of endpoints the service has.
- jrockway 6y agoThe big problem is that contexts are mostly mandatory if you want to be able to reason about the number of goroutines that are going to be around concurrently, but that the language syntax favors forgetting about them. For example, something like: go func() { ch <- foo } () Can easily block forever if you are no longer reading ch. Instead, you pretty much have to guard all channel reads and writes with: select { case <-ctx.Done(): return ctx.Err() case ch <- foo: } To some extent, this is all perfectly fair. Any programmer should see the first example and think "what causes this to return?" and won't be surprised when their program crashes every 2 weeks because it runs out of memory. But... it surprises everyone. I bet it might be the number one problem with Go, actually. The side effect of not planning to abort operations that will never complete is that people are super okay with using locking machinery that can't be aborted. You will see all sorts of things like WaitGroups or Mutexes inside operations that have an externally-limited lifespan, and then you get the same problem of running out of memory because you have a billion things waiting on some lock; waiting for their turn to do a computation and then discard the result because the caller disappeared long ago. Feels bad. (My number one Go pet peeve, btw, is mixing different types of locking machinery. Everyone should use channels pretty much 99.9% of the time, but should never lock a mutex before doing a channel write, or something... which I've seen.) I assume the expectation is that "something else" is supposed to make this all work. That's how it works in UNIX land. How does cat know to exit after printing 10 lines in a pipeline like "cat ... | head"? Easy; the OS kills it when head closes stdin. Unfortunately, nothing is sitting around killing your goroutines when someone presses the "stop" button in their browser. That is up to the application developer, i.e. you.
- morelisp 6y ago> Everyone should use channels pretty much 99.9% of the time This is the kind of thing that sounds nice in theory, but is not borne out as a good idea by research into what actually causes errors[0] nor is it how the standard library is actually implemented (context iself[1] mixes mutexes and channels liberally, you'll find similar code in http.Client and http.Server, sql.Tx and sql.DB, etc.). It is technically correct that you do not need to lock anything before sending a message down a channel. In practice, performance reasons and the anemic channel API means you often mix both. [0] https://blog.acolyer.org/2019/05/17/understanding-real-world-concurrency-bugs-in-go/ https://blog.acolyer.org/2019/05/17/understanding-real-world... [1] https://golang.org/src/context/context.go https://golang.org/src/context/context.go