There are some other choices they made that are arguably not the right ones - for example, async code can do some of its initial execution on the calling thread and do the rest wherever continuations get scheduled (which is configurable...) which means you have to have exception handling in two places and the way the exception handling works will be different (the article calls this out). It is possible to avoid this by having the initial call to the async function only create the task but not run any of it - of course, there are reasons not to do it, performance being one of them, so it makes sense that they did it... it's just bad to optimize by default instead of making code simpler and more reliable.
My least favorite decision is that inexplicably, async/await state machines are very error prone... if any part of your codebase accidentally invokes a continuation twice, the state machine will potentially begin running twice or even start running again from the beginning with the same local variables. Fixing this would have been as simple as setting a bool at the end and checking it at the beginning, but for some reason they are dead-set on not fixing it. Premature optimization once again.
The existence of 'async void' is also just a complete trainwreck. They shouldn't have allowed it, especially since an 'async Task' that discards its result is just as easy.
The approach to cancellation (intrusive only) is also needlessly complex and gross. Putting a Dispose method on a Task would allow consumers of any async API to cleanly signal that they no longer need the result of a Task and any implementation would be able to observe this without anyone having to introduce a new method overload that takes a CancellationToken, not to mention that the intrusive cancellation design creates extra garbage on the heap. Really not obvious to me why they did this instead of reusing 'using x' and IDisposable.
a := getFoo()
b := getBar()
c := getBaz()
If you want to do all 3 in parallel, you may need to use channels and wait groups and stuff. In a language with promises: const a = getFoo();
const b = getBar();
const c = getBaz();
await a
await b
await c
or even better const [a,b,c] = await Promise.all([getFoo(), getBar(), getBaz()])
If/when go generics become available, I expect to see some libraries that make things easier in golang.It is yet another example of impedance mismatch from guest languages, when the platform moves into another direction.
The platform language gets the true way, while the guest languages get the hard decision how to combine multiple approaches, and libraries that only use the new platform APIs.
But that management is the crux of the issue. The complexity of hopping into and out of contexts is what is exposed with async/await and coroutine scopes and hidden by the more simple syntax.