9 ms·
What canceled my Go context?
- ashishb 7mo agoContext cancellation (and it's propagation) is one of the best features in Go. Is there any equivalent in major popular languages like Python, Java, or JS of this?
- gzread 7mo agoNot really, since they don't have `select` There's a stop_token in some Microsoft C++ library but it's not nearly as convenient to interrupt a blocking operation with it.
- ndriscoll 7mo agoZIO in Scala tracks this sort of thing except you don't have to remember to pass around or select on the ctx (it's just part of the fibre/"goroutine"); if it's cancelled, the fibre and its children just stops the next time it yields (so e.g. if it "selects" on anything or does any kind of IO).
- nnx 7mo agoin JS, signals and AbortController can replicate some of the functionality but it's far less ergonomic than Go. https://github.com/ggoodman/context https://github.com/ggoodman/context provides nice helpers that brings the DX a bit closer to Go.
- drdaeman 7mo agoC# has CancellationToken, but it’s just for canceling operations, not a general purpose context.
- perfmode 7mo agoone of the reasons why i love writing control planes in Go.
- deathanatos 7mo agoPython async tasks can be cancelled. But, I don't think you can attach must context to the cancel (I think you can pass a text message), so it would seem the argument of what go suffered from would apply. (I also think there's some wonkiness with and barriers to understanding Python's implementation that I don't think plagues Go to quite the same extent.)
- NeutralForest 7mo agoThe current meta is to use task groups and bubble up the exception that cancelled the coroutine/task.
- lifis 7mo agoAll mainstream languages have it in one or more forms (either direct task I/O cancellation, or cancellation tokens or I/O polling that can include synthetic events) since otherwise several I/O patterns are impossible
- richbell 7mo agoKotlin Coroutine's structured concurrency. Cancelling a parent automatically cancels child jobs, unless explicitly handled not to. https://kotlinlang.org/docs/coroutines-basics.html https://kotlinlang.org/docs/coroutines-basics.html
- tadfisher 7mo agoStupidly, child cancellation cancels the parent scope as well, unless the scope opts out by including SupervisorJob.
- Quekid5 7mo agoJava's Virtual Threads (JVM 21) + the Structured Concurrency primitives (not sure exactly what's available in Java 21+) do this natively. Also, a sibling poster mentioned ZIO/Scala which does the Structured Concurrency thing out of the box.
- nh2 7mo agoHaskell is the king of cancellation. Using asynchronous exceptions, you can cancel anything, anytime, with user -defined exception types so you know what the cancellation reason is. Example: maybeVal <— timeout 1000000 myFunction Some people think that async exceptions are a pain because you nerd to be prepared that your code can be interrupted any time, but I think it's absolutely worth it because in all the other languages I encounter progress bars that keep running when I click the cancel button, or CLI programs that don't react to CTRL+C. In Haskell, cancellability is the default and carries no syntax overhead. This is one of the reasons why I think Haskell is currently the best language for writing IO programs.
- neonsunset 7mo ago[dead]
- lemoncucumber 7mo agoIt’s great that they identified this (incredibly common) pain point and introduced a way to solve it, but I can’t help being disappointed. Reading the examples I found myself thinking, “that looks like a really useful pattern, I should bookmark this so I can adopt it whenever I write code like that.” The fact that I’m considering bookmarking a blog post about complex boilerplate that I would want to use 100% of the times when it’s applicable is a huge red flag and is exactly why people complain about Go. It feels like you’re constantly fighting the language: having to add error handling boilerplate everywhere and having to pass contexts everywhere (more boilerplate). This is the intersection of those two annoyances so it feels especially annoying (particularly given the nuances/footguns the author describes). They say the point is that Go forces you to handle errors but 99% of the time that means just returning the error after possibly wrapping it. After a decade of writing Go I still don’t have a good rule of thumb for when I should wrap an error with more info or return it as-is. I hope someday they make another attempt at a Go 2.0.
- XorNot 7mo agoThere are two things I think you could have as implict in Go - error values, and contexts. Just pass along two hidden variables for both in parameters and returns, and would anything really change that the compiler wouldn't be able to follow? i.e. most functions return errors, so there should always be an implicit error return possible even if I don't use it. Let the compiler figure out if it needs to generate code for it. And same story for contexts: why shouldn't a Go program be a giant context tree? If a branch genuinely doesn't ever use it, the compiler should be able to just knock the code out.
- maleldil 7mo agoWhat's the difference between an implicit error and exceptions? Being explicit about errors is good. Go's syntactical implementation, coupled with its unexpressive type system, is the problem.
- XorNot 7mo agoI will freely go on the record as saying that there's nothing wrong with exceptions for this exact reason: errors are so common that a function being "pure" is the exception, and that errors-as-value handling invariable turns into an endless chain of something like "if err; return (nil/zero, err)" in every language which tries it. The same would apply to anytime you have Result types - ultimately its still just syntactic sugar over "if err then...". What's far more common in real programs is that an error can occur somewhere where you do not have enough context to handle or resolve it, or you're unaware it can happen. In which case the concept of exceptions is much more valid: "if <bad thing here> what do I want to do?" usually only has a couple of places you care about the answer (i.e. "bad thing happened during business process, so start unwinding that process" and many more where the answer is either "crash" or "log it and move on to the next item".
- gethly 7mo agoNever needed this. Nice to have but won't ever use it.
- NeutralForest 7mo agoYou're kind of building a stack of exceptional cases... Wonder what that is :D
- sethammons 7mo agoLe sigh. "I don't wrap errors and so I don't know where my errors come from." The code that justifies the special context handling: if err := chargePayment(ctx, orderID); err != nil { cancel(fmt.Errorf( "order %s: payment failed: %w", orderID, err, )) return err } Why not simply wrap that error with the same information?
- diarrhea 7mo agoI have the same question, I am confused by the premise of this article. Now you're recording everything twice?
- mstipetic 7mo agoGolang returning tuples but not having pattern matching is something I'll never get. I really feel it's a too dumbed down version of erlang/elixir, especially with this context passing business
- deleted 7mo ago[deleted]
- can3p 7mo agoI think this post needs better examples to show case the issue, because right now the issue is not clear. Ideally you would need an example that uses the context.Cause function, see below The contexts and errors communicate information in different directions. Errors let upstream function know what happened within the call, context lets downstream functions know what happened elsewhere in the system. As a consequence there isn't much point to cancel the context and return the error right away if there isn't anybody else listening to it. Also, context can be chained by definition. If you need to be able to cancel the context with a cause or cancel it with a timeout, you can just make two context and use them. Example that shows the approach as well as the specific issue raised by the post: https://go.dev/play/p/rpmqWJFQE05 https://go.dev/play/p/rpmqWJFQE05 Thanks for the post though! Made me think about contexts usage more
- diarrhea 7mo agoAgree. I am note sure I understand the premise of the article. You're now recording encountered errors twice, which can look like cancel(fmt.Errorf( "order %s: payment failed: %w", orderID, err, )) return fmt.Errorf("order %s: payment failed: %w, orderID, err) Not only that, isn't this a "lie"? You're cancelling the context explicitly, but that's not necessary is it? Because at the moment the above call fails, the called-into functions might not have cancelled the context. There might be cleanup running later on which will then refuse to run on this eagerly cancelled context. There is no need to cancel this eagerly. Perhaps I'm not seeing the problem being solved, but bog-standard `return err` with "lazy" context cancellation (in a top-level `defer cancel()`), or eager (in a leaf I/O goroutine) seems to carry similar functionality. Stacking both with ~identical information seems redundant.
- malklera 7mo agoI am new enough to programming that I actually have no concrete idea what a context is; I just use it when a library asks for it. > lemoncucumber > it can be tempting to keep adding layer upon layer of wrapping resulting in an unwieldy error string that’s practically a hand-rolled stacktrace I thought this was the whole reason to wrap errors, to know where they passed up the chain. Funny how it seems no matter the subject, if Go is involved, errors get discussed.
- neonsunset 7mo ago[dead]