4 ms·
Very 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 behi
by gravypod 6y ago
Very 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.