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?
Mostly 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).
I'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.
I 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.
I 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.
One 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.
It’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.
It 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.)
IDK, having never seen or used Go's contexts, the `With` prefix immediately made me think that it likely created a new context: it's a common naming convention in Rust or Python.
I'd have expected `SetValue` for updating keys in-place.
That `WithValue` creates an "inheritance" relationship surprised me a lot more, that I did not expect.
Since 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)
}
Interfaces are internally pointers, insofar as the value passed when you pass around an interface is a pointer to something so that the value passed is a fixed size regardless of the underlying type (otherwise you couldn't have any arrays of interfaces).
But if the interface is implemented by a pointer-to-T and you pass a T, you will get an error; and if you have wrapped a T in one interface and try to convert it to another interface that is only satisfied by a pointer-to-T, the conversion will fail.
So the answer to "are interfaces are always pointers?" is yes if you're asking about implementing the Go compiler or worried about the size of some allocation; but no if you are asking about Go's type system.
The 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.
It 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.
At 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.
What'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.
What would the goal be; for the compiler to warn if a type requires a method to be called on defer? What if you want to let the caller of the caller handle that? You would then need pragmas to silence the warning, which is something that the authors specifically don't want.
> for the compiler to warn if a type requires a method to be called on defer?
This seems a bigger ask than what's upthread and than what's necessary in most cases. You just need to make sure the return value is not implicitly discarded. Go linters already do this for `error`.
I have used both __checkReturn and __attribute__ ((warn_unused_result)) in C projects before and both were helpful, easy to reason about when the warning occurred, easy to work-around in the rare cases I really truly did want to drop the result (in Go `_ = ...` is already idiomatic), and useful in semantically varied cases (resource allocation, error handling, pure functions).
Explicit cancellation can be had just by.. dropping the Future.
Its great to have a Rust expert to turn up in a Go thread to educate the masses.
I have one question from the cheap seats though. How do you drop the Future, with a decision made at runtime, given that lifetimes are part of the type system?
> I have one question from the cheap seats though. How do you drop the Future, with a decision made at runtime, given that lifetimes are part of the type system?
Trivially, something like
let future = some_long_running_async_function();
let timer = delay(Duration::from_millis(10000));
select! {
result = future => {
// handle result of future completing successfully
},
_ = timer => {
// future missed the deadline, drop it, terminating execution.
std::mem::drop(future);
}
}
If this were wrapped in a function you could also just return out of the timer branch and let the future naturally drop as the stack popped.
> Futures provide pretty much everything that Context does, but without having to thread a magical value correctly through everything.
It has downsides though:
* Instead you have to use async/await throughout your code and suddenly have two execution models that you need to reason about
* Dropping the future won't help if the call is hanging on a blocking call either
* The callee doesn't know anything about a possible deadline
* You can only handle the cancellation in the callee in `Drop`, which gets complicated quickly if you need to rollback changes, because you need to track state manually
* After all the use of 'context' is optional. You only need to pass it if you need cancellation, whereas in async Rust you always need to keep in mind that you might get canceled.
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!
The 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.
At 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).
Or 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.
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.
I'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.
I 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.
Most 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]
The 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.
> 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.
The 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.
> 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.
Yeah, the code in context around locking the close of the done channel is a pretty common solution to a problem people often run into. The problem is that <-closedCh works perfectly -- it tells you that the channel is closed, but close(closedCh) and closedCh<- panic. There are a lot of hacks around that but the reality is -- you can't have an API where you intend for someone to close a channel that someone else is writing to. Only the writer can close the channel.
The core library's code kind of demonstrates the tradeoff; they use three concurrency primitives (!) in the same function to provide the API they want. The question is whether or not ordinary application code wants to debug that. It's worth it when it's being run billions of times a day by hundreds of thousands of people. It might not be in your one-off application.
I also worry that in the common case, people don't even consider the idiomatic solution before bringing out the esoteric solutions. I used to use a library that did this:
type counter struct { sync.RWMutex; value int64 }
func (c *counter) Inc() { c.Lock(); value++; c.Unlock() }
func (c *counter) Get() int64 { c.RLock(); defer c.RUnlock(); return value }
I had a few hundred goroutines that all contended on the write lock, and throughput was basically nothing. I think I was using less than half a CPU on a 128 thread machine, and the workers did actual CPU-intensive work... but not when they were asleep waiting for this mutex.
I eventually rewrote the monitoring library to use atomic.AddInt64(&counter, 1), and the throughput came back.
But in retrospect, I sometimes wonder about the actual costs of this. The hardware write barrier is somewhat insidious in that your profile won't show you "hey, everything is sitting here asleep" as it will with a mutex; instead your cache performance is subtly reduced and you'll never be quite sure how fast it could have been if you didn't have the synchronization there.
The ultimate best performance likely comes from doing a little more work and having a less-nice API:
type counter struct { in, out chan int64; value int64 }
func (c *counter) watch() {
for {
select {
case x := <-c.in:
value += x
case c.out <- value:
}
}
}
func (c *counter) Get() int64 { return <- c.out }
// elsewhere
go c.watch()
// in the writers
...
var buf int64 // goroutine-local counter
buf++
select {
case c.in <- buf:
buf = 0
default:
buf++
}
...
I have never bothered with this for counters, but this pattern shows up pretty regularly in other things. I have used this in streaming RPCs, where I could tolerate a little bit of latency in exchange for putting more data into one message. (One goroutine sends the message when it's big enough or enough time has passed; the writers just add messages to the channel or accumulate them for the next RPC.) It looks exactly like Nagle's algorithm for TCP, but at the application level.
Anyway, my conclusion from all of this is that application developers want something higher level than any of (atomic.*, sync.Mutex, or channels). They want multi-producer/multi-consumer, message queues, condvars, etc. Having only primitives result in a lot of wheel-reinvention by people that haven't thought enough about how wheels work.
I also want to put a rant in here about "buffer bloat" and how people will, by default, tolerate unbounded latency and unbounded memory usage to put something in a buffer instead of saying "the system cannot answer your query right now" or reducing throughput in a benchmark. It's a big problem on the Internet -- your SSH session and video conferences consistently lag because the designers of the system aimed to please reviewers, who only test throughp...
If 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.
Contexts are generally regarded as a workaround for the fact that Go doesn't offer goroutine-local storage -- a design feature that's often criticized.
Goroutine 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.
Pushing down 100 arguments to every function that might call something else in the call chain is horrible too, so what are you going to do?
A key-value container? Sounds like context, or goroutine-local storage with superfluous boilerplate.
A "request" object containing all the values? Sounds like goroutine-local storage by another name, just more verbose.
Restrict every function in the program to pass exactly the values used by any of its possible callee descendants? Sounds brittle, you'll never get anything finished.
> Sounds brittle, you'll never get anything finished.
This is really where we are? "Lexical scoping makes programming impossible"?
I have written over a dozen Go services totaling at least 50k lines of code. They all use context extensively, including at least 3 custom context types. I have not once put anything in a Value context.
Things I want to pass, either:
- Just pass them. Yes, some functions near the top have "too many" arguments and it sucks, but it sucks less than context k/v lookups.
- Pass a configuration object. This means passing at most an argument per layer, rather than an argument per field. Type-safe, faster than a context, overall usually less boilerplate than recasting a context value.
- Call bound methods. This is the "global context" you actually want in most code, containing "my net.Listener", "my sql.DB", etc. Type-safe, faster than a context, no boilerplate.
> This is really where we are? "Lexical scoping makes programming impossible"?
No.
"Pass exactly the values" is a subset of lexical scoping, which is much more brittle than lexical scoping in general.
> - Pass a configuration object. This means passing at most an argument per layer, rather than an argument per field. Type-safe, faster than a context, overall usually less boilerplate than recasting a context value.
So, a God-object then, what I called "request object". That's regarded by some as an anti-pattern, even though it's type safe.
> overall usually less boilerplate than recasting a context value.
When comparing with context, I agree.
But when comparing with goroutine local storage, which the comment was about, a configuration object passed everywhere is more boilerplate not less.
> bound methods.
I'm not clear on what you have in mind by bound methods, unless the values are lexicals who values must remain the same across all concurrent goroutines.
If that's so, "my sql.DB" probably uses thread-local storage when it needs a usable DB handle anyway, and both that and "my net.Listener" are effectively global variables, potentially breaking modular re-use within a single process.
Chris’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”.
For 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.
This 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].
Here'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.
I liked the following blog post where Jack Lindamood argues pretty convincingly against using Context.Value in nearly all circumstances, and gives some nice alternatives.
72 comments
[ 3.6 ms ] story [ 119 ms ] threadContexts 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?
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).
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.
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):
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: From a Java perspective I think this should be possible post-Loom.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.
(Though internally there are some nasty optimizations to chain cancellation with minimal channel waits.)
I'd have expected `SetValue` for updating keys in-place.
That `WithValue` creates an "inheritance" relationship surprised me a lot more, that I did not expect.
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:
But if the interface is implemented by a pointer-to-T and you pass a T, you will get an error; and if you have wrapped a T in one interface and try to convert it to another interface that is only satisfied by a pointer-to-T, the conversion will fail.
So the answer to "are interfaces are always pointers?" is yes if you're asking about implementing the Go compiler or worried about the size of some allocation; but no if you are asking about Go's type system.
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.
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.
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.
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.
This seems a bigger ask than what's upthread and than what's necessary in most cases. You just need to make sure the return value is not implicitly discarded. Go linters already do this for `error`.
I have used both __checkReturn and __attribute__ ((warn_unused_result)) in C projects before and both were helpful, easy to reason about when the warning occurred, easy to work-around in the rare cases I really truly did want to drop the result (in Go `_ = ...` is already idiomatic), and useful in semantically varied cases (resource allocation, error handling, pure functions).
Once upon a time it automatically retried many syscalls after EINTR, but it dropped that design flaw.
Its great to have a Rust expert to turn up in a Go thread to educate the masses.
I have one question from the cheap seats though. How do you drop the Future, with a decision made at runtime, given that lifetimes are part of the type system?
Thanks for your help.
Trivially, something like
If this were wrapped in a function you could also just return out of the timer branch and let the future naturally drop as the stack popped.It has downsides though:
* Instead you have to use async/await throughout your code and suddenly have two execution models that you need to reason about
* Dropping the future won't help if the call is hanging on a blocking call either
* The callee doesn't know anything about a possible deadline
* You can only handle the cancellation in the callee in `Drop`, which gets complicated quickly if you need to rollback changes, because you need to track state manually
* After all the use of 'context' is optional. You only need to pass it if you need cancellation, whereas in async Rust you always need to keep in mind that you might get canceled.
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
IMHO it's a rock and a hard place; either you break compatibility, or you have a slightly more confusing API.
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.
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
[2] https://github.com/golang/go/issues/5636#issuecomment-661926...
Go has no way to obtain the Id of that thread. That's what the issue I linked above covers.
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.
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.
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.
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.
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... [1] https://golang.org/src/context/context.go
The core library's code kind of demonstrates the tradeoff; they use three concurrency primitives (!) in the same function to provide the API they want. The question is whether or not ordinary application code wants to debug that. It's worth it when it's being run billions of times a day by hundreds of thousands of people. It might not be in your one-off application.
I also worry that in the common case, people don't even consider the idiomatic solution before bringing out the esoteric solutions. I used to use a library that did this:
I had a few hundred goroutines that all contended on the write lock, and throughput was basically nothing. I think I was using less than half a CPU on a 128 thread machine, and the workers did actual CPU-intensive work... but not when they were asleep waiting for this mutex.I eventually rewrote the monitoring library to use atomic.AddInt64(&counter, 1), and the throughput came back.
But in retrospect, I sometimes wonder about the actual costs of this. The hardware write barrier is somewhat insidious in that your profile won't show you "hey, everything is sitting here asleep" as it will with a mutex; instead your cache performance is subtly reduced and you'll never be quite sure how fast it could have been if you didn't have the synchronization there.
The ultimate best performance likely comes from doing a little more work and having a less-nice API:
I have never bothered with this for counters, but this pattern shows up pretty regularly in other things. I have used this in streaming RPCs, where I could tolerate a little bit of latency in exchange for putting more data into one message. (One goroutine sends the message when it's big enough or enough time has passed; the writers just add messages to the channel or accumulate them for the next RPC.) It looks exactly like Nagle's algorithm for TCP, but at the application level.Anyway, my conclusion from all of this is that application developers want something higher level than any of (atomic.*, sync.Mutex, or channels). They want multi-producer/multi-consumer, message queues, condvars, etc. Having only primitives result in a lot of wheel-reinvention by people that haven't thought enough about how wheels work.
I also want to put a rant in here about "buffer bloat" and how people will, by default, tolerate unbounded latency and unbounded memory usage to put something in a buffer instead of saying "the system cannot answer your query right now" or reducing throughput in a benchmark. It's a big problem on the Internet -- your SSH session and video conferences consistently lag because the designers of the system aimed to please reviewers, who only test throughp...
https://news.ycombinator.com/item?id=19245898
and
https://news.ycombinator.com/item?id=23609499
might be of use.
A key-value container? Sounds like context, or goroutine-local storage with superfluous boilerplate.
A "request" object containing all the values? Sounds like goroutine-local storage by another name, just more verbose.
Restrict every function in the program to pass exactly the values used by any of its possible callee descendants? Sounds brittle, you'll never get anything finished.
This is really where we are? "Lexical scoping makes programming impossible"?
I have written over a dozen Go services totaling at least 50k lines of code. They all use context extensively, including at least 3 custom context types. I have not once put anything in a Value context.
Things I want to pass, either:
- Just pass them. Yes, some functions near the top have "too many" arguments and it sucks, but it sucks less than context k/v lookups.
- Pass a configuration object. This means passing at most an argument per layer, rather than an argument per field. Type-safe, faster than a context, overall usually less boilerplate than recasting a context value.
- Call bound methods. This is the "global context" you actually want in most code, containing "my net.Listener", "my sql.DB", etc. Type-safe, faster than a context, no boilerplate.
No.
"Pass exactly the values" is a subset of lexical scoping, which is much more brittle than lexical scoping in general.
> - Pass a configuration object. This means passing at most an argument per layer, rather than an argument per field. Type-safe, faster than a context, overall usually less boilerplate than recasting a context value.
So, a God-object then, what I called "request object". That's regarded by some as an anti-pattern, even though it's type safe.
> overall usually less boilerplate than recasting a context value.
When comparing with context, I agree.
But when comparing with goroutine local storage, which the comment was about, a configuration object passed everywhere is more boilerplate not less.
> bound methods.
I'm not clear on what you have in mind by bound methods, unless the values are lexicals who values must remain the same across all concurrent goroutines.
If that's so, "my sql.DB" probably uses thread-local storage when it needs a usable DB handle anyway, and both that and "my net.Listener" are effectively global variables, potentially breaking modular re-use within a single process.
This generalises nicely to "If Go had paid attention to literally any lesson we've learnt about programming languages since approximately the 80's".
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”.
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.
[1] https://github.com/golang/go/issues/213
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://medium.com/@cep21/how-to-correctly-use-context-conte...