In Go, as long as you are not sharing data, you don't really have to care about whether something is concurrent or parallel, the runtime takes care of it for you. You just use goroutines, IO transparently happens on a dedicated threadpool, CPU heavy tasks are spread out over the available cores.
As long as you restrict sharing to sending owned data over channels, you are mostly in the clear. Of course sometimes performance concerns etc mandate more traditional tools (atomics, mutexes, etc) and there the compiler won't help you anymore, but I'd argue that is the exception in Go rather than the norm.
You can often solve problems with a actor-system inspired design. (learning Erlang/Elixir will probably make you a better Go developer!)
In Rust, you still have to care a lot about what you do. Things like tokio::fs can handle file system IO for you, but you need to ensure that code you are calling does no synchronous IO. Also, CPU heavy workloads should be offloaded to a dedicated CPU thread pool, since the tokio runtime is not intended for juggling CPU intensive tasks in addition to the async IO. This is fine for Rust, a language that wants to give you insight and control, but it is still much more demanding of the developer.
In Rust you can do that too! Just use threads. That's what Go does: it just uses threads.
Async I/O is there if you need better performance than what Rust—or Go—can provide. If you don't need that performance, then don't use async I/O.
As you know, Rust doesn't currently have stable support for a similarly high-level interface to async IO. See e.g. the following reddit thread from 3 months ago:
https://www.reddit.com/r/rust/comments/ausuld/what_is_the_cu...
Sure, modern Linux handles 10k threads just fine, but where Go shines are middle-level network services or microservices where you can easily have 10+ additional tasks for each incoming request. Using real threads quickly becomes unfeasible, and also more cumbersome (passing around threadpools, which threadpool do I use for this task?, do I really want to do those two things in parallel and eat up an extra thread? the cancellation issue, ....).
Also in Rust you'd need to rewrite a lot of code when switching from sync to async, and still need to delegate things to a threadpool since not every dependency will be async enabled.
When the ecosystem stabilizes Rust will be a great choice for the lower stack libraries and servers, where the performance matters. But for many projects the increased complexity, for moderate performance gains, would be a hard sell.
"It is practical to create hundreds of thousands of goroutines in the same address space. If goroutines were just threads, system resources would run out at a much smaller number."
More generally, Go is hardly the first language to implement userspace threads backed by OS threads. Erlang famously does this, for example, and so does GHC: https://en.wikibooks.org/wiki/Haskell/Concurrency
In response to your other comment, the whole point of Goroutines is that you can use them pretty freely without (usually) having to worry about how many there are. Let's say you're writing a server, and that handling one of the requests requires completing 10 tasks, each of which can run in parallel. In Go you can just spawn a Goroutine for each of those tasks. But if you were using real OS threads, then that implementation could easily end up spawning more than a few thousand threads, when the server was under significant load.
> But if you were using real OS threads, then that implementation could easily end up spawning more than a few thousand threads, when the server was under significant load.
There's no inherent problem with spawning a few thousand threads in Linux.
>If you have hundreds of thousands, you probably want true async I/O instead of having stackful threads at all.
This is a completely baseless assertion, and really makes no sense at all. "Async IO" in itself doesn’t even offer a means of exploiting multiple CPU cores. If "async IO" were all that was required, people would be sticking with Node.js for heavy loads.
>even 2kB of stack per thread is 195MB just from thread stacks.
You say this as if 195MB were a large amount of memory for a server to be using! Obviously, if the memory overhead of running 100,000 Goroutines is only 195MB, then memory limitations are not going to prevent you from running 100,000 Goroutines.
> "Async IO" in itself doesn’t even offer a means of exploiting multiple CPU cores.
Sure it does. Multithreaded epoll is the standard solution for such things; that's how nginx is multithreaded, for example. Node isn't multithreaded, but that's a Node problem, not an async I/O problem.
> then memory limitations are not going to prevent you from running 100,000 Goroutines.
100,000 Linux kernel threads with 8kB kernel stack + 2kB user stack is only 1GB, which is likewise tractable.
And that 2kB assumes that the goroutine stacks remain small, which is not always the case. They can and will grow based on the dynamic behavior of the program.
The rest of your comment is just a repeated insistence that "Go doesn't scale" without a shred of evidence in support. You really seem to have some kind of beef with Go that's clouding your judgment in this area.
>Multithreaded epoll is the standard solution for such things
Multithreaded epoll is exactly what Go provides. Rather than having to manage thread pools yourself, the Go runtime does it for you by distributing logical threads of execution over OS threads.
>We usually talk about high scalability as C10K; C100K is an extreme case.
You seem to be assuming one Goroutine per connection. As I said before, a more plausible scenario for 100,000 goroutines is C10K with 10 goroutines per connection.
The Rust compiler won't let you do that.
Getting concurrency _with_ parallelism right is a lot easier in rust than it is in go. You need to write thread safe code in both of them, but you get zero support from your compiler in go.