10 ms·
An interesting mistake with Go's context package
- gravypod 6y agoVery interesting article. I've run into similar issues with Contexts (in gRPC + Java). It's been so bad they I've had to hide all interaction with contexts behind interfaces that only allow down stream users to call `getThing()` to pull things out of the contexts. Contexts seem like exceptions but backwards and with less syntactic sugar which feels like an extremely annoying design clash with golang's error handling semantics. It seems like a better way of handling this would be some dynamic thread/context aware DI system but that's also not very go-y. Does anyone have experience taming complexity here around this area?
- neilalexander 6y agoMostly I think of contexts as a way of helping you to deal with the fact that something might initiate some work but that you can’t necessarily guarantee that you’ll be given enough time to complete it. This might be because there’s a time limit/deadline or because some other condition can be cancelled. This is useful particularly in places where you are dealing with clients/servers over a network. An example of this is accepting an HTTP request from a client and then creating a response to that request. Ultimately, the client can ultimately give up waiting at any time and disconnect, so being given a request context allows you to deal with this situation - you can check if the context is cancelled at any time and abort whatever expensive operation you were midway through doing or about to do. Some internal packages, such as net/http and database/sql, accept contexts as a means of avoiding unnecessary work - they will just “no op” if the context has been cancelled by that point, or they may add some new information to the context and pass it downward. You can also check yourself if a context has been cancelled and you can even use the Done() function to break out of a “select” block early. Another important detail is that contexts are always limited by their parent context. If the parent gets cancelled, so will any child contexts (such as those created using WithTimeout or WithCancellation).
- georgethomas 6y agoI've experienced both: explicit passing of immutable Contexts in Go and using ThreadLocal storage in Java (DropWizard) apps. I think it was our mistake to use ThreadLocal storage for request-specific data. This directly couples threads (which are an OS / scheduling concern) to the serving of requests. We then ran into issues spawning new background threads, or naively switching to Kotlin Coroutines (which can be scheduled on a different thread). So I prefer the explicit, immutable context passing that Go uses. The `getThing()` pattern you mention seems reasonable and gives you some type safety that is otherwise missing with a bare `context.Value()` calls.
- gravypod 6y agoI think golang is in a unique position to make `ThreadLocal`-like communication-by-state a workable solution due to the go-routine process model. You can get the benefits of imitable-ness + type safety of `ThreadLocal`-ness with some simple maneuvering. This could be accomplished by treating "context" like in-process environment variables. Essentially: 1. My Threadlet Variables are immutable to me. 2. I can create children Threadlets (by default) inherit all of my Threadlet Variables. 3. I can modify the Threadlet Variables of my children Threadlets. This could look something like this (Java + Golang mashup): class User { ... } class UserSingleton implements ThreadletLocal { // implements static set() and static get() and reset() or something } requestMiddleware(request, next) { go next(request), Context.with( UserSingleton.set(extractUserFrom(request.headers)), DeadlineThreadletLocal.within(1 second) ); } This idea is much more refined and further developed in Laravel's facade APIs. They can bind an object dynamically to a "class" and change that underlying implementation dynamically. The benefit of this approach is you can actively distinguish between: 1. who is using your the User class and 2. who is injecting/receiving a User class statically. This allows you to automatically track your "DI". You could also make some syntactic sugar for this like: class UserSettingsController { @ProvidedBy(UserSingleton.class) Optional<User> user; .... some code here who talks to user .... } From a Java perspective I think this should be possible post-Loom.
- cle 6y agoI disagree that they’re like exceptions. Sure they share state but that’s not a core characteristic of exceptions. Contexts leave it up the consumer to decide when to care about state changes, something exceptions definitely do not. Contexts are complicated because concurrency is complicated. You can translate that complexity into other complexity like your Java example (to keep code idiomatic and unsurprising), but the intrinsic complexity doesn’t really go away. You can build higher level abstractions that add constraints on what you can do (eg structured concurrency) in order to hide the complexity, but that’s at a different abstraction layer than contexts.
- sudhirj 6y agoOne of the things I miss in Go is immutability. Here is the context object is trying to be kept immutable with the pattern of returning a copy of the object with the mutation, which is pretty great but needs to be popular in the ecosystem to use it effectively.
- cle 6y agoIt’s not about immutability, it’s about cascading contexts, all the Context “With” functions create children that are automatically cancelled when any ancestor is cancelled.
- morelisp 6y agoIt is about immutability, at least in the visible API. The stdlib context functions all only expose the `context.Context` interface which has no methods to mutate it. Any "changes" to a context can only be done by making a child context, which also ensures the caller's desired deadline/timeout semantics are enforced as each layer can only add cancellation conditions; similarly you cannot remove values, only replace them. (Though internally there are some nasty optimizations to chain cancellation with minimal channel waits.)
- zmj 6y agoAren't contexts supposed to flow mutations down the call stack, not up it?
- nkozyra 6y agoThat was my first impression and why I guessed nil value.
- travmatt 6y agoMy second thought was that go will pass the struct by value, so even if it didn’t crash that member would not have been set in the parent function.
- TheDong 6y agoSince the argument is "context.Context", an interface type, it's not necessarily passing a value. It is actually a pointer since the implementation, if you look at the definition of `context.TODO()`, is of type `* context.emptyContext`. Looking at just a function definition, you can't know if a variable is a value or a pointer unless you first determine if it's an interface type, and if it is an interface type, you have to know the concrete type of the caller to know. However, even a struct passed by value can be mutated since interior mutability can be managed with internal pointers. For example: type value struct { interior *int } func(v value) mutate(to int) { *v.interior = to } func(v value) String() string { return fmt.Sprintf("%d", *v.interior) } func newValue(i int) value { return value{&i} } func mutateValue(v value, to int) { v.mutate(to) } func main() { val := newValue(10) mutateValue(val, 20) fmt.Println(val) }
- tsimionescu 6y agoAren't all interface variables pointers in Go, regardless of whether the type implementing the interface is a pointer or a value?
- 6y ago
- mperham 6y agoThe Context package is tricky to use correctly. I got bit because I designed an API that assumed you could embed a Context within a larger struct. Because it's immutable you can't and have to pass it as a value everywhere. It wasn't fun having to break backwards compatibility to fix that mistake.
- AlexCoventry 6y agoNot sure what you mean by "embed a Context within a larger struct." It's certainly possible to have a struct field of type context.Context.
- sagichmal 6y agoPossible, but usually a bad idea.
- mperham 6y agoI didn’t say it wasn’t possible, I meant could as in “safe to do so”. Read the package docs, it explicitly says not to do that.
- morelisp 6y agoIt says not to, but the reason is because you will probably not handle context semantics correctly, not because it inherently breaks anything. If the object you embed the context in has all the properties of a context (short-lived, request-scoped, passed to every function that needs to handle it, and provides a way to create a nested context) then embedding the context is at worst unidiomatic. This is how http.Request did it to preserve backwards-compatibility; http.Handler usage was too pervasive to ever change its signature.
- Nullabillity 6y agoAt the risk of sounding like a broken record, Rust attacks this problem from multiple avenues... 1. You can tag a type or function as #[must_use], either of which makes discarding a value trigger a warning. This would have caught the (equivalent of the) example code. 2. Futures provide pretty much everything that Context does, but without having to thread a magical value correctly through everything. Explicit cancellation can be had just by.. dropping the Future. This means that you can get timeouts by racing with a timer Future. Arbitrary values (as in the example) are slightly more verbose, but you can have a decorator Future that sets a thread-local for the duration of the poll.
- Thaxll 6y agoIf don't find decorator / tags in programming elegant, it polutes your code so much.
- oh_sigh 6y agoHow would you like to tag functions or things with properties (like the "return value must be used" property?
- bigdubs 6y agoGodoc. This less applies to something needing to be "used" as much as something needing to have an action taken in a `defer` like `.Close()` etc.
- BoorishBears 6y agoWhat's wrong with formalizing a way to have tooling give that information at the call site? There's no reason the directive couldn't be compatible with Godoc either? The only argument I can see is that it's a "slippery slope", but that's a pretty tired argument, you can use it against most any sort of progress in a language.
- 6y ago
- cfors 6y agoGo'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.
- throw_m239339 6y agoIf the Go team made Goroutines types like ADA tasks, nobody would have to deal with any of the problems people using Goroutines have to deal with. This is yet another short sighted design decision made by Go designers. There would be no need for a context package at first place. The Go team tried once again to solve a language design problem with a library, it didn't work.
- masklinn 6y agoCould you explain how Ada tasks work and why they solve the issues the context package is intended for, for those who don't know ada?
- a1369209993 6y agoSeconded, that sounds interesting and a brief search didn't turn up anything promising.
- johnisgood 6y agoI do not know if this is anything close to what you want, but: https://news.ycombinator.com/item?id=19245898 https://news.ycombinator.com/item?id=19245898 and https://news.ycombinator.com/item?id=23609499 https://news.ycombinator.com/item?id=23609499 might be of use.
- networkimprov 6y agoContexts are generally regarded as a workaround for the fact that Go doesn't offer goroutine-local storage -- a design feature that's often criticized.
- ohnoesjmr 6y agoGoroutine local storage is the stack? If you don't want to push values down as arguments and want some global state to access it, your code is probably horrible already. Pushing down a context as a value container instead of actual values is horrible too.
- shorsher 6y agoHere's a question from someone who is recently out of school and somewhat new to Go. Is it OK to use Context for request-scoped values? i.e. passing data between middleware? I have started doing this, but is there a better alternative? Peter Bourgon's blog post[0] about context made me think it is fine. After reading the comments here, I am not so sure. Especially since his post was roughly 4 years ago. 0: https://peter.bourgon.org/blog/2016/07/11/context.html https://peter.bourgon.org/blog/2016/07/11/context.html
- ChrisFoster 6y agoI liked the following blog post where Jack Lindamood argues pretty convincingly against using Context.Value in nearly all circumstances, and gives some nice alternatives. https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39 https://medium.com/@cep21/how-to-correctly-use-context-conte...
- earthboundkid 6y agoChris’s blog is general quite good, but I thought this entry was weak. I guess he doesn’t use context much for his devops stuff. If you use context at all, you know that it doesn’t mutate in place. That’s why it’s goroutine safe. It’s the whole point of using context. The answer to the pop quiz was obvious, and I only second guessed myself because I thought it must be a trick question somehow. Like “well obviously you can’t mutate a context but maybe context.TODO is broken somehow”.
- shirro 6y agoFor a language which seems to have been intended initially for writing maintainable network services the absence of a means to set timeouts or cancel requests in the original network apis was clearly an oversight. I like what they have proposed with generics and other recent changes and I would like to see a fresh look at this problem. Context is working as intended here as it returns a copy which is well documented. Cancellation and timeouts are useful but WithValue seems like a nasty hack to me. I think it should be avoided and possibly deprecated.
- dvt 6y agoThis is actually kind of funny (and true) as I was the one that implemented sensible HTTP timeouts in the standard library after a server I wrote oddly kept eternally open-connections[1]. [1] https://github.com/golang/go/issues/213 https://github.com/golang/go/issues/213