Comment by lukaslalinsky
11 hours ago
I was amazed how well are goroutines integrated into the language when I saw the first videos from Rob Pike. Then I actually started using Go for concurrent code, and noticed one thing, it's extremely easy to leak goroutines. There is no proper way to cancel them, they need to cooperate via select/context. Go developers eventually learn hacks to deal with it, but the simple go+chan style of programming style you see in tutorials is usually not safe. I still consider Go a remarkable piece of software. The runtime really doesn't have any seriously bad edge cases, it just works. But as a developer, I now prefer a slightly more explicit approach to concurrency. I've spent the last year developing an async runtime for Zig and I'm now more comfortable writing concurrent code in Zig than I was every using Go. I have more options for how to handle closed channels, I can cancel any operation, etc.
Never really thought about this but it seems to me that if you want to launch separate processes? Goroutines are built around functions, so you're just stuck with function semantics. If you want an entire process, you just invoke self with a feature flag on your binary and control a subprocess. If you need to communicate you establish your own message passing channels with STDIO or something.
I don't feel this is hacky or even a work around. Just different promises on what goroutines are vs threads/concurrency/processes in other languages.
Exit and panics are promised at the process level in Go.
One reason it's easy to leak goroutine is that channel producer blocks waiting on consumer, once channel consumer exits the producer goroutine leaks. Go doesn't allow consumer to close the channel.
In Rust when receivers all drop, the producer will error instead of blocking, so Rust is better in this aspect.
> There is no proper way to cancel them, they need to cooperate via select/context
Isn’t this also true of threads? I know you can usually cancel them from a thread handle, but that kills the thread ~immediately without cleaning anything up, right? Presumably you pretty much always want cooperative cancellation?
It's true for almost all pthread implementations, not all. But when talking about asynchronous I/O runtimes and coroutines, you have more options. Systems like Tokio, or zio (the one I'm working on), give you a task handle, and when you call `cancel()` on the handle, it will cancel whatever async operation the task is currently running. And it does so reliably.
These things were all known when development on Go began. But, as with so many other aspects of the language, if it wasn't known in the 80s/90s then it may as well not have existed.
1 reply →
Yup. A popular way to get proper cancellation is to build exceptions into the language and specifically async exceptions so one goroutine can throw an exception into another goroutine. And Go does not have exceptions. Doing so would require all regular Go code to be exception safe, and really requires some form of try/finally or RAII but not defer. Anyways exceptions are quite far from the Go creators’ vision of the language.